Merge: + qlog: anonymize_client_ip setting

Close #916

* commit 'a0be7f5566a7d5986eb04ed7ffb79c5230171e2a':
  + client: handle hide_client_ip
  + qlog: hide_client_ip setting
This commit is contained in:
Simon Zolin 2020-03-20 14:55:55 +03:00
commit 5fe984741e
14 changed files with 118 additions and 31 deletions

View File

@ -1287,12 +1287,22 @@ Request:
{ {
"enabled": true | false "enabled": true | false
"interval": 1 | 7 | 30 | 90 "interval": 1 | 7 | 30 | 90
"anonymize_client_ip": true | false // anonymize clients' IP addresses
} }
Response: Response:
200 OK 200 OK
`anonymize_client_ip`:
1. New log entries written to a log file will contain modified client IP addresses. Note that there's no way to obtain the full IP address later for these entries.
2. `GET /control/querylog` response data will contain modified client IP addresses (masked /24 or /112).
3. Searching by client IP won't work for the previously stored entries.
How `anonymize_client_ip` affects Stats:
1. After AGH restart, new stats entries will contain modified client IP addresses.
2. Existing entries are not affected.
### API: Get querylog parameters ### API: Get querylog parameters
@ -1307,6 +1317,7 @@ Response:
{ {
"enabled": true | false "enabled": true | false
"interval": 1 | 7 | 30 | 90 "interval": 1 | 7 | 30 | 90
"anonymize_client_ip": true | false
} }

View File

@ -199,6 +199,8 @@
"query_log_disabled": "The query log is disabled and can be configured in the <0>settings</0>", "query_log_disabled": "The query log is disabled and can be configured in the <0>settings</0>",
"query_log_strict_search": "Use double quotes for strict search", "query_log_strict_search": "Use double quotes for strict search",
"query_log_retention_confirm": "Are you sure you want to change query log retention? If you decrease the interval value, some data will be lost", "query_log_retention_confirm": "Are you sure you want to change query log retention? If you decrease the interval value, some data will be lost",
"anonymize_client_ip": "Anonymize client IP",
"anonymize_client_ip_desc": "Don't save the full IP address of the client in logs and statistics",
"dns_config": "DNS server configuration", "dns_config": "DNS server configuration",
"blocking_mode": "Blocking mode", "blocking_mode": "Blocking mode",
"default": "Default", "default": "Default",

View File

