├── .gitignore ├── sysd.go ├── go.mod ├── journald ├── prefixes.go └── slog │ └── handler.go ├── LICENSE ├── go.sum ├── notify ├── notify.go └── watchdog │ └── watchdog.go ├── README.md └── resolved ├── resolver_test.go ├── resolver.go └── dbus.go /.gitignore: -------------------------------------------------------------------------------- 1 | # VSCode 2 | .vscode 3 | -------------------------------------------------------------------------------- /sysd.go: -------------------------------------------------------------------------------- 1 | package sysd 2 | 3 | import "os" 4 | 5 | // GetInvocationID returns the systemd invocation ID. 6 | // If exists is false, we have not been launched by systemd. 7 | // Present since systemd v232: https://github.com/systemd/systemd/blob/v232/NEWS#L254 8 | func GetInvocationID() (ID string, exists bool) { 9 | return os.LookupEnv("INVOCATION_ID") 10 | } 11 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/iguanesolutions/go-systemd/v6 2 | 3 | go 1.22.0 4 | 5 | toolchain go1.23.2 6 | 7 | require ( 8 | github.com/godbus/dbus/v5 v5.1.0 9 | github.com/miekg/dns v1.1.62 10 | golang.org/x/net v0.31.0 11 | ) 12 | 13 | require ( 14 | golang.org/x/mod v0.22.0 // indirect 15 | golang.org/x/sync v0.9.0 // indirect 16 | golang.org/x/sys v0.27.0 // indirect 17 | golang.org/x/text v0.20.0 // indirect 18 | golang.org/x/tools v0.27.0 // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /journald/prefixes.go: -------------------------------------------------------------------------------- 1 | package sysdjournald 2 | 3 | const ( 4 | // EmergPrefix is the string to prefix in Emergency for systemd-journald 5 | EmergPrefix = "<0>" 6 | // AlertPrefix is the string to prefix in Alert for systemd-journald 7 | AlertPrefix = "<1>" 8 | // CritPrefix is the string to prefix in Critical for systemd-journald 9 | CritPrefix = "<2>" 10 | // ErrPrefix is the string to prefix in Error for systemd-journald 11 | ErrPrefix = "<3>" 12 | // WarningPrefix is the string to prefix in Warning for systemd-journald 13 | WarningPrefix = "<4>" 14 | // NoticePrefix is the string to prefix in Notice for systemd-journald 15 | NoticePrefix = "<5>" 16 | // InfoPrefix is the string to prefix in Info for systemd-journald 17 | InfoPrefix = "<6>" 18 | // DebugPrefix is the string to prefix in Debug for systemd-journald 19 | DebugPrefix = "<7>" 20 | ) 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Iguane Solutions 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /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 | github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= 4 | github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= 5 | golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= 6 | golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 7 | golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= 8 | golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= 9 | golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= 10 | golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 11 | golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= 12 | golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 13 | golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= 14 | golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= 15 | golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o= 16 | golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q= 17 | -------------------------------------------------------------------------------- /notify/notify.go: -------------------------------------------------------------------------------- 1 | package sysdnotify 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "os" 7 | ) 8 | 9 | var socket *net.UnixAddr 10 | 11 | func init() { 12 | if notifySocketName := os.Getenv("NOTIFY_SOCKET"); notifySocketName != "" { 13 | socket = &net.UnixAddr{ 14 | Name: notifySocketName, 15 | Net: "unixgram", 16 | } 17 | } 18 | } 19 | 20 | // IsEnabled tells if systemd notify socket has been detected or not. 21 | func IsEnabled() bool { 22 | return socket != nil 23 | } 24 | 25 | // Ready sends systemd notify READY=1 26 | func Ready() error { 27 | return Send("READY=1") 28 | } 29 | 30 | // Reloading sends systemd notify RELOADING=1 31 | func Reloading() error { 32 | return Send("RELOADING=1") 33 | } 34 | 35 | // Stopping sends systemd notify STOPPING=1 36 | func Stopping() error { 37 | return Send("STOPPING=1") 38 | } 39 | 40 | // Status sends systemd notify STATUS=%s{status} 41 | func Status(status string) error { 42 | return Send(fmt.Sprintf("STATUS=%s", status)) 43 | } 44 | 45 | // ErrNo sends systemd notify ERRNO=%d{errno} 46 | func ErrNo(errno int) error { 47 | return Send(fmt.Sprintf("ERRNO=%d", errno)) 48 | } 49 | 50 | // BusError sends systemd notify BUSERROR=%s{buserror} 51 | func BusError(buserror string) error { 52 | return Send(fmt.Sprintf("BUSERROR=%s", buserror)) 53 | } 54 | 55 | // MainPID sends systemd notify MAINPID=%d{mainpid} 56 | func MainPID(mainpid int) error { 57 | return Send(fmt.Sprintf("MAINPID=%d", mainpid)) 58 | } 59 | 60 | // WatchDog sends systemd notify WATCHDOG=1 61 | func WatchDog() error { 62 | return Send("WATCHDOG=1") 63 | } 64 | 65 | // WatchDogUSec sends systemd notify WATCHDOG_USEC=%d{µsec} 66 | func WatchDogUSec(usec int64) error { 67 | return Send(fmt.Sprintf("WATCHDOG_USEC=%d", usec)) 68 | } 69 | 70 | // Send state thru the notify socket if any. 71 | // If the notify socket was not detected, it is a noop call. 72 | // Use IsEnabled() to determine if the notify socket has been detected. 73 | func Send(state string) error { 74 | if socket == nil { 75 | return nil 76 | } 77 | conn, err := net.DialUnix(socket.Net, nil, socket) 78 | if err != nil { 79 | return fmt.Errorf("can't open unix socket: %v", err) 80 | } 81 | defer conn.Close() 82 | if _, err = conn.Write([]byte(state)); err != nil { 83 | return fmt.Errorf("can't write into the unix socket: %v", err) 84 | } 85 | return nil 86 | } 87 | -------------------------------------------------------------------------------- /notify/watchdog/watchdog.go: -------------------------------------------------------------------------------- 1 | package sysdwatchdog 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "strconv" 8 | "time" 9 | 10 | sysdnotify "github.com/iguanesolutions/go-systemd/v6/notify" 11 | ) 12 | 13 | // WatchDog is an interface to the systemd watchdog mechanism 14 | type WatchDog struct { 15 | interval time.Duration 16 | checks time.Duration 17 | } 18 | 19 | // New returns an initialized and ready to use WatchDog 20 | func New() (wd *WatchDog, err error) { 21 | // Check WatchDog is supported and usable 22 | interval, err := getWatchDogInterval() 23 | if err != nil { 24 | return 25 | } 26 | // Return the initialized controller 27 | wd = &WatchDog{ 28 | interval: interval, 29 | checks: interval / 2, 30 | } 31 | return 32 | } 33 | 34 | // based on https://github.com/coreos/go-systemd/blob/master/daemon/watchdog.go 35 | func getWatchDogInterval() (interval time.Duration, err error) { 36 | // WATCHDOG_USEC 37 | wusec := os.Getenv("WATCHDOG_USEC") 38 | if wusec == "" { 39 | err = errors.New("watchdog does not seem to be enabled: WATCHDOG_USEC env unset") 40 | return 41 | } 42 | wusecTyped, err := strconv.Atoi(wusec) 43 | if err != nil { 44 | err = fmt.Errorf("can't convert WATCHDOG_USEC as int: %s", err) 45 | return 46 | } 47 | if wusecTyped <= 0 { 48 | err = fmt.Errorf("WATCHDOG_USEC must be a positive number") 49 | return 50 | } 51 | interval = time.Duration(wusecTyped) * time.Microsecond 52 | // WATCHDOG_PID 53 | wpid := os.Getenv("WATCHDOG_PID") 54 | if wpid == "" { 55 | return // No WATCHDOG_PID: can't check if we are the one, let's go with it 56 | } 57 | wpidTyped, err := strconv.Atoi(wpid) 58 | if err != nil { 59 | err = fmt.Errorf("can't convert WATCHDOG_PID as int: %s", err) 60 | return 61 | } 62 | if os.Getpid() != wpidTyped { 63 | err = fmt.Errorf("WATCHDOG_PID is %d and we are %d: we are not the watched PID", wpidTyped, os.Getpid()) 64 | } 65 | return 66 | } 67 | 68 | // SendHeartbeat sends a keepalive notification to systemd watchdog 69 | func (c *WatchDog) SendHeartbeat() error { 70 | if !sysdnotify.IsEnabled() { 71 | return errors.New("failed to notify watchdog: systemd notify is diabled") 72 | } 73 | return sysdnotify.WatchDog() 74 | } 75 | 76 | // GetChecksDuration returns the ideal time for a client to perform (active or passive collect) checks. 77 | // Is is equal at 1/3 of watchdogInterval 78 | func (c *WatchDog) GetChecksDuration() time.Duration { 79 | return c.checks 80 | } 81 | 82 | // GetLimitDuration returns the systemd watchdog limit provided by systemd 83 | func (c *WatchDog) GetLimitDuration() time.Duration { 84 | return c.interval 85 | } 86 | 87 | // NewTicker initializes and returns a ticker set at watchdogChecks (which is set at 1/3 of watchdogInterval). 88 | // It can be used by clients to trigger checks before using SendHeartbeat(). 89 | func (c *WatchDog) NewTicker() *time.Ticker { 90 | return time.NewTicker(c.checks) 91 | } 92 | -------------------------------------------------------------------------------- /journald/slog/handler.go: -------------------------------------------------------------------------------- 1 | package sysdjournaldslog 2 | 3 | import ( 4 | "fmt" 5 | "log/slog" 6 | "os" 7 | "strings" 8 | 9 | sysdjournald "github.com/iguanesolutions/go-systemd/v6/journald" 10 | ) 11 | 12 | const ( 13 | // https://wiki.archlinux.org/title/Systemd/Journal#Priority_level 14 | LevelDebug = slog.LevelDebug 15 | LevelDebugStr = "DEBUG" 16 | LevelInfo = slog.LevelInfo 17 | LevelInfoStr = "INFO" 18 | LevelNotice = LevelInfo + 2 19 | LevelNoticeStr = "NOTICE" 20 | LevelWarning = slog.LevelWarn 21 | LevelWarningStr = "WARNING" 22 | LevelError = slog.LevelError 23 | LevelErrorStr = "ERROR" 24 | LevelCritical = LevelError + 2 25 | LevelCriticalStr = "CRITICAL" 26 | LevelAlert = LevelCritical + 2 27 | LevelAlertStr = "ALERT" 28 | // LevelEmergency should not be used by applications 29 | LevelEmergency = LevelAlert + 2 30 | LevelEmergencyStr = "EMERGENCY" 31 | ) 32 | 33 | const ( 34 | prefixDebugStr = sysdjournald.DebugPrefix + slog.LevelKey 35 | prefixInfoStr = sysdjournald.InfoPrefix + slog.LevelKey 36 | prefixNoticeStr = sysdjournald.NoticePrefix + slog.LevelKey 37 | prefixWarningStr = sysdjournald.WarningPrefix + slog.LevelKey 38 | prefixErrorStr = sysdjournald.ErrPrefix + slog.LevelKey 39 | prefixCriticalStr = sysdjournald.CritPrefix + slog.LevelKey 40 | prefixAlertStr = sysdjournald.AlertPrefix + slog.LevelKey 41 | prefixEmergencyStr = sysdjournald.EmergPrefix + slog.LevelKey 42 | ) 43 | 44 | // GetAvailableLogLevels returns a list of available log levels that can be used by GetLogLevel() 45 | func GetAvailableLogLevels() []string { 46 | return []string{ 47 | LevelDebugStr, 48 | LevelInfoStr, 49 | LevelNoticeStr, 50 | LevelWarningStr, 51 | LevelErrorStr, 52 | LevelCriticalStr, 53 | LevelAlertStr, 54 | LevelEmergencyStr, 55 | } 56 | } 57 | 58 | // GetLogLevel returns a log level based on the given string. If the string is not recognized, it will return LevelInfo. 59 | func GetLogLevel(raw string) slog.Leveler { 60 | switch strings.ToUpper(raw) { 61 | case LevelDebugStr: 62 | return LevelDebug 63 | case LevelInfoStr: 64 | return LevelInfo 65 | case LevelNoticeStr: 66 | return LevelNotice 67 | case LevelWarningStr: 68 | return LevelWarning 69 | case LevelErrorStr: 70 | return LevelError 71 | case LevelCriticalStr: 72 | return LevelCritical 73 | case LevelAlertStr: 74 | return LevelAlert 75 | case LevelEmergencyStr: 76 | return LevelEmergency 77 | default: 78 | return LevelInfo 79 | } 80 | } 81 | 82 | // NewHandler returns a new slog handler that writes logs in a journald compatible/enhanced format. 83 | func NewHandler(opts slog.HandlerOptions) slog.Handler { 84 | return slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ 85 | Level: opts.Level, 86 | AddSource: opts.AddSource, 87 | ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { 88 | switch a.Key { 89 | case slog.TimeKey: 90 | // Remove time from the output as journald will add its own timestamp and 91 | // we want the level first for journald marker to be effective 92 | return slog.Attr{} 93 | case slog.LevelKey: 94 | // Customize the name of the level key for pretty printing and the output string, 95 | // including custom level values 96 | level := a.Value.Any().(slog.Level) 97 | switch { 98 | case level < LevelInfo: 99 | a.Key = prefixDebugStr 100 | a.Value = slog.StringValue(str(LevelDebugStr, level-LevelDebug)) 101 | case level < LevelNotice: 102 | a.Key = prefixInfoStr 103 | a.Value = slog.StringValue(str(LevelInfoStr, level-LevelInfo)) 104 | case level < LevelWarning: 105 | a.Key = prefixNoticeStr 106 | a.Value = slog.StringValue(str(LevelNoticeStr, level-LevelNotice)) 107 | case level < LevelError: 108 | a.Key = prefixWarningStr 109 | a.Value = slog.StringValue(str(LevelWarningStr, level-LevelWarning)) 110 | case level < LevelCritical: 111 | a.Key = prefixErrorStr 112 | a.Value = slog.StringValue(str(LevelErrorStr, level-LevelError)) 113 | case level < LevelAlert: 114 | a.Key = prefixCriticalStr 115 | a.Value = slog.StringValue(str(LevelCriticalStr, level-LevelCritical)) 116 | case level < LevelEmergency: 117 | a.Key = prefixAlertStr 118 | a.Value = slog.StringValue(str(LevelAlertStr, level-LevelAlert)) 119 | default: 120 | a.Key = prefixEmergencyStr 121 | a.Value = slog.StringValue(str(LevelEmergencyStr, level-LevelEmergency)) 122 | } 123 | default: 124 | if opts.ReplaceAttr != nil { 125 | a = opts.ReplaceAttr(groups, a) 126 | } 127 | } 128 | // This key does not need modification, return it as is. 129 | return a 130 | }, 131 | }) 132 | } 133 | 134 | func str(base string, val slog.Level) string { 135 | if val == 0 { 136 | return base 137 | } 138 | return fmt.Sprintf("%s%+d", base, val) 139 | } 140 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-systemd 2 | 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/iguanesolutions/go-systemd/v5)](https://goreportcard.com/report/github.com/iguanesolutions/go-systemd/v5) [![PkgGoDev](https://pkg.go.dev/badge/github.com/iguanesolutions/go-systemd/v5)](https://pkg.go.dev/github.com/iguanesolutions/go-systemd/v5) 4 | 5 | Easily communicate with systemd when run as daemon within a service unit. 6 | 7 | ## journald 8 | 9 | [![PkgGoDev](https://pkg.go.dev/badge/github.com/iguanesolutions/go-systemd/v5/journald)](https://pkg.go.dev/github.com/iguanesolutions/go-systemd/v5/journald) 10 | 11 | ### slog handler 12 | 13 | [![PkgGoDev](https://pkg.go.dev/badge/github.com/iguanesolutions/go-systemd/v5/journald/slog)](https://pkg.go.dev/github.com/iguanesolutions/go-systemd/v5/journald/slog) 14 | 15 | This package provides a systemd journald compatible [log/slog handler](https://pkg.go.dev/log/slog#Handler). 16 | 17 | In a nutshell it: 18 | * removes slog timestamp as journald will have its own 19 | * provides journald additionnal log levels 20 | * indicate the log level to journald in order for it to colorize the log message when showing it with `journalctl` 21 | 22 | #### Installation 23 | 24 | ```bash 25 | go get -v "github.com/iguanesolutions/go-systemd/v5/journald/slog" 26 | ``` 27 | 28 | #### Example 29 | 30 | ```go 31 | package main 32 | 33 | import ( 34 | "log/slog" 35 | "os" 36 | "strings" 37 | 38 | sysd "github.com/iguanesolutions/go-systemd/v5" 39 | sysdjournaldslog "github.com/iguanesolutions/go-systemd/v5/journald/slog" 40 | ) 41 | 42 | const ENVVAR_LOGLEVEL = "LOG_LEVEL" 43 | 44 | func main() { 45 | logger := slog.New(GetAppropriateSlogHandler()) 46 | logger.Debug("shhhh...") 47 | logger.Info("Noice") 48 | logger.Warn("Try me with and without systemd!") 49 | logger.Error("Ouch") 50 | } 51 | 52 | func GetAppropriateSlogHandler() (loggerHandler slog.Handler) { 53 | // Get raw log level 54 | levelStr := os.Getenv(ENVVAR_LOGLEVEL) 55 | // Check if we need to return a systemd handler 56 | _, sysdStarted := sysd.GetInvocationID() 57 | if sysdStarted { 58 | return sysdjournaldslog.NewHandler(sysdjournaldslog.GetLogLevel(levelStr)) 59 | } 60 | // Return regular text handler if not started by systemd 61 | var level slog.Level 62 | switch strings.ToUpper(levelStr) { 63 | case sysdjournaldslog.LevelDebugStr: 64 | level = slog.LevelDebug 65 | case sysdjournaldslog.LevelInfoStr: 66 | level = slog.LevelInfo 67 | case sysdjournaldslog.LevelWarningStr: 68 | level = slog.LevelWarn 69 | case sysdjournaldslog.LevelErrorStr: 70 | level = slog.LevelError 71 | default: 72 | level = slog.LevelInfo 73 | } 74 | return slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ 75 | Level: level, 76 | }) 77 | } 78 | ``` 79 | 80 | 81 | ## Notifier 82 | 83 | [![PkgGoDev](https://pkg.go.dev/badge/github.com/iguanesolutions/go-systemd/v5/notify)](https://pkg.go.dev/github.com/iguanesolutions/go-systemd/v5/notify) 84 | 85 | With notifier you can notify to systemd that your program is starting, stopping, reloading... 86 | 87 | For example, if your daemon needs some time for initializing its controllers before really being considered as ready, you can specify to systemd that this is a "notify" service and send it a notification when ready. 88 | 89 | It is safe to use it even if systemd notify support is disabled (noop call). 90 | 91 | ```systemdunit 92 | [Service] 93 | Type=notify 94 | ``` 95 | 96 | ```go 97 | import ( 98 | sysdnotify "github.com/iguanesolutions/go-systemd/v5/notify" 99 | ) 100 | 101 | // Init http server 102 | server := &http.Server{ 103 | Addr: "host:port", 104 | Handler: myHTTPHandler, 105 | } 106 | 107 | /* 108 | Do some more inits 109 | */ 110 | 111 | // Notify ready to systemd 112 | if err = sysdnotify.Ready(); err != nil { 113 | log.Printf("failed to notify ready to systemd: %v\n", err) 114 | } 115 | 116 | // Start the server 117 | if err = server.ListenAndServe(); err != nil { 118 | log.Printf("failed to start http server: %v\n", err) 119 | } 120 | ``` 121 | 122 | When stopping, you can notify systemd that you have indeed received the SIGTERM and you have launched the stop procedure 123 | 124 | ```go 125 | import ( 126 | sysdnotify "github.com/iguanesolutions/go-systemd/v5/notify" 127 | ) 128 | 129 | // Notify to systemd that we are stopping 130 | var err error 131 | if err = sysdnotify.Stopping(); err != nil { 132 | log.Printf("failed to notify stopping to systemd: %v\n", err) 133 | } 134 | 135 | /* 136 | Stop others things 137 | */ 138 | 139 | // Stop the server (with timeout) 140 | ctx, cancelCtx := context.WithTimeout(context.Background(), 5*time.Second) 141 | defer cancelCtx() 142 | if err = server.Shutdown(ctx); err != nil { 143 | log.Printf("failed to shutdown http server: %v\n", err) 144 | } 145 | ``` 146 | 147 | You can also notify status to systemd 148 | 149 | ```go 150 | import ( 151 | sysdnotify "github.com/iguanesolutions/go-systemd/v5/notify" 152 | ) 153 | 154 | if err := sysdnotify.Status(fmt.Sprintf("There is currently %d active connections", activeConns)); err != nil { 155 | log.Printf("failed to notify status to systemd: %v\n", err) 156 | } 157 | 158 | ``` 159 | 160 | systemctl status output example: 161 | 162 | ```systemctlstatus 163 | user@host:~$ systemctl status superapp.service 164 | ● superapp.service - superapp 165 | Loaded: loaded (/lib/systemd/system/superapp.service; enabled) 166 | Active: active (running) since Mon 2018-06-25 08:54:35 UTC; 3 days ago 167 | Main PID: 2604 (superapp) 168 | Status: "There is currently 1506 active connections" 169 | ... 170 | ``` 171 | 172 | ### Watchdog 173 | 174 | [![PkgGoDev](https://pkg.go.dev/badge/github.com/iguanesolutions/go-systemd/v5/notify/watchdog)](https://pkg.go.dev/github.com/iguanesolutions/go-systemd/v5/notify/watchdog) 175 | 176 | ```systemdunit 177 | [Service] 178 | Type=notify 179 | WatchdogSec=30s 180 | ``` 181 | 182 | ```go 183 | import ( 184 | sysdwatchdog "github.com/iguanesolutions/go-systemd/v5/notify/watchdog" 185 | ) 186 | 187 | // Init systemd watchdog, same as the notifier, it can be nil if your os does not support it 188 | watchdog, err := sysdwatchdog.New() 189 | if err != nil { 190 | log.Printf("failed to initialize systemd watchdog controller: %v\n", err) 191 | } 192 | 193 | if watchdog != nil { 194 | // Then start a watcher worker 195 | go func() { 196 | ticker := watchdog.NewTicker() 197 | defer ticker.Stop() 198 | for { 199 | select { 200 | // Ticker chan 201 | case <-ticker.C: 202 | // Check if something wrong, if not send heartbeat 203 | if allGood { 204 | if err = watchdog.SendHeartbeat(); err != nil { 205 | log.Printf("failed to send systemd watchdog heartbeat: %v\n", err) 206 | } 207 | } 208 | // Some stop signal chan 209 | case <-stopSig: 210 | return 211 | } 212 | } 213 | }() 214 | } 215 | ``` 216 | 217 | ## Resolved 218 | 219 | [![PkgGoDev](https://pkg.go.dev/badge/github.com/iguanesolutions/go-systemd/resolved/resolved)](https://pkg.go.dev/github.com/iguanesolutions/go-systemd/v5/resolved) 220 | 221 | This package is still under development and very experimental, do not use it in production. 222 | We started this package in order to go deep into the DNS world. So we are opened to any suggestions/contributions on this. 223 | DNS is not trivial at all so there can be some stuff that are not rfc compliant (like sorting addresses etc...). 224 | 225 | The resolved package features: 226 | * Pure Go implementation of `org.freedesktop.resolve1` dbus interface 227 | * Resolver type (which uses the underlying dbus interface) that tries to implement the same methods as `net.Resolver` from Go standard library 228 | * Unit tests (make sure Go resolver and systemd-resolved query the same dns server) 229 | 230 | ### Dbus 231 | 232 | The following example shows how to use the resolve1 dbus connection to resolve an host: 233 | 234 | ```go 235 | package main 236 | 237 | import ( 238 | "context" 239 | "fmt" 240 | "log" 241 | "syscall" 242 | 243 | "github.com/iguanesolutions/go-systemd/v5/resolved" 244 | ) 245 | 246 | func main() { 247 | c, err := resolved.NewConn() 248 | if err != nil { 249 | log.Fatal("ERROR: ", err) 250 | } 251 | ctx := context.Background() 252 | addrs, canonical, flags, err := c.ResolveHostname(ctx, 0, "google.com", syscall.AF_UNSPEC, 0) 253 | if err != nil { 254 | log.Println("ERROR: ", err) 255 | } else { 256 | fmt.Println("Addresses: ", addrs) 257 | fmt.Println("Canonical: ", canonical) 258 | fmt.Println("OutputFlags: ", flags) 259 | } 260 | err = c.Close() 261 | if err != nil { 262 | log.Println("ERROR: ", err) 263 | } 264 | } 265 | ``` 266 | 267 | Output: 268 | 269 | ```output 270 | Addresses: [{ 271 | IfIndex: 2, 272 | Family: 2, 273 | IP: 142.250.74.238, 274 | } { 275 | IfIndex: 2, 276 | Family: 10, 277 | IP: 2a00:1450:4007:80b::200e, 278 | }] 279 | Canonical: google.com 280 | Flags: 1 281 | ``` 282 | 283 | ### Resolver 284 | 285 | The following example shows how to use the resolved Resolver to resolve an host: 286 | 287 | ```go 288 | package main 289 | 290 | import ( 291 | "context" 292 | "fmt" 293 | "log" 294 | 295 | "github.com/iguanesolutions/go-systemd/v5/resolved" 296 | ) 297 | 298 | func main() { 299 | r, err := resolved.NewResolver() 300 | if err != nil { 301 | log.Fatal("ERROR: ", err) 302 | } 303 | ctx := context.Background() 304 | addrs, err := r.LookupHost(ctx, "google.com") 305 | if err != nil { 306 | log.Println("ERROR: ", err) 307 | } else { 308 | fmt.Println("Addresses: ", addrs) 309 | } 310 | err = r.Close() 311 | if err != nil { 312 | log.Println("ERROR: ", err) 313 | } 314 | } 315 | ``` 316 | 317 | Output: 318 | 319 | ```output 320 | Addresses: [2a00:1450:4007:80b::200e 142.250.74.238] 321 | ``` 322 | 323 | ### HTTP Client 324 | 325 | The following example shows how to use the systemd-resolved Resolver with the Go http client from the standard library: 326 | 327 | ```go 328 | package main 329 | 330 | import ( 331 | "fmt" 332 | "log" 333 | "net/http" 334 | 335 | "github.com/iguanesolutions/go-systemd/v5/resolved" 336 | ) 337 | 338 | func main() { 339 | r, err := resolved.NewResolver() 340 | if err != nil { 341 | log.Fatal("ERROR: ", err) 342 | } 343 | // if you want to make a custom http client using systemd-resolved as resolver 344 | httpCli := &http.Client{ 345 | Transport: &http.Transport{ 346 | DialContext: r.DialContext, 347 | }, 348 | } 349 | // or if you don't have an http client you can call HTTPClient method on resolver 350 | // it comes with some nice default values. 351 | httpCli = r.HTTPClient() 352 | resp, err := httpCli.Get("https://google.com") 353 | if err != nil { 354 | log.Println("ERROR: ", err) 355 | } else { 356 | fmt.Println("Status: ", resp.Status) 357 | err = resp.Body.Close() 358 | if err != nil { 359 | log.Println("ERROR: ", err) 360 | } 361 | } 362 | err = r.Close() 363 | if err != nil { 364 | log.Println("ERROR: ", err) 365 | } 366 | } 367 | ``` 368 | 369 | Output: 370 | 371 | ```output 372 | Status: 200 OK 373 | ``` 374 | -------------------------------------------------------------------------------- /resolved/resolver_test.go: -------------------------------------------------------------------------------- 1 | package resolved 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "net/http" 7 | "sort" 8 | "testing" 9 | ) 10 | 11 | // In order to run the test make sure that systemd-resolved resolver query the same dns server as the go one. 12 | 13 | const ( 14 | lookupHost = "google.com" 15 | lookupAddr4 = "142.250.178.142" 16 | lookupAddr6 = "2a00:1450:4007:81a::200e" 17 | lookupCNAMEHost = "en.wikipedia.org" 18 | lookupSRVDomain = "google.com" 19 | lookupSRVService = "xmpp-server" 20 | lookupSRVProto = "tcp" 21 | lookupSRVServiceDomain = "_xmpp-server._tcp.google.com" 22 | getUrl = "https://google.com" 23 | ) 24 | 25 | func TestLookupHost(t *testing.T) { 26 | sysdResolver, err := NewResolver() 27 | if err != nil { 28 | t.Fatal(err) 29 | } 30 | defer sysdResolver.Close() 31 | ctx := context.Background() 32 | sysdAddrs, err := sysdResolver.LookupHost(ctx, lookupHost) 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | goResolver := &net.Resolver{} 37 | goAddrs, err := goResolver.LookupHost(ctx, lookupHost) 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | if len(goAddrs) != len(sysdAddrs) { 42 | t.Fatal("len(goAddrs) != len(sysdAddrs)", len(goAddrs), len(sysdAddrs)) 43 | } 44 | sort.Strings(sysdAddrs) 45 | sort.Strings(goAddrs) 46 | for i, sAddr := range sysdAddrs { 47 | goAddr := goAddrs[i] 48 | if goAddr != sAddr { 49 | t.Error("goAddr != sAddr", goAddr, sAddr) 50 | } 51 | } 52 | } 53 | 54 | func TestLookupAddr4(t *testing.T) { 55 | sysdResolver, err := NewResolver() 56 | if err != nil { 57 | t.Fatal(err) 58 | } 59 | defer sysdResolver.Close() 60 | ctx := context.Background() 61 | sysdNames, err := sysdResolver.LookupAddr(ctx, lookupAddr4) 62 | if err != nil { 63 | t.Fatal(err) 64 | } 65 | goResolver := &net.Resolver{} 66 | goNames, err := goResolver.LookupAddr(ctx, lookupAddr4) 67 | if err != nil { 68 | t.Fatal(err) 69 | } 70 | if len(goNames) != len(sysdNames) { 71 | t.Fatal("len(goNames) != len(sysdNames)", len(goNames), len(sysdNames)) 72 | } 73 | sort.Strings(goNames) 74 | sort.Strings(sysdNames) 75 | for i, sName := range sysdNames { 76 | goName := goNames[i] 77 | if goName != sName { 78 | t.Error("goName != sName", goName, sName) 79 | } 80 | } 81 | } 82 | 83 | func TestLookupAddr6(t *testing.T) { 84 | sysdResolver, err := NewResolver() 85 | if err != nil { 86 | t.Fatal(err) 87 | } 88 | defer sysdResolver.Close() 89 | ctx := context.Background() 90 | sysdNames, err := sysdResolver.LookupAddr(ctx, lookupAddr6) 91 | if err != nil { 92 | t.Fatal(err) 93 | } 94 | goResolver := &net.Resolver{} 95 | goNames, err := goResolver.LookupAddr(ctx, lookupAddr6) 96 | if err != nil { 97 | t.Fatal(err) 98 | } 99 | if len(goNames) != len(sysdNames) { 100 | t.Fatal("len(goNames) != len(sysdNames)", len(goNames), len(sysdNames)) 101 | } 102 | sort.Strings(goNames) 103 | sort.Strings(sysdNames) 104 | for i, sName := range sysdNames { 105 | goName := goNames[i] 106 | if goName != sName { 107 | t.Error("goName != sName", goName, sName) 108 | } 109 | } 110 | } 111 | 112 | func TestLookupIP(t *testing.T) { 113 | sysdResolver, err := NewResolver() 114 | if err != nil { 115 | t.Fatal(err) 116 | } 117 | defer sysdResolver.Close() 118 | ctx := context.Background() 119 | sysdAddrs, err := sysdResolver.LookupIP(ctx, "ip", lookupHost) 120 | if err != nil { 121 | t.Fatal(err) 122 | } 123 | goResolver := &net.Resolver{} 124 | goAddrs, err := goResolver.LookupIP(ctx, "ip", lookupHost) 125 | if err != nil { 126 | t.Fatal(err) 127 | } 128 | if len(goAddrs) != len(sysdAddrs) { 129 | t.Fatal("len(goAddrs) != len(sysdAddrs)", len(goAddrs), len(sysdAddrs)) 130 | } 131 | sort.Slice(sysdAddrs, func(i, j int) bool { 132 | return sysdAddrs[i].String() < sysdAddrs[j].String() 133 | }) 134 | sort.Slice(goAddrs, func(i, j int) bool { 135 | return goAddrs[i].String() < goAddrs[j].String() 136 | }) 137 | for i, sAddr := range sysdAddrs { 138 | goAddr := goAddrs[i] 139 | if goAddr.String() != sAddr.String() { 140 | t.Error("goAddr != sAddr", goAddr, sAddr) 141 | } 142 | } 143 | } 144 | 145 | func TestLookupIP4(t *testing.T) { 146 | sysdResolver, err := NewResolver() 147 | if err != nil { 148 | t.Fatal(err) 149 | } 150 | defer sysdResolver.Close() 151 | ctx := context.Background() 152 | sysdAddrs, err := sysdResolver.LookupIP(ctx, "ip4", lookupHost) 153 | if err != nil { 154 | t.Fatal(err) 155 | } 156 | goResolver := &net.Resolver{} 157 | goAddrs, err := goResolver.LookupIP(ctx, "ip4", lookupHost) 158 | if err != nil { 159 | t.Fatal(err) 160 | } 161 | if len(goAddrs) != len(sysdAddrs) { 162 | t.Fatal("len(goAddrs) != len(sysdAddrs)", len(goAddrs), len(sysdAddrs)) 163 | } 164 | sort.Slice(sysdAddrs, func(i, j int) bool { 165 | return sysdAddrs[i].String() < sysdAddrs[j].String() 166 | }) 167 | sort.Slice(goAddrs, func(i, j int) bool { 168 | return goAddrs[i].String() < goAddrs[j].String() 169 | }) 170 | for i, sAddr := range sysdAddrs { 171 | goAddr := goAddrs[i] 172 | if goAddr.String() != sAddr.String() { 173 | t.Error("goAddr != sAddr", goAddr, sAddr) 174 | } 175 | } 176 | } 177 | 178 | func TestLookupIP6(t *testing.T) { 179 | sysdResolver, err := NewResolver() 180 | if err != nil { 181 | t.Fatal(err) 182 | } 183 | defer sysdResolver.Close() 184 | ctx := context.Background() 185 | sysdAddrs, err := sysdResolver.LookupIP(ctx, "ip6", lookupHost) 186 | if err != nil { 187 | t.Fatal(err) 188 | } 189 | goResolver := &net.Resolver{} 190 | goAddrs, err := goResolver.LookupIP(ctx, "ip6", lookupHost) 191 | if err != nil { 192 | t.Fatal(err) 193 | } 194 | if len(goAddrs) != len(sysdAddrs) { 195 | t.Fatal("len(goAddrs) != len(sysdAddrs)", len(goAddrs), len(sysdAddrs)) 196 | } 197 | sort.Slice(sysdAddrs, func(i, j int) bool { 198 | return sysdAddrs[i].String() < sysdAddrs[j].String() 199 | }) 200 | sort.Slice(goAddrs, func(i, j int) bool { 201 | return goAddrs[i].String() < goAddrs[j].String() 202 | }) 203 | for i, sAddr := range sysdAddrs { 204 | goAddr := goAddrs[i] 205 | if goAddr.String() != sAddr.String() { 206 | t.Error("goAddr != sAddr", goAddr, sAddr) 207 | } 208 | } 209 | } 210 | 211 | func TestLookupIPAddr(t *testing.T) { 212 | sysdResolver, err := NewResolver() 213 | if err != nil { 214 | t.Fatal(err) 215 | } 216 | defer sysdResolver.Close() 217 | ctx := context.Background() 218 | sysdAddrs, err := sysdResolver.LookupIPAddr(ctx, lookupHost) 219 | if err != nil { 220 | t.Fatal(err) 221 | } 222 | goResolver := &net.Resolver{} 223 | goAddrs, err := goResolver.LookupIPAddr(ctx, lookupHost) 224 | if err != nil { 225 | t.Fatal(err) 226 | } 227 | if len(goAddrs) != len(sysdAddrs) { 228 | t.Fatal("len(goAddrs) != len(sysdAddrs)", len(goAddrs), len(sysdAddrs)) 229 | } 230 | sort.Slice(sysdAddrs, func(i, j int) bool { 231 | return sysdAddrs[i].String() < sysdAddrs[j].String() 232 | }) 233 | sort.Slice(goAddrs, func(i, j int) bool { 234 | return goAddrs[i].String() < goAddrs[j].String() 235 | }) 236 | for i, sAddr := range sysdAddrs { 237 | goAddr := goAddrs[i] 238 | if goAddr.String() != sAddr.String() { 239 | t.Error("goAddr != sAddr", goAddr, sAddr) 240 | } 241 | if goAddr.Zone != sAddr.Zone { 242 | t.Error("goAddr .Zone!= sAddr.Zone", goAddr.Zone, sAddr.Zone) 243 | } 244 | } 245 | } 246 | 247 | func TestLookupCNAME(t *testing.T) { 248 | sysdResolver, err := NewResolver() 249 | if err != nil { 250 | t.Fatal(err) 251 | } 252 | defer sysdResolver.Close() 253 | ctx := context.Background() 254 | sysdCNAME, err := sysdResolver.LookupCNAME(ctx, lookupCNAMEHost) 255 | if err != nil { 256 | t.Fatal(err) 257 | } 258 | goResolver := &net.Resolver{} 259 | goCNAME, err := goResolver.LookupCNAME(ctx, lookupCNAMEHost) 260 | if err != nil { 261 | t.Fatal(err) 262 | } 263 | if goCNAME != sysdCNAME { 264 | t.Error("goCNAME != sysdCNAME", goCNAME, sysdCNAME) 265 | } 266 | } 267 | 268 | func TestLookupMX(t *testing.T) { 269 | sysdResolver, err := NewResolver() 270 | if err != nil { 271 | t.Fatal(err) 272 | } 273 | defer sysdResolver.Close() 274 | ctx := context.Background() 275 | sysdMxs, err := sysdResolver.LookupMX(ctx, lookupHost) 276 | if err != nil { 277 | t.Fatal(err) 278 | } 279 | goResolver := &net.Resolver{} 280 | goMxs, err := goResolver.LookupMX(ctx, lookupHost) 281 | if err != nil { 282 | t.Fatal(err) 283 | } 284 | if len(goMxs) != len(sysdMxs) { 285 | t.Fatal("len(goMxs) != len(sysdMxs)", len(goMxs), len(sysdMxs)) 286 | } 287 | for i, sMx := range sysdMxs { 288 | goMx := goMxs[i] 289 | if goMx.Host != sMx.Host { 290 | t.Error("goMx.Host != sMx.Host", goMx.Host, sMx.Host) 291 | } 292 | if goMx.Pref != sMx.Pref { 293 | t.Error("goMx.Pref != sMx.Pref", goMx.Pref, sMx.Pref) 294 | } 295 | } 296 | } 297 | 298 | func TestLookupNS(t *testing.T) { 299 | sysdResolver, err := NewResolver() 300 | if err != nil { 301 | t.Fatal(err) 302 | } 303 | defer sysdResolver.Close() 304 | ctx := context.Background() 305 | sysdNss, err := sysdResolver.LookupNS(ctx, lookupHost) 306 | if err != nil { 307 | t.Fatal(err) 308 | } 309 | goResolver := &net.Resolver{} 310 | goNss, err := goResolver.LookupNS(ctx, lookupHost) 311 | if err != nil { 312 | t.Fatal(err) 313 | } 314 | if len(goNss) != len(sysdNss) { 315 | t.Fatal("len(goNss) != len(sysdNss)", len(goNss), len(sysdNss)) 316 | } 317 | sort.Slice(sysdNss, func(i, j int) bool { 318 | return sysdNss[i].Host < sysdNss[j].Host 319 | }) 320 | sort.Slice(goNss, func(i, j int) bool { 321 | return goNss[i].Host < goNss[j].Host 322 | }) 323 | for i, sNs := range sysdNss { 324 | goNs := goNss[i] 325 | if goNs.Host != sNs.Host { 326 | t.Error("goNs.Host != sNs.Host", goNs.Host, sNs.Host) 327 | } 328 | } 329 | } 330 | 331 | func TestLookupSRV(t *testing.T) { 332 | sysdResolver, err := NewResolver() 333 | if err != nil { 334 | t.Fatal(err) 335 | } 336 | defer sysdResolver.Close() 337 | ctx := context.Background() 338 | sysdCNAME, sysdSrvs, err := sysdResolver.LookupSRV(ctx, lookupSRVService, lookupSRVProto, lookupSRVDomain) 339 | if err != nil { 340 | t.Fatal(err) 341 | } 342 | goResolver := &net.Resolver{} 343 | goCNAME, goSrvs, err := goResolver.LookupSRV(ctx, lookupSRVService, lookupSRVProto, lookupSRVDomain) 344 | if err != nil { 345 | t.Fatal(err) 346 | } 347 | if sysdCNAME != goCNAME { 348 | t.Fatal("sysdCNAME != goCNAME", sysdCNAME, goCNAME) 349 | } 350 | if len(goSrvs) != len(sysdSrvs) { 351 | t.Fatal("len(goSrvs) != len(sysdSrvs)", len(goSrvs), len(sysdSrvs)) 352 | } 353 | sort.Slice(sysdSrvs, func(i, j int) bool { 354 | return sysdSrvs[i].Target < sysdSrvs[j].Target 355 | }) 356 | sort.Slice(goSrvs, func(i, j int) bool { 357 | return goSrvs[i].Target < goSrvs[j].Target 358 | }) 359 | for i, sSrv := range sysdSrvs { 360 | goSrv := goSrvs[i] 361 | if goSrv.Target != sSrv.Target { 362 | t.Error("goSrv.Target != sSrv.Target", goSrv.Target, sSrv.Target) 363 | } 364 | if goSrv.Port != sSrv.Port { 365 | t.Error("goSrv.Port != sSrv.Port", goSrv.Port, sSrv.Port) 366 | } 367 | if goSrv.Priority != sSrv.Priority { 368 | t.Error("goSrv.Priority != sSrv.Priority", goSrv.Priority, sSrv.Priority) 369 | } 370 | if goSrv.Weight != sSrv.Weight { 371 | t.Error("goSrv.Weight != sSrv.Weight", goSrv.Weight, sSrv.Weight) 372 | } 373 | } 374 | } 375 | 376 | func TestLookupTXT(t *testing.T) { 377 | sysdResolver, err := NewResolver() 378 | if err != nil { 379 | t.Fatal(err) 380 | } 381 | defer sysdResolver.Close() 382 | ctx := context.Background() 383 | sysdTxts, err := sysdResolver.LookupTXT(ctx, lookupHost) 384 | if err != nil { 385 | t.Fatal(err) 386 | } 387 | goResolver := &net.Resolver{} 388 | goTxts, err := goResolver.LookupTXT(ctx, lookupHost) 389 | if err != nil { 390 | t.Fatal(err) 391 | } 392 | if len(goTxts) != len(sysdTxts) { 393 | t.Fatal("len(goTxts) != len(sysdTxts)", len(goTxts), len(sysdTxts)) 394 | } 395 | sort.Strings(sysdTxts) 396 | sort.Strings(goTxts) 397 | for i, sTxt := range sysdTxts { 398 | goTxt := goTxts[i] 399 | if goTxt != sTxt { 400 | t.Error("goTxt != sTxt", goTxt, sTxt) 401 | } 402 | } 403 | } 404 | 405 | func BenchmarkLookupHostGoResolver(b *testing.B) { 406 | r := &net.Resolver{} 407 | ctx := context.Background() 408 | for n := 0; n < b.N; n++ { 409 | _, err := r.LookupHost(ctx, lookupHost) 410 | if err != nil { 411 | b.Error(err) 412 | continue 413 | } 414 | } 415 | } 416 | 417 | func BenchmarkLookupHostSystemdResolver(b *testing.B) { 418 | r, err := NewResolver() 419 | if err != nil { 420 | b.Fatal(err) 421 | } 422 | defer r.Close() 423 | ctx := context.Background() 424 | for n := 0; n < b.N; n++ { 425 | _, err := r.LookupHost(ctx, lookupHost) 426 | if err != nil { 427 | b.Error(err) 428 | continue 429 | } 430 | } 431 | } 432 | 433 | func BenchmarkLookupAddrGoResolver(b *testing.B) { 434 | r := &net.Resolver{} 435 | ctx := context.Background() 436 | for n := 0; n < b.N; n++ { 437 | _, err := r.LookupAddr(ctx, lookupAddr4) 438 | if err != nil { 439 | b.Error(err) 440 | continue 441 | } 442 | } 443 | } 444 | 445 | func BenchmarkLookupAddrSystemdResolver(b *testing.B) { 446 | r, err := NewResolver() 447 | if err != nil { 448 | b.Fatal(err) 449 | } 450 | defer r.Close() 451 | ctx := context.Background() 452 | for n := 0; n < b.N; n++ { 453 | _, err := r.LookupAddr(ctx, lookupAddr4) 454 | if err != nil { 455 | b.Error(err) 456 | continue 457 | } 458 | } 459 | } 460 | 461 | func BenchmarkHTTPClientGoResolver(b *testing.B) { 462 | httpCli := &http.Client{} 463 | for n := 0; n < b.N; n++ { 464 | resp, err := httpCli.Get(getUrl) 465 | if err != nil { 466 | b.Error(err) 467 | continue 468 | } 469 | resp.Body.Close() 470 | } 471 | } 472 | 473 | func BenchmarkHTTPClientSystemdResolver(b *testing.B) { 474 | r, err := NewResolver() 475 | if err != nil { 476 | b.Fatal(err) 477 | } 478 | defer r.Close() 479 | httpCli := r.HTTPClient() 480 | for n := 0; n < b.N; n++ { 481 | resp, err := httpCli.Get(getUrl) 482 | if err != nil { 483 | b.Error(err) 484 | continue 485 | } 486 | resp.Body.Close() 487 | } 488 | } 489 | -------------------------------------------------------------------------------- /resolved/resolver.go: -------------------------------------------------------------------------------- 1 | package resolved 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net" 7 | "net/http" 8 | "runtime" 9 | "sort" 10 | "syscall" 11 | "time" 12 | 13 | "github.com/miekg/dns" 14 | "golang.org/x/net/idna" 15 | ) 16 | 17 | // Note: This is still under development and very experimental, do not use it in production. 18 | 19 | // resolver is the interface to implements the same methods as the net.Resolver 20 | type resolver interface { 21 | LookupAddr(ctx context.Context, addr string) (names []string, err error) 22 | LookupCNAME(ctx context.Context, host string) (cname string, err error) 23 | LookupHost(ctx context.Context, host string) (addrs []string, err error) 24 | LookupIP(ctx context.Context, network, host string) ([]net.IP, error) 25 | LookupIPAddr(ctx context.Context, host string) ([]net.IPAddr, error) 26 | LookupMX(ctx context.Context, name string) ([]*net.MX, error) 27 | LookupNS(ctx context.Context, name string) ([]*net.NS, error) 28 | LookupPort(ctx context.Context, network, service string) (port int, err error) 29 | LookupSRV(ctx context.Context, service, proto, name string) (cname string, addrs []*net.SRV, err error) 30 | LookupTXT(ctx context.Context, name string) ([]string, error) 31 | } 32 | 33 | var ( 34 | // ensure that types implement resolver interface 35 | _ resolver = &Resolver{} 36 | _ resolver = &net.Resolver{} 37 | ) 38 | 39 | // Resolver represents the systemd-resolved resolver 40 | // throught dbus connection. 41 | type Resolver struct { 42 | conn *Conn 43 | dialer *net.Dialer 44 | profile *idna.Profile 45 | } 46 | 47 | type resolverOption func(r *Resolver) error 48 | 49 | // WithConn allow you to use a custom systemd-resolved dbus connection. 50 | func WithConn(c *Conn) resolverOption { 51 | return func(r *Resolver) error { 52 | if c == nil { 53 | return errors.New("conn is nil") 54 | } 55 | r.conn = c 56 | return nil 57 | } 58 | } 59 | 60 | // WithDialer allow you to use a custom net.Dialer. 61 | func WithDialer(d *net.Dialer) resolverOption { 62 | return func(r *Resolver) error { 63 | if d == nil { 64 | return errors.New("dialer is nil") 65 | } 66 | r.dialer = d 67 | return nil 68 | } 69 | } 70 | 71 | // WithProfile allow you to use custom idna.Profile. 72 | func WithProfile(p *idna.Profile) resolverOption { 73 | return func(r *Resolver) error { 74 | if p == nil { 75 | return errors.New("profile is nil") 76 | } 77 | r.profile = p 78 | return nil 79 | } 80 | } 81 | 82 | // NewResolver returns a new systemd Resolver with an initialized dbus connection. 83 | // it's up to you to close that connection when you have been done with the Resolver. 84 | func NewResolver(opts ...resolverOption) (*Resolver, error) { 85 | r := &Resolver{} 86 | var err error 87 | for _, opt := range opts { 88 | err = opt(r) 89 | if err != nil { 90 | return nil, err 91 | } 92 | } 93 | if r.conn == nil { 94 | var err error 95 | r.conn, err = NewConn() 96 | if err != nil { 97 | return nil, err 98 | } 99 | } 100 | if r.dialer == nil { 101 | r.dialer = &net.Dialer{ 102 | Timeout: 30 * time.Second, 103 | KeepAlive: 30 * time.Second, 104 | } 105 | } 106 | if r.profile == nil { 107 | r.profile = idna.New() 108 | } 109 | return r, nil 110 | } 111 | 112 | // Close closes the current dbus connection. 113 | // You need to close the connection when you've done with it. 114 | func (r *Resolver) Close() error { 115 | return r.conn.Close() 116 | } 117 | 118 | // DialContext resolves address using systemd-network and use internal dialer with the resolved ip address. 119 | // It is useful when it comes to integration with go standard library. 120 | func (r *Resolver) DialContext(ctx context.Context, network string, address string) (net.Conn, error) { 121 | host, port, err := net.SplitHostPort(address) 122 | if err != nil { 123 | return nil, err 124 | } 125 | addrs, _, _, err := r.conn.ResolveHostname(ctx, 0, host, syscall.AF_UNSPEC, 0) 126 | if err != nil { 127 | return nil, err 128 | } 129 | for _, addr := range addrs { 130 | if addr.Address.To4() == nil { 131 | // prefer ipv6 132 | address = addr.Address.String() 133 | break 134 | } 135 | address = addr.Address.String() 136 | } 137 | return r.dialer.DialContext(ctx, network, net.JoinHostPort(address, port)) 138 | } 139 | 140 | // HTTPClient returns a new http.Client with systemd-resolved as resolver 141 | // and idle connections + keepalives disabled. 142 | func (r *Resolver) HTTPClient() *http.Client { 143 | transport := r.pooledTransport() 144 | transport.DisableKeepAlives = true 145 | transport.MaxIdleConnsPerHost = -1 146 | return &http.Client{ 147 | Transport: transport, 148 | } 149 | } 150 | 151 | // HTTPPooledClient returns a new http.Client with systemd-resolved as resolver 152 | // and similar default values to http.DefaultTransport. 153 | // Do not use this for transient transports as 154 | // it can leak file descriptors over time. Only use this for transports that 155 | // will be re-used for the same host(s). 156 | func (r *Resolver) HTTPPooledClient() *http.Client { 157 | return &http.Client{ 158 | Transport: r.pooledTransport(), 159 | } 160 | } 161 | 162 | func (r *Resolver) pooledTransport() *http.Transport { 163 | transport := &http.Transport{ 164 | Proxy: http.ProxyFromEnvironment, 165 | DialContext: r.DialContext, 166 | MaxIdleConns: 100, 167 | IdleConnTimeout: 90 * time.Second, 168 | TLSHandshakeTimeout: 10 * time.Second, 169 | ExpectContinueTimeout: 1 * time.Second, 170 | ForceAttemptHTTP2: true, 171 | MaxIdleConnsPerHost: runtime.GOMAXPROCS(0) + 1, 172 | } 173 | return transport 174 | } 175 | 176 | // LookupHost looks up the given host using the systemd-resolved resolver. 177 | // It returns a slice of that host's addresses. 178 | func (r *Resolver) LookupHost(ctx context.Context, host string) (addrs []string, err error) { 179 | if host == "" { 180 | return nil, &net.DNSError{Err: "no such host", Name: host, IsNotFound: true} 181 | } 182 | addresses, _, _, err := r.conn.ResolveHostname(ctx, 0, host, syscall.AF_UNSPEC, 0) 183 | if err != nil { 184 | return nil, err 185 | } 186 | addrs = make([]string, len(addresses)) 187 | for i, addr := range addresses { 188 | addrs[i] = addr.Address.String() 189 | } 190 | return 191 | } 192 | 193 | // LookupAddr performs a reverse lookup for the given address, returning a list 194 | // of names mapping to that address. 195 | func (r *Resolver) LookupAddr(ctx context.Context, addr string) (names []string, err error) { 196 | ip := net.ParseIP(addr) 197 | if ip == nil { 198 | return nil, &net.DNSError{Err: "unrecognized address", Name: addr} 199 | } 200 | var family int 201 | if ipv4 := ip.To4(); ipv4 != nil { 202 | // use 4-byte representation 203 | ip = ipv4 204 | family = syscall.AF_INET 205 | } else { 206 | family = syscall.AF_INET6 207 | } 208 | hostnames, _, err := r.conn.ResolveAddress(ctx, 0, family, ip, 0) 209 | if err != nil { 210 | return nil, err 211 | } 212 | names = make([]string, len(hostnames)) 213 | for i, name := range hostnames { 214 | names[i] = fullyQualified(name.Hostname) 215 | } 216 | return 217 | } 218 | 219 | // LookupIP looks up host for the given network using the systemd-resolved resolver. 220 | // It returns a slice of that host's IP addresses of the type specified by network. 221 | // network must be one of "ip", "ip4" or "ip6". 222 | func (r *Resolver) LookupIP(ctx context.Context, network, host string) ([]net.IP, error) { 223 | if host == "" { 224 | return nil, &net.DNSError{Err: "no such host", Name: host, IsNotFound: true} 225 | } 226 | var family int 227 | switch network { 228 | case "ip": 229 | family = syscall.AF_UNSPEC 230 | case "ip4": 231 | family = syscall.AF_INET 232 | case "ip6": 233 | family = syscall.AF_INET6 234 | default: 235 | return nil, errors.New("bad network") 236 | } 237 | addresses, _, _, err := r.conn.ResolveHostname(ctx, 0, host, family, 0) 238 | if err != nil { 239 | return nil, err 240 | } 241 | addrs := make([]net.IP, len(addresses)) 242 | for i, addr := range addresses { 243 | addrs[i] = addr.Address 244 | } 245 | return addrs, nil 246 | } 247 | 248 | // LookupIPAddr looks up host using the systemd-resolved resolver. 249 | // It returns a slice of that host's IPv4 and IPv6 addresses. 250 | func (r *Resolver) LookupIPAddr(ctx context.Context, host string) ([]net.IPAddr, error) { 251 | if host == "" { 252 | return nil, &net.DNSError{Err: "no such host", Name: host, IsNotFound: true} 253 | } 254 | addresses, _, _, err := r.conn.ResolveHostname(ctx, 0, host, syscall.AF_UNSPEC, 0) 255 | if err != nil { 256 | return nil, err 257 | } 258 | addrs := make([]net.IPAddr, len(addresses)) 259 | for i, addr := range addresses { 260 | addrs[i] = net.IPAddr{ 261 | IP: addr.Address, 262 | } 263 | } 264 | return addrs, nil 265 | } 266 | 267 | // LookupCNAME returns the canonical name for the given host. 268 | func (r *Resolver) LookupCNAME(ctx context.Context, host string) (string, error) { 269 | var ok bool 270 | if host, ok = r.IsDomainName(host); !ok { 271 | return "", &net.DNSError{Err: "no such host", Name: host, IsNotFound: true} 272 | } 273 | records, _, err := r.conn.ResolveRecord(ctx, 0, host, dns.ClassINET, dns.Type(dns.TypeCNAME), 0) 274 | if err != nil { 275 | return "", err 276 | } 277 | if len(records) == 0 { 278 | return "", &net.DNSError{Err: "no such host", Name: host, IsNotFound: true} 279 | } 280 | cname, err := records[0].CNAME() 281 | if err != nil { 282 | return "", err 283 | } 284 | return cname.Target, nil 285 | } 286 | 287 | // LookupMX returns the DNS MX records for the given domain name sorted by preference. 288 | func (r *Resolver) LookupMX(ctx context.Context, name string) ([]*net.MX, error) { 289 | var ok bool 290 | if name, ok = r.IsDomainName(name); !ok { 291 | return nil, &net.DNSError{Err: "no such host", Name: name, IsNotFound: true} 292 | } 293 | records, _, err := r.conn.ResolveRecord(ctx, 0, name, dns.ClassINET, dns.Type(dns.TypeMX), 0) 294 | if err != nil { 295 | return nil, err 296 | } 297 | mxs := make([]*net.MX, len(records)) 298 | for i, record := range records { 299 | mx, err := record.MX() 300 | if err != nil { 301 | return nil, err 302 | } 303 | mxs[i] = &net.MX{ 304 | Host: mx.Mx, 305 | Pref: mx.Preference, 306 | } 307 | } 308 | sort.Slice(mxs, func(i, j int) bool { 309 | return mxs[i].Pref < mxs[j].Pref 310 | }) 311 | return mxs, nil 312 | } 313 | 314 | // LookupNS returns the DNS NS records for the given domain name. 315 | func (r *Resolver) LookupNS(ctx context.Context, name string) ([]*net.NS, error) { 316 | var ok bool 317 | if name, ok = r.IsDomainName(name); !ok { 318 | return nil, &net.DNSError{Err: "no such host", Name: name, IsNotFound: true} 319 | } 320 | records, _, err := r.conn.ResolveRecord(ctx, 0, name, dns.ClassINET, dns.Type(dns.TypeNS), 0) 321 | if err != nil { 322 | return nil, err 323 | } 324 | nss := make([]*net.NS, len(records)) 325 | for i, record := range records { 326 | ns, err := record.NS() 327 | if err != nil { 328 | return nil, err 329 | } 330 | nss[i] = &net.NS{ 331 | Host: ns.Ns, 332 | } 333 | } 334 | return nss, nil 335 | } 336 | 337 | // LookupPort looks up the port for the given network and service. 338 | func (r *Resolver) LookupPort(ctx context.Context, network, service string) (port int, err error) { 339 | // this is not supported because i don't want to implement again what's inside the go standard library 340 | // like the port map filled with /etc/service etc... 341 | err = errors.New("not supported yet") 342 | return 343 | } 344 | 345 | // LookupSRV tries to resolve an SRV query of the given service, protocol, and domain name. 346 | // The proto is "tcp" or "udp". The returned records are sorted by priority. 347 | func (r *Resolver) LookupSRV(ctx context.Context, service, proto, name string) (cname string, addrs []*net.SRV, err error) { 348 | var target string 349 | if service == "" && proto == "" { 350 | target = name 351 | } else { 352 | target = "_" + service + "._" + proto + "." + name 353 | } 354 | srvData, _, _, canonicalType, canonicalDomain, _, err := r.conn.ResolveService(ctx, 0, "", "", target, syscall.AF_UNSPEC, 0) 355 | if err != nil { 356 | return 357 | } 358 | addrs = make([]*net.SRV, len(srvData)) 359 | for i, srv := range srvData { 360 | addrs[i] = &net.SRV{ 361 | Target: fullyQualified(srv.Hostname), 362 | Port: srv.Port, 363 | Priority: srv.Priority, 364 | Weight: srv.Weight, 365 | } 366 | } 367 | sort.Slice(addrs, func(i, j int) bool { 368 | return addrs[i].Priority < addrs[j].Priority 369 | }) 370 | if canonicalType != "" { 371 | cname = fullyQualified(canonicalType + "." + canonicalDomain) 372 | } else { 373 | cname = fullyQualified(canonicalDomain) 374 | } 375 | return 376 | } 377 | 378 | // LookupTXT returns the DNS TXT records for the given domain name. 379 | func (r *Resolver) LookupTXT(ctx context.Context, name string) ([]string, error) { 380 | var ok bool 381 | if name, ok = r.IsDomainName(name); !ok { 382 | return nil, &net.DNSError{Err: "no such host", Name: name, IsNotFound: true} 383 | } 384 | records, _, err := r.conn.ResolveRecord(ctx, 0, name, dns.ClassINET, dns.Type(dns.TypeTXT), 0) 385 | if err != nil { 386 | return nil, err 387 | } 388 | txts := make([]string, 0, len(records)) 389 | for _, record := range records { 390 | txt, err := record.TXT() 391 | if err != nil { 392 | return nil, err 393 | } 394 | txts = append(txts, txt.Txt...) 395 | } 396 | return txts, nil 397 | } 398 | 399 | // IsDomainName tries to convert name to ASCII (IANA conversion) if name is not a strict domain name (see RFC 1035) 400 | // It returns false if name is not a domain before and after ASCII conversion. 401 | // It uses isDomainName from go standard library. 402 | func (r *Resolver) IsDomainName(name string) (string, bool) { 403 | if !isDomainName(name) { 404 | var err error 405 | name, err = r.profile.ToASCII(name) 406 | if err != nil { 407 | return name, false 408 | } 409 | if !isDomainName(name) { 410 | return name, false 411 | } 412 | } 413 | return name, true 414 | } 415 | 416 | func fullyQualified(s string) string { 417 | b := []byte(s) 418 | hasDots := false 419 | for _, x := range b { 420 | if x == '.' { 421 | hasDots = true 422 | break 423 | } 424 | } 425 | if hasDots && b[len(b)-1] != '.' { 426 | b = append(b, '.') 427 | } 428 | return string(b) 429 | } 430 | 431 | // this function comes from go standard library 432 | // there is issues about it since it denied some valid domains. 433 | // see: https://github.com/golang/go/issues/17659 434 | func isDomainName(s string) bool { 435 | l := len(s) 436 | if l == 0 || l > 254 || l == 254 && s[l-1] != '.' { 437 | return false 438 | } 439 | last := byte('.') 440 | nonNumeric := false // true once we've seen a letter or hyphen 441 | partlen := 0 442 | for i := 0; i < len(s); i++ { 443 | c := s[i] 444 | switch { 445 | default: 446 | return false 447 | case 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || c == '_': 448 | nonNumeric = true 449 | partlen++ 450 | case '0' <= c && c <= '9': 451 | // fine 452 | partlen++ 453 | case c == '-': 454 | // Byte before dash cannot be dot. 455 | if last == '.' { 456 | return false 457 | } 458 | partlen++ 459 | nonNumeric = true 460 | case c == '.': 461 | // Byte before dot cannot be dot, dash. 462 | if last == '.' || last == '-' { 463 | return false 464 | } 465 | if partlen > 63 || partlen == 0 { 466 | return false 467 | } 468 | partlen = 0 469 | } 470 | last = c 471 | } 472 | if last == '-' || partlen > 63 { 473 | return false 474 | } 475 | return nonNumeric 476 | } 477 | -------------------------------------------------------------------------------- /resolved/dbus.go: -------------------------------------------------------------------------------- 1 | package resolved 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "net" 8 | "os" 9 | "strconv" 10 | 11 | "github.com/godbus/dbus/v5" 12 | "github.com/miekg/dns" 13 | ) 14 | 15 | const ( 16 | dbusDest = "org.freedesktop.resolve1" 17 | dbusInterface = "org.freedesktop.resolve1.Manager" 18 | dbusPath = "/org/freedesktop/resolve1" 19 | ) 20 | 21 | // Conn represents a systemd-resolved dbus connection. 22 | type Conn struct { 23 | conn *dbus.Conn 24 | obj dbus.BusObject 25 | } 26 | 27 | // NewConn returns a new and ready to use dbus connection. 28 | // You must close that connection when you have been done with it. 29 | func NewConn() (*Conn, error) { 30 | conn, err := dbus.SystemBusPrivate() 31 | if err != nil { 32 | return nil, fmt.Errorf("failed to init private conn to system bus: %v", err) 33 | } 34 | methods := []dbus.Auth{dbus.AuthExternal(strconv.Itoa(os.Getuid()))} 35 | err = conn.Auth(methods) 36 | if err != nil { 37 | conn.Close() 38 | return nil, fmt.Errorf("failed to auth with external method: %v", err) 39 | } 40 | err = conn.Hello() 41 | if err != nil { 42 | conn.Close() 43 | return nil, fmt.Errorf("failed to make hello call: %v", err) 44 | } 45 | return &Conn{ 46 | conn: conn, 47 | obj: conn.Object(dbusDest, dbus.ObjectPath(dbusPath)), 48 | }, nil 49 | } 50 | 51 | // Call wraps obj.CallWithContext by using 0 as flags and format the method with the dbus manager interface. 52 | func (c *Conn) Call(ctx context.Context, method string, args ...interface{}) *dbus.Call { 53 | return c.obj.CallWithContext(ctx, fmt.Sprintf("%s.%s", dbusInterface, method), 0, args...) 54 | } 55 | 56 | // Close closes the current dbus connection. 57 | func (c *Conn) Close() error { 58 | return c.conn.Close() 59 | } 60 | 61 | // ResolveHostname, ResolveAddress, ResolveRecord, ResolveService 62 | // The four methods above accept and return a 64-bit flags value. 63 | // In most cases passing 0 is sufficient and recommended. 64 | // However, the following flags are defined to alter the look-up 65 | const ( 66 | SD_RESOLVED_DNS = uint64(1) << 0 67 | SD_RESOLVED_LLMNR_IPV4 = uint64(1) << 1 68 | SD_RESOLVED_LLMNR_IPV6 = uint64(1) << 2 69 | SD_RESOLVED_MDNS_IPV4 = uint64(1) << 3 70 | SD_RESOLVED_MDNS_IPV6 = uint64(1) << 4 71 | SD_RESOLVED_NO_CNAME = uint64(1) << 5 72 | SD_RESOLVED_NO_TXT = uint64(1) << 6 73 | SD_RESOLVED_NO_ADDRESS = uint64(1) << 7 74 | SD_RESOLVED_NO_SEARCH = uint64(1) << 8 75 | SD_RESOLVED_AUTHENTICATED = uint64(1) << 9 76 | ) 77 | 78 | // Address represents an address returned by ResolveHostname. 79 | type Address struct { 80 | IfIndex int // network interface index 81 | Family int // can be either syscall.AF_INET or syscall.AF_INET6 82 | Address net.IP // binary address 83 | } 84 | 85 | func (a Address) String() string { 86 | return fmt.Sprintf(`{ 87 | IfIndex: %d, 88 | Family: %d, 89 | IP: %s, 90 | }`, a.IfIndex, a.Family, a.Address.String()) 91 | } 92 | 93 | // ResolveHostname takes a hostname and resolves it to one or more IP addresses. 94 | // ctx: Context to use 95 | // ifindex: Network interface index where to look (0 means any) 96 | // name: Hostname 97 | // family: Which address family to look for (syscall.AF_UNSPEC, syscall.AF_INET, syscall.AF_INET6) 98 | // flags: Input flags parameter 99 | func (c *Conn) ResolveHostname(ctx context.Context, ifindex int, name string, family int, flags uint64) (addresses []Address, canonical string, outflags uint64, err error) { 100 | err = c.Call(ctx, "ResolveHostname", ifindex, name, family, flags).Store(&addresses, &canonical, &outflags) 101 | return 102 | } 103 | 104 | // Name represents a hostname returned by ResolveAddress. 105 | type Name struct { 106 | IfIndex int // network interface index 107 | Hostname string // hostname 108 | } 109 | 110 | func (n Name) String() string { 111 | return fmt.Sprintf(`{ 112 | IfIndex: %d, 113 | Name: %s, 114 | }`, n.IfIndex, n.Hostname) 115 | } 116 | 117 | // ResolveAddress takes a DNS resource record (RR) type, class and name 118 | // and retrieves the full resource record set (RRset), including the RDATA, for it. 119 | // ctx: Context to use 120 | // ifindex: Network interface index where to look (0 means any) 121 | // family: Address family (syscall.AF_INET, syscall.AF_INET6) 122 | // address: the binary address (4 or 16 bytes) 123 | // flags: Input flags parameter 124 | func (c *Conn) ResolveAddress(ctx context.Context, ifindex int, family int, address net.IP, flags uint64) (names []Name, outflags uint64, err error) { 125 | err = c.Call(ctx, "ResolveAddress", ifindex, family, address, flags).Store(&names, &outflags) 126 | return 127 | } 128 | 129 | // ResourceRecord represents a DNS RR as it returned by 130 | // by ResolveRecord. 131 | type ResourceRecord struct { 132 | IfIndex int // network interface index 133 | Type dns.Type // dns type 134 | Class dns.Class // dns class 135 | // The raw RR data starts with the RR's domain name, in the original casing, followed by the RR type, class, 136 | // TTL and RDATA, in the binary format documented in RFC 1035. For RRs that support name compression in the payload 137 | // (such as MX or PTR), the compression is expanded in the returned data. 138 | Data []byte 139 | } 140 | 141 | func (r ResourceRecord) String() string { 142 | return fmt.Sprintf(`{ 143 | IfIndex: %d, 144 | Type: %s, 145 | Class: %s, 146 | Data: %v, 147 | }`, r.IfIndex, r.Type.String(), r.Class.String(), r.Data) 148 | } 149 | 150 | // Unpack unpacks a ResourceRecord to dns.RR interface. 151 | func (r ResourceRecord) Unpack() (dns.RR, error) { 152 | rr, _, err := dns.UnpackRR(r.Data, 0) 153 | if err != nil { 154 | return nil, err 155 | } 156 | return rr, nil 157 | } 158 | 159 | // CNAME unpacks a ResourceRecord to *dns.CNAME. 160 | func (r ResourceRecord) CNAME() (*dns.CNAME, error) { 161 | rr, err := r.Unpack() 162 | if err != nil { 163 | return nil, err 164 | } 165 | if rr.Header().Rrtype != dns.TypeCNAME { 166 | return nil, errors.New("not an CNAME record type") 167 | } 168 | cname, ok := rr.(*dns.CNAME) 169 | if !ok { 170 | return nil, errors.New("dns.RR is not a *dns.CNAME") 171 | } 172 | return cname, nil 173 | } 174 | 175 | // MX unpacks a ResourceRecord to *dns.MX. 176 | func (r ResourceRecord) MX() (*dns.MX, error) { 177 | rr, err := r.Unpack() 178 | if err != nil { 179 | return nil, err 180 | } 181 | if rr.Header().Rrtype != dns.TypeMX { 182 | return nil, errors.New("not an MX record type") 183 | } 184 | mx, ok := rr.(*dns.MX) 185 | if !ok { 186 | return nil, errors.New("dns.RR is not a *dns.MX") 187 | } 188 | return mx, nil 189 | } 190 | 191 | // NS unpacks a ResourceRecord to *dns.NS. 192 | func (r ResourceRecord) NS() (*dns.NS, error) { 193 | rr, err := r.Unpack() 194 | if err != nil { 195 | return nil, err 196 | } 197 | if rr.Header().Rrtype != dns.TypeNS { 198 | return nil, errors.New("not an NS record type") 199 | } 200 | ns, ok := rr.(*dns.NS) 201 | if !ok { 202 | return nil, errors.New("dns.RR is not a *dns.NS") 203 | } 204 | return ns, nil 205 | } 206 | 207 | // SRV unpacks a ResourceRecord to *dns.SRV. 208 | func (r ResourceRecord) SRV() (*dns.SRV, error) { 209 | rr, err := r.Unpack() 210 | if err != nil { 211 | return nil, err 212 | } 213 | if rr.Header().Rrtype != dns.TypeSRV { 214 | return nil, errors.New("not an SRV record type") 215 | } 216 | srv, ok := rr.(*dns.SRV) 217 | if !ok { 218 | return nil, errors.New("dns.RR is not a *dns.SRV") 219 | } 220 | return srv, nil 221 | } 222 | 223 | // TXT unpacks a ResourceRecord to *dns.TXT. 224 | func (r ResourceRecord) TXT() (*dns.TXT, error) { 225 | rr, err := r.Unpack() 226 | if err != nil { 227 | return nil, err 228 | } 229 | if rr.Header().Rrtype != dns.TypeTXT { 230 | return nil, errors.New("not an TXT record type") 231 | } 232 | txt, ok := rr.(*dns.TXT) 233 | if !ok { 234 | return nil, errors.New("dns.RR is not a *dns.TXT") 235 | } 236 | return txt, nil 237 | } 238 | 239 | // ResolveRecord takes a DNS resource record (RR) type, class and name, and retrieves the full resource record set (RRset), including the RDATA, for it. 240 | // ctx: Context to use 241 | // ifindex: Network interface index where to look (0 means any) 242 | // name: Specifies the RR domain name to look up 243 | // class: 16-bit dns class 244 | // rtype: 16-bit dns type 245 | // flags: Input flags parameter 246 | 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) { 247 | err = c.Call(ctx, "ResolveRecord", ifindex, name, class, rtype, flags).Store(&records, &outflags) 248 | return 249 | } 250 | 251 | // SRVRecord represents an service record as it returned 252 | // by ResolveService. 253 | type SRVRecord struct { 254 | Priority uint16 255 | Weight uint16 256 | Port uint16 257 | Hostname string 258 | Addresses []Address 259 | CNAME string 260 | } 261 | 262 | func (r SRVRecord) String() string { 263 | return fmt.Sprintf(`{ 264 | Priority: %d, 265 | Weight: %d, 266 | Port: %d, 267 | Hostname: %s, 268 | Addresses: %v, 269 | }`, r.Priority, r.Weight, r.Port, r.Hostname, r.Addresses) 270 | } 271 | 272 | // TXTRecord represents a raw TXT RR string 273 | type TXTRecord []byte 274 | 275 | func (r TXTRecord) String() string { 276 | return string(r) 277 | } 278 | 279 | // ResolveService resolves a DNS SRV service record, as well as the hostnames referenced in it 280 | // and possibly an accompanying DNS-SD TXT record containing additional service metadata. 281 | // ctx: Context to use 282 | // ifindex: Network interface index where to look (0 means any) 283 | // name: the service name 284 | // stype: the service type (eg: _webdav._tcp) 285 | // domain: the service domain 286 | // family: Address family (syscall.AF_UNSPEC, syscall.AF_INET, syscall.AF_INET6) 287 | // flags: Input flags parameter 288 | func (c *Conn) ResolveService(ctx context.Context, ifindex int, name string, stype string, domain string, family int, 289 | flags uint64) (srvData []SRVRecord, txtData []TXTRecord, canonicalName string, canonicalType string, canonicalDomain string, outflags uint64, err error) { 290 | err = c.Call(ctx, "ResolveService", ifindex, name, stype, domain, family, flags).Store(&srvData, &txtData, &canonicalName, &canonicalType, &canonicalDomain, &outflags) 291 | return 292 | } 293 | 294 | // GetLink takes a network interface index and returns the object path 295 | // to the org.freedesktop.resolve1.Link object corresponding to it. 296 | // ctx: Context to use 297 | // ifindex: The network interface index to get link for 298 | func (c *Conn) GetLink(ctx context.Context, ifindex int) (path string, err error) { 299 | err = c.Call(ctx, "GetLink", ifindex).Store(&path) 300 | return 301 | } 302 | 303 | // LinkDNS represents a DNS server address to use in SetLinkDNS method. 304 | type LinkDNS struct { 305 | Family int // can be either syscall.AF_INET or syscall.AF_INET6 306 | Address net.IP // binary address 307 | } 308 | 309 | // SetLinkDNS sets the DNS servers to use on a specific interface. 310 | // ctx: Context to use 311 | // ifindex: The network interface index to set 312 | // addrs: array of DNS server IP address records. 313 | func (c *Conn) SetLinkDNS(ctx context.Context, ifindex int, addrs []LinkDNS) (err error) { 314 | err = c.Call(ctx, "SetLinkDNS", ifindex, addrs).Store() 315 | return 316 | } 317 | 318 | type LinkDNSEx struct { 319 | Family int // can be either syscall.AF_INET or syscall.AF_INET6 320 | Address net.IP // binary address 321 | Port uint16 // the port number 322 | Name string // the DNS Name 323 | } 324 | 325 | // SetLinkDNSEx is similar to SetLinkDNS(), but allows an IP port 326 | // (instead of the default 53) and DNS name to be specified for each DNS server. 327 | // The server name is used for Server Name Indication (SNI), which is useful when DNS-over-TLS is used. 328 | // ctx: Context to use 329 | // ifindex: The network interface index 330 | // addrs: array of DNS server IP address records. 331 | func (c *Conn) SetLinkDNSEx(ctx context.Context, ifindex int, addrs []LinkDNSEx) error { 332 | return c.Call(ctx, "SetLinkDNSEx", ifindex, addrs).Store() 333 | } 334 | 335 | type LinkDomain struct { 336 | Domain string // the domain name 337 | RoutingDomain bool // whether the specified domain shall be used as a search domain (false), or just as a routing domain (true). 338 | } 339 | 340 | // SetLinkDomains sets the search and routing domains to use on a specific network interface for DNS look-ups. 341 | // ctx: Context to use 342 | // ifindex: The network interface index 343 | // domains: array of domains 344 | func (c *Conn) SetLinkDomains(ctx context.Context, ifindex int, domains []LinkDomain) error { 345 | return c.Call(ctx, "SetLinkDomains", ifindex, domains).Store() 346 | } 347 | 348 | // SetLinkDefaultRoute specifies whether the link shall be used as the default route for name queries 349 | // ctx: Context to use 350 | // ifindex: The network interface index 351 | // enable: enable/disable link as default route. 352 | func (c *Conn) SetLinkDefaultRoute(ctx context.Context, ifindex int, enable bool) error { 353 | return c.Call(ctx, "SetLinkDefaultRoute", ifindex, enable).Store() 354 | } 355 | 356 | // SetLinkLLMNR enables or disables LLMNR support on a specific network interface. 357 | // ctx: Context to use 358 | // ifindex: The network interface index 359 | // mode: either empty or one of "yes", "no" or "resolve". 360 | func (c *Conn) SetLinkLLMNR(ctx context.Context, ifindex int, mode string) error { 361 | return c.Call(ctx, "SetLinkLLMNR", ifindex, mode).Store() 362 | } 363 | 364 | // SetLinkMulticastDNS enables or disables MulticastDNS support on a specific interface. 365 | // ctx: Context to use 366 | // ifindex: The network interface index 367 | // mode: either empty or one of "yes", "no" or "resolve". 368 | func (c *Conn) SetLinkMulticastDNS(ctx context.Context, ifindex int, mode string) error { 369 | return c.Call(ctx, "SetLinkMulticastDNS", ifindex, mode).Store() 370 | } 371 | 372 | // SetLinkDNSOverTLS enables or disables enables or disables DNS-over-TLS on a specific network interface. 373 | // ctx: Context to use 374 | // ifindex: The network interface index 375 | // mode: either empty or one of "yes", "no", or "opportunistic" 376 | func (c *Conn) SetLinkDNSOverTLS(ctx context.Context, ifindex int, mode string) error { 377 | return c.Call(ctx, "SetLinkDNSOverTLS", ifindex, mode).Store() 378 | } 379 | 380 | // SetLinkDNSSEC enables or disables DNSSEC validation on a specific network interface. 381 | // ctx: Context to use 382 | // ifindex: The network interface index 383 | // mode: either empty or one of "yes", "no", or "allow-downgrade" 384 | func (c *Conn) SetLinkDNSSEC(ctx context.Context, ifindex int, mode string) error { 385 | return c.Call(ctx, "SetLinkDNSSEC", ifindex, mode).Store() 386 | } 387 | 388 | // SetLinkDNSSECNegativeTrustAnchors configures DNSSEC Negative Trust Anchors (NTAs) for a specific network interface. 389 | // ctx: Context to use 390 | // ifindex: The network interface index 391 | // names: array of domains 392 | func (c *Conn) SetLinkDNSSECNegativeTrustAnchors(ctx context.Context, ifindex int, names []string) error { 393 | return c.Call(ctx, "SetLinkDNSSECNegativeTrustAnchors", ifindex, names).Store() 394 | } 395 | 396 | // RevertLink reverts all per-link settings to the defaults on a specific network interface. 397 | // ctx: Context to use 398 | // ifindex: The network interface index. 399 | func (c *Conn) RevertLink(ctx context.Context, ifindex int) error { 400 | return c.Call(ctx, "RevertLink", ifindex).Store() 401 | } 402 | 403 | // RegisterService 404 | func (c *Conn) RegisterService(ctx context.Context, name string, nameTemplate string, stype string, 405 | svcPort uint16, svcPriority uint16, svcWeight uint16, txtData []TXTRecord) (svcPath string, err error) { 406 | err = c.Call(ctx, "RegisterService", name, nameTemplate, stype, svcPort, svcPriority, svcWeight, txtData).Store(&svcPath) 407 | return 408 | } 409 | 410 | // UnregisterService 411 | func (c *Conn) UnregisterService(ctx context.Context, svcPath string) error { 412 | return c.Call(ctx, "UnregisterService", svcPath).Store() 413 | } 414 | 415 | // ResetStatistics resets the various statistics counters that systemd-resolved maintains to zero. 416 | func (c *Conn) ResetStatistics(ctx context.Context) error { 417 | return c.Call(ctx, "ResetStatistics").Store() 418 | } 419 | 420 | // FlushCaches 421 | func (c *Conn) FlushCaches(ctx context.Context) error { 422 | return c.Call(ctx, "FlushCaches").Store() 423 | } 424 | 425 | // ResetServerFeatures 426 | func (c *Conn) ResetServerFeatures(ctx context.Context) error { 427 | return c.Call(ctx, "ResetServerFeatures").Store() 428 | } 429 | 430 | type Link struct { 431 | obj dbus.BusObject 432 | } 433 | 434 | func NewLink(c *Conn, path string) Link { 435 | return Link{ 436 | obj: c.conn.Object(dbusDest, dbus.ObjectPath(path)), 437 | } 438 | } 439 | 440 | // TODO 441 | // SetDNS(in a(iay) addresses); 442 | // SetDNSEx(in a(iayqs) addresses); 443 | // SetDomains(in a(sb) domains); 444 | // SetDefaultRoute(in b enable); 445 | // SetLLMNR(in s mode); 446 | // SetMulticastDNS(in s mode); 447 | // SetDNSOverTLS(in s mode); 448 | // SetDNSSEC(in s mode); 449 | // SetDNSSECNegativeTrustAnchors(in as names); 450 | // Revert(); 451 | --------------------------------------------------------------------------------