Implement whitelists

Fixes #293
This commit is contained in:
Frank Denis 2018-04-07 23:02:40 +02:00
parent ceb2d55afd
commit 65e6b8569e
8 changed files with 213 additions and 0 deletions

View File

@ -37,6 +37,7 @@ type Config struct {
QueryLog QueryLogConfig `toml:"query_log"`
NxLog NxLogConfig `toml:"nx_log"`
BlockName BlockNameConfig `toml:"blacklist"`
WhitelistName WhitelistNameConfig `toml:"whitelist"`
BlockIP BlockIPConfig `toml:"ip_blacklist"`
ForwardFile string `toml:"forwarding_rules"`
CloakFile string `toml:"cloaking_rules"`
@ -121,6 +122,12 @@ type BlockNameConfig struct {
Format string `toml:"log_format"`
}
type WhitelistNameConfig struct {
File string `toml:"whitelist_file"`
LogFile string `toml:"log_file"`
Format string `toml:"log_format"`
}
type BlockIPConfig struct {
File string `toml:"blacklist_file"`
LogFile string `toml:"log_file"`
@ -294,6 +301,18 @@ func ConfigLoad(proxy *Proxy, svcFlag *string) error {
proxy.blockNameFormat = config.BlockName.Format
proxy.blockNameLogFile = config.BlockName.LogFile
if len(config.WhitelistName.Format) == 0 {
config.WhitelistName.Format = "tsv"
} else {
config.WhitelistName.Format = strings.ToLower(config.WhitelistName.Format)
}
if config.WhitelistName.Format != "tsv" && config.WhitelistName.Format != "ltsv" {
return errors.New("Unsupported whitelist log format")
}
proxy.whitelistNameFile = config.WhitelistName.File
proxy.whitelistNameFormat = config.WhitelistName.Format
proxy.whitelistNameLogFile = config.WhitelistName.LogFile
if len(config.BlockIP.Format) == 0 {
config.BlockIP.Format = "tsv"
} else {

View File

@ -341,6 +341,34 @@ cache_neg_ttl = 60
######################################################
# Pattern-based whitelisting (blacklists bypass) #
######################################################
## Whitelists support the same patterns as blacklists
## If a name matches a whitelist entry, the corresponding session
## will bypass names and IP filters.
##
## Time-based rules are also supported to make some websites only accessible at specific times of the day.
[whitelist]
## Path to the file of whitelisting rules (absolute, or relative to the same directory as the executable file)
# whitelist_file = 'whitelist.txt'
## Optional path to a file logging whitelisted queries
# log_file = 'whitelisted.log'
## Optional log format: tsv or ltsv (default: tsv)
# log_format = 'tsv'
##########################################
# Time access restrictions #
##########################################

View File

@ -0,0 +1,22 @@
###########################
# Whitelist #
###########################
## Rules for name-based query whitelisting, one per line
##
## Example of valid patterns:
##
## ads.* | matches anything with an "ads." prefix
## *.example.com | matches example.com and all names within that zone such as www.example.com
## example.com | identical to the above
## *sex* | matches any name containing that substring
## ads[0-9]* | matches "ads" followed by one or more digits
## ads*.example* | *, ? and [] can be used anywhere, but prefixes/suffixes are faster
## Time-based rules
# *.youtube.* @time-to-play
# facebook.com @play

View File

@ -88,6 +88,9 @@ func (plugin *PluginBlockIP) Reload() error {
}
func (plugin *PluginBlockIP) Eval(pluginsState *PluginsState, msg *dns.Msg) error {
if pluginsState.sessionData["whitelisted"] != nil {
return nil
}
answers := msg.Answer
if len(answers) == 0 {
return nil

View File

@ -83,6 +83,9 @@ func (plugin *PluginBlockName) Reload() error {
}
func (plugin *PluginBlockName) Eval(pluginsState *PluginsState, msg *dns.Msg) error {
if pluginsState.sessionData["whitelisted"] != nil {
return nil
}
questions := msg.Question
if len(questions) != 1 {
return nil

View File

@ -0,0 +1,132 @@
package main
import (
"errors"
"fmt"
"io/ioutil"
"net"
"strings"
"time"
"unicode"
"github.com/jedisct1/dlog"
"github.com/miekg/dns"
lumberjack "gopkg.in/natefinch/lumberjack.v2"
)
type PluginWhitelistName struct {
allWeeklyRanges *map[string]WeeklyRanges
patternMatcher *PatternMatcher
logger *lumberjack.Logger
format string
}
func (plugin *PluginWhitelistName) Name() string {
return "whitelist_name"
}
func (plugin *PluginWhitelistName) Description() string {
return "Whitelists DNS queries matching name patterns"
}
func (plugin *PluginWhitelistName) Init(proxy *Proxy) error {
dlog.Noticef("Loading the set of whitelisting rules from [%s]", proxy.whitelistNameFile)
bin, err := ioutil.ReadFile(proxy.whitelistNameFile)
if err != nil {
return err
}
plugin.allWeeklyRanges = proxy.allWeeklyRanges
plugin.patternMatcher = NewPatternPatcher()
for lineNo, line := range strings.Split(string(bin), "\n") {
line = strings.TrimFunc(line, unicode.IsSpace)
if len(line) == 0 || strings.HasPrefix(line, "#") {
continue
}
parts := strings.Split(line, "@")
timeRangeName := ""
if len(parts) == 2 {
line = strings.TrimFunc(parts[0], unicode.IsSpace)
timeRangeName = strings.TrimFunc(parts[1], unicode.IsSpace)
} else if len(parts) > 2 {
dlog.Errorf("Syntax error in whitelist rules at line %d -- Unexpected @ character", 1+lineNo)
continue
}
var weeklyRanges *WeeklyRanges
if len(timeRangeName) > 0 {
weeklyRangesX, ok := (*plugin.allWeeklyRanges)[timeRangeName]
if !ok {
dlog.Errorf("Time range [%s] not found at line %d", timeRangeName, 1+lineNo)
} else {
weeklyRanges = &weeklyRangesX
}
}
if _, err := plugin.patternMatcher.Add(line, weeklyRanges, lineNo+1); err != nil {
dlog.Error(err)
continue
}
}
if len(proxy.whitelistNameLogFile) == 0 {
return nil
}
plugin.logger = &lumberjack.Logger{LocalTime: true, MaxSize: proxy.logMaxSize, MaxAge: proxy.logMaxAge, MaxBackups: proxy.logMaxBackups, Filename: proxy.whitelistNameLogFile, Compress: true}
plugin.format = proxy.whitelistNameFormat
return nil
}
func (plugin *PluginWhitelistName) Drop() error {
return nil
}
func (plugin *PluginWhitelistName) Reload() error {
return nil
}
func (plugin *PluginWhitelistName) Eval(pluginsState *PluginsState, msg *dns.Msg) error {
questions := msg.Question
if len(questions) != 1 {
return nil
}
qName := strings.ToLower(StripTrailingDot(questions[0].Name))
whitelist, reason, xweeklyRanges := plugin.patternMatcher.Eval(qName)
var weeklyRanges *WeeklyRanges
if xweeklyRanges != nil {
weeklyRanges = xweeklyRanges.(*WeeklyRanges)
}
if whitelist {
if weeklyRanges != nil && !weeklyRanges.Match() {
whitelist = false
}
}
if whitelist {
if pluginsState.sessionData == nil {
pluginsState.sessionData = make(map[string]interface{})
}
pluginsState.sessionData["whitelisted"] = true
if plugin.logger != nil {
var clientIPStr string
if pluginsState.clientProto == "udp" {
clientIPStr = (*pluginsState.clientAddr).(*net.UDPAddr).IP.String()
} else {
clientIPStr = (*pluginsState.clientAddr).(*net.TCPAddr).IP.String()
}
var line string
if plugin.format == "tsv" {
now := time.Now()
year, month, day := now.Date()
hour, minute, second := now.Clock()
tsStr := fmt.Sprintf("[%d-%02d-%02d %02d:%02d:%02d]", year, int(month), day, hour, minute, second)
line = fmt.Sprintf("%s\t%s\t%s\t%s\n", tsStr, clientIPStr, StringQuote(qName), StringQuote(reason))
} else if plugin.format == "ltsv" {
line = fmt.Sprintf("time:%d\thost:%s\tqname:%s\tmessage:%s\n", time.Now().Unix(), clientIPStr, StringQuote(qName), StringQuote(reason))
} else {
dlog.Fatalf("Unexpected log format: [%s]", plugin.format)
}
if plugin.logger == nil {
return errors.New("Log file not initialized")
}
plugin.logger.Write([]byte(line))
}
}
return nil
}

View File

@ -45,6 +45,9 @@ func InitPluginsGlobals(pluginsGlobals *PluginsGlobals, proxy *Proxy) error {
if len(proxy.queryLogFile) != 0 {
*queryPlugins = append(*queryPlugins, Plugin(new(PluginQueryLog)))
}
if len(proxy.whitelistNameFile) != 0 {
*queryPlugins = append(*queryPlugins, Plugin(new(PluginWhitelistName)))
}
if len(proxy.blockNameFile) != 0 {
*queryPlugins = append(*queryPlugins, Plugin(new(PluginBlockName)))
}

View File

@ -38,8 +38,11 @@ type Proxy struct {
nxLogFile string
nxLogFormat string
blockNameFile string
whitelistNameFile string
blockNameLogFile string
whitelistNameLogFile string
blockNameFormat string
whitelistNameFormat string
blockIPFile string
blockIPLogFile string
blockIPFormat string