@ -42,6 +42,16 @@ const Form = (props) => {
disabled={processing} disabled={processing}
/> />
</div> </div>
<div className="form__group form__group--settings">
<Field
name="anonymize_client_ip"
type="checkbox"
component={renderSelectField}
placeholder={t('anonymize_client_ip')}
subtitle={t('anonymize_client_ip_desc')}
disabled={processing}
/>
</div>
<label className="form__label"> <label className="form__label">
<Trans>query_log_retention</Trans> <Trans>query_log_retention</Trans>
</label> </label>

View File

@ -30,7 +30,7 @@ class LogsConfig extends Component {
render() { render() {
const { const {
t, enabled, interval, processing, processingClear, t, enabled, interval, processing, processingClear, anonymize_client_ip,
} = this.props; } = this.props;
return ( return (
@ -44,6 +44,7 @@ class LogsConfig extends Component {
initialValues={{ initialValues={{
enabled, enabled,
interval, interval,
anonymize_client_ip,
}} }}
onSubmit={this.handleFormSubmit} onSubmit={this.handleFormSubmit}
processing={processing} processing={processing}
@ -59,6 +60,7 @@ class LogsConfig extends Component {
LogsConfig.propTypes = { LogsConfig.propTypes = {
interval: PropTypes.number.isRequired, interval: PropTypes.number.isRequired,
enabled: PropTypes.bool.isRequired, enabled: PropTypes.bool.isRequired,
anonymize_client_ip: PropTypes.bool.isRequired,
processing: PropTypes.bool.isRequired, processing: PropTypes.bool.isRequired,
processingClear: PropTypes.bool.isRequired, processingClear: PropTypes.bool.isRequired,
setLogsConfig: PropTypes.func.isRequired, setLogsConfig: PropTypes.func.isRequired,

View File

@ -106,6 +106,7 @@ class Settings extends Component {
<LogsConfig <LogsConfig
enabled={queryLogs.enabled} enabled={queryLogs.enabled}
interval={queryLogs.interval} interval={queryLogs.interval}
anonymize_client_ip={queryLogs.anonymize_client_ip}
processing={queryLogs.processingSetConfig} processing={queryLogs.processingSetConfig}
processingClear={queryLogs.processingClear} processingClear={queryLogs.processingClear}
setLogsConfig={setLogsConfig} setLogsConfig={setLogsConfig}

View File

@ -134,6 +134,7 @@ const queryLogs = handleActions(
oldest: '', oldest: '',
filter: DEFAULT_LOGS_FILTER, filter: DEFAULT_LOGS_FILTER,
isFiltered: false, isFiltered: false,
anonymize_client_ip: false,
}, },
); );

View File

@ -80,6 +80,7 @@ type dnsConfig struct {
QueryLogEnabled bool `yaml:"querylog_enabled"` // if true, query log is enabled QueryLogEnabled bool `yaml:"querylog_enabled"` // if true, query log is enabled
QueryLogInterval uint32 `yaml:"querylog_interval"` // time interval for query log (in days) QueryLogInterval uint32 `yaml:"querylog_interval"` // time interval for query log (in days)
QueryLogMemSize uint32 `yaml:"querylog_size_memory"` // number of entries kept in memory before they are flushed to disk QueryLogMemSize uint32 `yaml:"querylog_size_memory"` // number of entries kept in memory before they are flushed to disk
AnonymizeClientIP bool `yaml:"anonymize_client_ip"` // anonymize clients' IP addresses in logs and stats
dnsforward.FilteringConfig `yaml:",inline"` dnsforward.FilteringConfig `yaml:",inline"`
@ -242,6 +243,7 @@ func (c *configuration) write() error {
config.DNS.QueryLogEnabled = dc.Enabled config.DNS.QueryLogEnabled = dc.Enabled
config.DNS.QueryLogInterval = dc.Interval config.DNS.QueryLogInterval = dc.Interval
config.DNS.QueryLogMemSize = dc.MemSize config.DNS.QueryLogMemSize = dc.MemSize
config.DNS.AnonymizeClientIP = dc.AnonymizeClientIP
} }
if Context.dnsFilter != nil { if Context.dnsFilter != nil {

View File

@ -31,6 +31,7 @@ func initDNSServer() error {
statsConf := stats.Config{ statsConf := stats.Config{
Filename: filepath.Join(baseDir, "stats.db"), Filename: filepath.Join(baseDir, "stats.db"),
LimitDays: config.DNS.StatsInterval, LimitDays: config.DNS.StatsInterval,
AnonymizeClientIP: config.DNS.AnonymizeClientIP,
ConfigModified: onConfigModified, ConfigModified: onConfigModified,
HTTPRegister: httpRegister, HTTPRegister: httpRegister,
} }
@ -43,6 +44,7 @@ func initDNSServer() error {
BaseDir: baseDir, BaseDir: baseDir,
Interval: config.DNS.QueryLogInterval, Interval: config.DNS.QueryLogInterval,
MemSize: config.DNS.QueryLogMemSize, MemSize: config.DNS.QueryLogMemSize,
AnonymizeClientIP: config.DNS.AnonymizeClientIP,
ConfigModified: onConfigModified, ConfigModified: onConfigModified,
HTTPRegister: httpRegister, HTTPRegister: httpRegister,
} }

View File

@ -1585,6 +1585,9 @@ definitions:
interval: interval:
type: "integer" type: "integer"
description: "Time period to keep data (1 | 7 | 30 | 90)" description: "Time period to keep data (1 | 7 | 30 | 90)"
anonymize_client_ip:
type: "boolean"
description: "Anonymize clients' IP addresses"
TlsConfig: TlsConfig:
type: "object" type: "object"

View File

@ -2,6 +2,7 @@ package querylog
import ( import (
"fmt" "fmt"
"net"
"os" "os"
"path/filepath" "path/filepath"
"strconv" "strconv"
@ -66,6 +67,7 @@ func (l *queryLog) WriteDiskConfig(dc *DiskConfig) {
dc.Enabled = l.conf.Enabled dc.Enabled = l.conf.Enabled
dc.Interval = l.conf.Interval dc.Interval = l.conf.Interval
dc.MemSize = l.conf.MemSize dc.MemSize = l.conf.MemSize
dc.AnonymizeClientIP = l.conf.AnonymizeClientIP
} }
// Clear memory buffer and remove log files // Clear memory buffer and remove log files
@ -123,7 +125,7 @@ func (l *queryLog) Add(params AddParams) {
now := time.Now() now := time.Now()
entry := logEntry{ entry := logEntry{
IP: params.ClientIP.String(), IP: l.getClientIP(params.ClientIP.String()),
Time: now, Time: now,
Result: *params.Result, Result: *params.Result,
@ -196,6 +198,10 @@ const (
func (l *queryLog) getData(params getDataParams) map[string]interface{} { func (l *queryLog) getData(params getDataParams) map[string]interface{} {
now := time.Now() now := time.Now()
if len(params.Client) != 0 && l.conf.AnonymizeClientIP {
params.Client = l.getClientIP(params.Client)
}
// add from file // add from file
fileEntries, oldest, total := l.searchFiles(params) fileEntries, oldest, total := l.searchFiles(params)
@ -246,7 +252,7 @@ func (l *queryLog) getData(params getDataParams) map[string]interface{} {
// the elements order is already reversed (from newer to older) // the elements order is already reversed (from newer to older)
for i := 0; i < len(entries); i++ { for i := 0; i < len(entries); i++ {
entry := entries[i] entry := entries[i]
jsonEntry := logEntryToJSONEntry(entry) jsonEntry := l.logEntryToJSONEntry(entry)
data = append(data, jsonEntry) data = append(data, jsonEntry)
} }
@ -262,7 +268,26 @@ func (l *queryLog) getData(params getDataParams) map[string]interface{} {
return result return result
} }
func logEntryToJSONEntry(entry *logEntry) map[string]interface{} { // Get Client IP address
func (l *queryLog) getClientIP(clientIP string) string {
if l.conf.AnonymizeClientIP {
ip := net.ParseIP(clientIP)
if ip != nil {
ip4 := ip.To4()
const AnonymizeClientIP4Mask = 24
const AnonymizeClientIP6Mask = 112
if ip4 != nil {
clientIP = ip4.Mask(net.CIDRMask(AnonymizeClientIP4Mask, 32)).String()
} else {
clientIP = ip.Mask(net.CIDRMask(AnonymizeClientIP6Mask, 128)).String()
}
}
}
return clientIP
}
func (l *queryLog) logEntryToJSONEntry(entry *logEntry) map[string]interface{} {
var msg *dns.Msg var msg *dns.Msg
if len(entry.Answer) > 0 { if len(entry.Answer) > 0 {
@ -277,7 +302,7 @@ func logEntryToJSONEntry(entry *logEntry) map[string]interface{} {
"reason": entry.Result.Reason.String(), "reason": entry.Result.Reason.String(),
"elapsedMs": strconv.FormatFloat(entry.Elapsed.Seconds()*1000, 'f', -1, 64), "elapsedMs": strconv.FormatFloat(entry.Elapsed.Seconds()*1000, 'f', -1, 64),
"time": entry.Time.Format(time.RFC3339Nano), "time": entry.Time.Format(time.RFC3339Nano),
"client": entry.IP, "client": l.getClientIP(entry.IP),
} }
jsonEntry["question"] = map[string]interface{}{ jsonEntry["question"] = map[string]interface{}{
"host": entry.QHost, "host": entry.QHost,

View File

@ -108,6 +108,7 @@ func (l *queryLog) handleQueryLogClear(w http.ResponseWriter, r *http.Request) {
type qlogConfig struct { type qlogConfig struct {
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
Interval uint32 `json:"interval"` Interval uint32 `json:"interval"`
AnonymizeClientIP bool `json:"anonymize_client_ip"`
} }
// Get configuration // Get configuration
@ -115,6 +116,7 @@ func (l *queryLog) handleQueryLogInfo(w http.ResponseWriter, r *http.Request) {
resp := qlogConfig{} resp := qlogConfig{}
resp.Enabled = l.conf.Enabled resp.Enabled = l.conf.Enabled
resp.Interval = l.conf.Interval resp.Interval = l.conf.Interval
resp.AnonymizeClientIP = l.conf.AnonymizeClientIP
jsonVal, err := json.Marshal(resp) jsonVal, err := json.Marshal(resp)
if err != nil { if err != nil {
@ -151,6 +153,9 @@ func (l *queryLog) handleQueryLogConfig(w http.ResponseWriter, r *http.Request)
if req.Exists("interval") { if req.Exists("interval") {
conf.Interval = d.Interval conf.Interval = d.Interval
} }
if req.Exists("anonymize_client_ip") {
conf.AnonymizeClientIP = d.AnonymizeClientIP
}
l.conf = &conf l.conf = &conf
l.lock.Unlock() l.lock.Unlock()

View File

@ -14,6 +14,7 @@ type DiskConfig struct {
Enabled bool Enabled bool
Interval uint32 Interval uint32
MemSize uint32 MemSize uint32
AnonymizeClientIP bool
} }
// QueryLog - main interface // QueryLog - main interface
@ -36,6 +37,7 @@ type Config struct {
BaseDir string // directory where log file is stored BaseDir string // directory where log file is stored
Interval uint32 // interval to rotate logs (in days) Interval uint32 // interval to rotate logs (in days)
MemSize uint32 // number of entries kept in memory before they are flushed to disk MemSize uint32 // number of entries kept in memory before they are flushed to disk
AnonymizeClientIP bool // anonymize clients' IP addresses
// Called when the configuration is changed by HTTP request // Called when the configuration is changed by HTTP request
ConfigModified func() ConfigModified func()

View File

@ -19,6 +19,7 @@ type Config struct {
Filename string // database file name Filename string // database file name
LimitDays uint32 // time limit (in days) LimitDays uint32 // time limit (in days)
UnitID unitIDCallback // user function to get the current unit ID. If nil, the current time hour is used. UnitID unitIDCallback // user function to get the current unit ID. If nil, the current time hour is used.
AnonymizeClientIP bool // anonymize clients' IP addresses
// Called when the configuration is changed by HTTP request // Called when the configuration is changed by HTTP request
ConfigModified func() ConfigModified func()

View File

@ -5,6 +5,7 @@ import (
"encoding/binary" "encoding/binary"
"encoding/gob" "encoding/gob"
"fmt" "fmt"
"net"
"os" "os"
"sort" "sort"
"sync" "sync"
@ -442,6 +443,25 @@ func (s *statsCtx) clear() {
log.Debug("Stats: cleared") log.Debug("Stats: cleared")
} }
// Get Client IP address
func (s *statsCtx) getClientIP(clientIP string) string {
if s.conf.AnonymizeClientIP {
ip := net.ParseIP(clientIP)
if ip != nil {
ip4 := ip.To4()
const AnonymizeClientIP4Mask = 24
const AnonymizeClientIP6Mask = 112
if ip4 != nil {
clientIP = ip4.Mask(net.CIDRMask(AnonymizeClientIP4Mask, 32)).String()
} else {
clientIP = ip.Mask(net.CIDRMask(AnonymizeClientIP6Mask, 128)).String()
}
}
}
return clientIP
}
func (s *statsCtx) Update(e Entry) { func (s *statsCtx) Update(e Entry) {
if e.Result == 0 || if e.Result == 0 ||
e.Result >= rLast || e.Result >= rLast ||
@ -449,7 +469,7 @@ func (s *statsCtx) Update(e Entry) {
!(len(e.Client) == 4 || len(e.Client) == 16) { !(len(e.Client) == 4 || len(e.Client) == 16) {
return return
} }
client := e.Client.String() client := s.getClientIP(e.Client.String())
s.unitLock.Lock() s.unitLock.Lock()
u := s.unit u := s.unit