diff --git a/README.md b/README.md index b1b5b01..b4f6e5a 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,33 @@ Domain-specific nameservers configuration, formatting keep compatible with Dnsma More cases please refererence [dnsmasq-china-list](https://github.com/felixonmars/dnsmasq-china-list) +#### audit + +Only redis storage backend is currently implemented. + +Audit logs are in format: + +``` +{ "remoteaddr": "127.0.0.1", "domain": "domain.com", "qtype": "A", "timestamp": "2019-04-15T12:16:21.875492605Z" } +``` + +Backend uses lists to store logs in redis. + +Logs grouped by hour. + +Redis keys have format: + +``` +audit-YYYY-MM-DDTHH:00 +``` + +Example request to get audit logs from redis: + +``` +LRANGE audit-2019-04-15T00:00 0 -1 +``` + + #### cache Only the local memory storage backend is currently implemented. The redis backend is in the todo list diff --git a/audit.go b/audit.go new file mode 100644 index 0000000..0a8caa9 --- /dev/null +++ b/audit.go @@ -0,0 +1,155 @@ +package main + +import ( + "database/sql" + "encoding/json" + "fmt" + "time" + + "github.com/hoisie/redis" + _ "github.com/lib/pq" +) + +const AUDIT_LOG_OUTPUT_BUFFER = 1024 + +type AuditLogger interface { + Run() + Write(mesg *AuditMesg) +} + +type AuditMesg struct { + RemoteAddr string `json:"remoteaddr"` + Domain string `json:"domain"` + QType string `json:"qtype"` + Timestamp time.Time `json:"timestamp"` +} + +func NewAuditMessage(remoteAddr string, domain string, qtype string) *AuditMesg { + return &AuditMesg{ + RemoteAddr: remoteAddr, + Domain: domain, + QType: qtype, + Timestamp: time.Now(), + } +} + +type RedisAuditLogger struct { + backend *redis.Client + mesgs chan *AuditMesg + expire int64 +} + +func NewRedisAuditLogger(rs RedisSettings, expire int64) AuditLogger { + rc := &redis.Client{Addr: rs.Addr(), Db: rs.DB, Password: rs.Password} + auditLogger := &RedisAuditLogger{ + backend: rc, + mesgs: make(chan *AuditMesg, AUDIT_LOG_OUTPUT_BUFFER), + expire: expire, + } + go auditLogger.Run() + return auditLogger +} + +func (rl *RedisAuditLogger) Run() { + for { + select { + case mesg := <-rl.mesgs: + jsonMesg, err := json.Marshal(mesg) + if err != nil { + logger.Error("Can't write to redis audit log: %v", err) + continue + } + redisKey := fmt.Sprintf("audit-%s:00", mesg.Timestamp.Format("2006-01-02T15")) + err = rl.backend.Rpush(redisKey, jsonMesg) + if err != nil { + logger.Error("Can't write to redis audit log: %v", err) + continue + } + _, err = rl.backend.Expire(redisKey, rl.expire) + if err != nil { + logger.Error("Can't set expiration for redis audit log: %v", err) + continue + } + } + } +} + +func (rl *RedisAuditLogger) Write(mesg *AuditMesg) { + rl.mesgs <- mesg +} + +type PostgresqlAuditLogger struct { + backend *sql.DB + mesgs chan *AuditMesg + expire int64 +} + +func NewPostgresqlAuditLogger(ps PostgresqlSettings, expire int64) AuditLogger { + connStr := fmt.Sprintf(` + host=%s port=%d + user=%s password=%s + dbname=%s sslmode=%s + sslcert=%s sslkey=%s + sslrootcert=%s + `, + ps.Host, ps.Port, + ps.User, ps.Password, + ps.DB, ps.Sslmode, + ps.Sslcert, ps.Sslkey, + ps.Sslrootcert, + ) + pc, err := sql.Open("postgres", connStr) + if err != nil { + logger.Error("Can't connect to audit log postgresql: %v", err) + } + rows, err := pc.Query(` + CREATE TABLE IF NOT EXISTS audit ( + id BIGSERIAL NOT NULL, + remoteaddr TEXT, + domain TEXT, + qtype TEXT, + timestamp TIMESTAMP + ) + `) + defer rows.Close() + auditLogger := &PostgresqlAuditLogger{ + backend: pc, + mesgs: make(chan *AuditMesg, AUDIT_LOG_OUTPUT_BUFFER), + expire: expire, + } + go auditLogger.Run() + go auditLogger.Expire() + return auditLogger +} + +func (pl *PostgresqlAuditLogger) Run() { + for { + select { + case mesg := <-pl.mesgs: + rows, err := pl.backend.Query(`INSERT INTO audit (remoteaddr, domain, qtype, timestamp) VALUES ($1, $2, $3, $4)`, + mesg.RemoteAddr, mesg.Domain, mesg.QType, mesg.Timestamp, + ) + rows.Close() + if err != nil { + logger.Error("Can't write to postgresql audit log: %v", err) + continue + } + } + } +} + +func (pl *PostgresqlAuditLogger) Write(mesg *AuditMesg) { + pl.mesgs <- mesg +} + +func (pl *PostgresqlAuditLogger) Expire() { + for { + expireTime := time.Now().Add(time.Duration(-pl.expire) * time.Second) + rows, err := pl.backend.Query(`DELETE FROM audit WHERE timestamp < $1`, expireTime) + rows.Close() + if err != nil { + logger.Error("Can't expire postgresql audit log: %v", err) + } + time.Sleep(time.Duration(pl.expire) * time.Second / 2) + } +} diff --git a/etc/godns.conf b/etc/godns.conf index 9a947eb..76b44e7 100644 --- a/etc/godns.conf +++ b/etc/godns.conf @@ -30,6 +30,17 @@ port = 6379 db = 0 password ="" +[postgresql] +host = "127.0.0.1" +port = 5432 +user = "godns" +password = "godns" +db = "godns" +sslmode = "require" +sslcert = "" +sslkey = "" +sslrootcert = "" + [memcache] servers = ["127.0.0.1:11211"] @@ -39,6 +50,11 @@ file = "./godns.log" level = "INFO" #DEBUG | INFO |NOTICE | WARN | ERROR +# [audit] +# backend option [redis|postgresql|file] +# file backend not implemented yet +# backend = "redis" +# expire = 864000 # 10 days [cache] # backend option [memory|memcache|redis] diff --git a/handler.go b/handler.go index 66d5152..fc94f31 100644 --- a/handler.go +++ b/handler.go @@ -27,6 +27,7 @@ type GODNSHandler struct { resolver *Resolver cache, negCache Cache hosts Hosts + audit AuditLogger } func NewHandler() *GODNSHandler { @@ -76,7 +77,14 @@ func NewHandler() *GODNSHandler { hosts = NewHosts(settings.Hosts, settings.Redis) } - return &GODNSHandler{resolver, cache, negCache, hosts} + var auditLogger AuditLogger + + switch settings.Audit.Backend { + case "redis": + auditLogger = NewRedisAuditLogger(settings.Redis, settings.Audit.Expire) + } + + return &GODNSHandler{resolver, cache, negCache, hosts, auditLogger} } func (h *GODNSHandler) do(Net string, w dns.ResponseWriter, req *dns.Msg) { @@ -91,6 +99,11 @@ func (h *GODNSHandler) do(Net string, w dns.ResponseWriter, req *dns.Msg) { } logger.Info("%s lookup %s", remote, Q.String()) + if h.audit != nil { + auditMesg := NewAuditMessage(remote.String(), Q.qname, Q.qtype) + h.audit.Write(auditMesg) + } + IPQuery := h.isIPQuery(q) // Query hosts diff --git a/settings.go b/settings.go index f2310a6..9e30e2b 100644 --- a/settings.go +++ b/settings.go @@ -24,13 +24,15 @@ var LogLevelMap = map[string]int{ type Settings struct { Version string Debug bool - Server DNSServerSettings `toml:"server"` - ResolvConfig ResolvSettings `toml:"resolv"` - Redis RedisSettings `toml:"redis"` - Memcache MemcacheSettings `toml:"memcache"` - Log LogSettings `toml:"log"` - Cache CacheSettings `toml:"cache"` - Hosts HostsSettings `toml:"hosts"` + Server DNSServerSettings `toml:"server"` + ResolvConfig ResolvSettings `toml:"resolv"` + Redis RedisSettings `toml:"redis"` + Memcache MemcacheSettings `toml:"memcache"` + Postgresql PostgresqlSettings `toml:"postgresql"` + Log LogSettings `toml:"log"` + Cache CacheSettings `toml:"cache"` + Hosts HostsSettings `toml:"hosts"` + Audit AuditSettings `toml:"audit"` } type ResolvSettings struct { @@ -53,6 +55,18 @@ type RedisSettings struct { Password string } +type PostgresqlSettings struct { + Host string + Port int + DB string + User string + Password string + Sslmode string + Sslcert string + Sslkey string + Sslrootcert string +} + type MemcacheSettings struct { Servers []string } @@ -67,6 +81,11 @@ type LogSettings struct { Level string } +type AuditSettings struct { + Expire int64 + Backend string +} + func (ls LogSettings) LogLevel() int { l, ok := LogLevelMap[ls.Level] if !ok {