opensnitch/daemon/dns/systemd/monitor.go
Gustavo Iñiguez Goia 5184c45086
changed dns log messages
Moved some messages to Trace.
2025-01-22 19:19:57 +01:00

237 lines
6.4 KiB
Go

// Package systemd defines several utilities to interact with systemd.
//
// ResolvedMonitor:
// * To debug systemd-resolved queries and inspect the protocol:
// - resolvectl monitor
// * Resources:
// - https://github.com/systemd/systemd/blob/main/src/resolve/resolvectl.c
// - The protocol used to send and receive data is varlink:
// https://github.com/varlink/go
// https://github.com/systemd/systemd/blob/main/src/resolve/resolved-varlink.c
// - https://systemd.io/RESOLVED-VPNS/
package systemd
import (
"context"
"errors"
"fmt"
"sync"
"time"
"github.com/evilsocket/opensnitch/daemon/core"
"github.com/evilsocket/opensnitch/daemon/log"
"github.com/varlink/go/varlink"
)
// whenever there's a new DNS response, this callback will be invoked.
// the second parameter is a MonitorResponse struct that will be filled with
// data.
type resolvedCallback func(context.Context, interface{}) (uint64, error)
const (
// SuccessState is the string returned by systemd-resolved when a DNS query is successful.
// Other states: https://github.com/systemd/systemd/blob/main/src/resolve/resolved-dns-transaction.c#L3608
SuccessState = "success"
socketPath = "/run/systemd/resolve/io.systemd.Resolve.Monitor"
resolvedSubscribeMethod = "io.systemd.Resolve.Monitor.SubscribeQueryResults"
// DNSTypeA A
DNSTypeA = 1
// DNSTypeAAAA AAAA
DNSTypeAAAA = 28
// DNSTypeCNAME cname
DNSTypeCNAME = 5
// DNSTypeSOA soa
DNSTypeSOA = 6
)
// QuestionMonitorResponse represents a DNS query
// "question": [{"class": 1, "type": 28,"name": "images.site.com"}],
type QuestionMonitorResponse struct {
Name string `json:"name"`
Class int `json:"class"`
Type int `json:"type"`
}
// KeyType holds question that generated the answer
/*answer: [{
"rr": {
"key": {
"class": 1,
"type": 28,
"name": "images.site.com"
},
"address": [100, 13, 45, 111]
},
"raw": "DFJFKE343443EFKEREKET=",
"ifindex": 3
}]*/
type KeyType struct {
Name string `json:"name"`
Class int `json:"class"`
Type int `json:"type"`
}
// RRType represents a DNS answer
// if the response is a CNAME, Address will be nil, and Name a domain name.
type RRType struct {
Name string `json:"name"`
Address []byte `json:"address"`
Key QuestionMonitorResponse `json:"key"`
}
// AnswerMonitorResponse represents the DNS answer of a DNS query.
type AnswerMonitorResponse struct {
Raw string `json:"raw"`
RR RRType `json:"rr"`
Ifindex int `json:"ifindex"`
}
// MonitorResponse represents the systemd-resolved protocol message
// sent over the wire, that holds the answer to a DNS query.
type MonitorResponse struct {
State string `json:"state"`
Question []QuestionMonitorResponse `json:"question"`
// CollectedQuestions
// "collectedQuestions":[{"class":1,"type":1,"name":"translate.google.com"}]
Answer []AnswerMonitorResponse `json:"answer"`
Continues bool `json:"continues"`
}
// ResolvedMonitor represents a systemd-resolved monitor
type ResolvedMonitor struct {
Ctx context.Context
Cancel context.CancelFunc
// connection with the systemd-resolved unix socket:
// /run/systemd/resolve/io.systemd.Resolve.Monitor
Conn *varlink.Connection
// channel where all the DNS respones will be sent
ChanResponse chan *MonitorResponse
// error channel to signal any problem
ChanConnError chan error
// callback that is emited when systemd-resolved resolves a domain name.
receiverCb resolvedCallback
mu *sync.RWMutex
connected bool
}
// NewResolvedMonitor returns a new ResolvedMonitor object.
// With this object you can passively read DNS answers.
func NewResolvedMonitor() (*ResolvedMonitor, error) {
if core.Exists(socketPath) == false {
return nil, fmt.Errorf("%s doesn't exist", socketPath)
}
ctx, cancel := context.WithCancel(context.Background())
return &ResolvedMonitor{
mu: &sync.RWMutex{},
Ctx: ctx,
Cancel: cancel,
ChanResponse: make(chan *MonitorResponse),
ChanConnError: make(chan error),
}, nil
}
// Connect opens a unix socket with systemd-resolved
func (r *ResolvedMonitor) Connect() (*varlink.Connection, error) {
r.mu.Lock()
defer r.mu.Unlock()
var err error
r.Conn, err = varlink.NewConnection(r.Ctx, fmt.Sprintf("unix://%s", socketPath))
if err != nil {
return nil, err
}
r.connected = true
go r.connPoller()
return r.Conn, nil
}
// if we're connected to the unix socket, check every few seconds if we're still
// connected, and if not, reconnect, to survive to systemd-resolved restarts.
func (r *ResolvedMonitor) connPoller() {
for {
select {
case <-time.After(5 * time.Second):
if r.isConnected() {
continue
}
log.Debug("ResolvedMonitor not connected")
if _, err := r.Connect(); err == nil {
r.Subscribe()
}
goto Exit
}
}
Exit:
log.Debug("ResolvedMonitor connection poller exit.")
}
// Subscribe sends the instruction to systemd-resolved to start monitoring
// DNS answers.
func (r *ResolvedMonitor) Subscribe() error {
if r.isConnected() == false {
return errors.New("Not connected")
}
var err error
type emptyT struct{}
empty := &emptyT{}
r.receiverCb, err = r.Conn.Send(r.Ctx, resolvedSubscribeMethod, empty, varlink.Continues|varlink.More)
if err != nil {
return err
}
go r.monitor(r.Ctx, r.ChanResponse, r.ChanConnError, r.receiverCb)
return nil
}
// monitor will listen for DNS answers from systemd-resolved.
func (r *ResolvedMonitor) monitor(ctx context.Context, chanResponse chan *MonitorResponse, chanConnError chan error, callback resolvedCallback) {
for {
m := &MonitorResponse{}
continues, err := callback(ctx, m)
if err != nil {
chanConnError <- err
goto Exit
}
if continues != varlink.Continues {
goto Exit
}
log.Trace("ResolvedMonitor >> new response: %#v", m)
chanResponse <- m
}
Exit:
r.mu.Lock()
r.connected = false
r.mu.Unlock()
log.Debug("ResolvedMonitor.monitor() exit.")
}
// GetDNSResponses returns a channel that you can use to read responses.
func (r *ResolvedMonitor) GetDNSResponses() chan *MonitorResponse {
return r.ChanResponse
}
// Exit returns a channel to listen for connection errors.
func (r *ResolvedMonitor) Exit() chan error {
return r.ChanConnError
}
// Close closes the unix socket with systemd-resolved
func (r *ResolvedMonitor) Close() {
r.ChanConnError <- nil
r.Cancel()
}
func (r *ResolvedMonitor) isConnected() bool {
r.mu.RLock()
defer r.mu.RUnlock()
return r.connected
}