mirror of
https://github.com/evilsocket/opensnitch.git
synced 2025-03-04 08:34:40 +01:00
237 lines
6.4 KiB
Go
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
|
|
}
|