mirror of https://github.com/0xERR0R/blocky.git
Compare commits
7 Commits
036e4f4e11
...
7b9c83ecd2
Author | SHA1 | Date |
---|---|---|
Pascal Gauthier | 7b9c83ecd2 | |
dependabot[bot] | 4ebe1ef21a | |
dependabot[bot] | 7f20d17d2e | |
dependabot[bot] | cbbe8d46f0 | |
dependabot[bot] | 62b1354fba | |
Thomas Anderson | e99c98b4c2 | |
Pascal Gauthier | 05d579a1cd |
|
@ -23,6 +23,7 @@ Blocky is a DNS proxy and ad-blocker for the local network written in Go with fo
|
||||||
- **Blocking** - Blocking of DNS queries with external lists (Ad-block, malware) and allowlisting
|
- **Blocking** - Blocking of DNS queries with external lists (Ad-block, malware) and allowlisting
|
||||||
|
|
||||||
- Definition of allow/denylists per client group (Kids, Smart home devices, etc.)
|
- Definition of allow/denylists per client group (Kids, Smart home devices, etc.)
|
||||||
|
- Denylists can be configured with a schedules map (active/inactive depending of days and hours ranges)
|
||||||
- Periodical reload of external allow/denylists
|
- Periodical reload of external allow/denylists
|
||||||
- Regex support
|
- Regex support
|
||||||
- Blocking of request domain, response CNAME (deep CNAME inspection) and response IP addresses (against IP lists)
|
- Blocking of request domain, response CNAME (deep CNAME inspection) and response IP addresses (against IP lists)
|
||||||
|
|
|
@ -50,7 +50,12 @@ func (cache stringMap) contains(searchString string) bool {
|
||||||
})
|
})
|
||||||
|
|
||||||
if idx < searchBucketLen {
|
if idx < searchBucketLen {
|
||||||
return cache[searchLen][idx*searchLen:idx*searchLen+searchLen] == strings.ToLower(normalized)
|
blockRule := cache[searchLen][idx*searchLen : idx*searchLen+searchLen]
|
||||||
|
if blockRule == normalized {
|
||||||
|
log.PrefixedLog("string_map").Debugf("block rule '%s' matched with '%s'", blockRule, searchString)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
|
@ -132,7 +137,7 @@ func (cache regexCache) elementCount() int {
|
||||||
func (cache regexCache) contains(searchString string) bool {
|
func (cache regexCache) contains(searchString string) bool {
|
||||||
for _, regex := range cache {
|
for _, regex := range cache {
|
||||||
if regex.MatchString(searchString) {
|
if regex.MatchString(searchString) {
|
||||||
log.PrefixedLog("regexCache").Debugf("regex '%s' matched with '%s'", regex, searchString)
|
log.PrefixedLog("regex_cache").Debugf("regex '%s' matched with '%s'", regex, searchString)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ type Blocking struct {
|
||||||
Denylists map[string][]BytesSource `yaml:"denylists"`
|
Denylists map[string][]BytesSource `yaml:"denylists"`
|
||||||
Allowlists map[string][]BytesSource `yaml:"allowlists"`
|
Allowlists map[string][]BytesSource `yaml:"allowlists"`
|
||||||
ClientGroupsBlock map[string][]string `yaml:"clientGroupsBlock"`
|
ClientGroupsBlock map[string][]string `yaml:"clientGroupsBlock"`
|
||||||
|
Schedules Schedules `yaml:"schedules"`
|
||||||
BlockType string `yaml:"blockType" default:"ZEROIP"`
|
BlockType string `yaml:"blockType" default:"ZEROIP"`
|
||||||
BlockTTL Duration `yaml:"blockTTL" default:"6h"`
|
BlockTTL Duration `yaml:"blockTTL" default:"6h"`
|
||||||
Loading SourceLoading `yaml:"loading"`
|
Loading SourceLoading `yaml:"loading"`
|
||||||
|
@ -80,6 +81,26 @@ func (c *Blocking) LogConfig(logger *logrus.Entry) {
|
||||||
log.WithIndent(logger, " ", func(logger *logrus.Entry) {
|
log.WithIndent(logger, " ", func(logger *logrus.Entry) {
|
||||||
c.logListGroups(logger, c.Allowlists)
|
c.logListGroups(logger, c.Allowlists)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
logger.Info("schedules:")
|
||||||
|
log.WithIndent(logger, " ", func(logger *logrus.Entry) {
|
||||||
|
c.logSchedules(logger, c.Schedules)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Blocking) logSchedules(logger *logrus.Entry, schedules map[string][]Schedule) {
|
||||||
|
for group, schedulesList := range schedules {
|
||||||
|
logger.Infof("%s:", group)
|
||||||
|
|
||||||
|
for _, schedule := range schedulesList {
|
||||||
|
logger.Infof(" - days: %s hoursRanges: %s", schedule.Days, schedule.HoursRanges)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, ok := c.Denylists[group]
|
||||||
|
if !ok {
|
||||||
|
logger.Warnf(" !! %s not found in denylists, schedule will have no effect", group)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Blocking) logListGroups(logger *logrus.Entry, listGroups map[string][]BytesSource) {
|
func (c *Blocking) logListGroups(logger *logrus.Entry, listGroups map[string][]BytesSource) {
|
||||||
|
|
|
@ -14,6 +14,8 @@ var _ = Describe("BlockingConfig", func() {
|
||||||
suiteBeforeEach()
|
suiteBeforeEach()
|
||||||
|
|
||||||
BeforeEach(func() {
|
BeforeEach(func() {
|
||||||
|
hr, _ := parseHoursRange("09:00-17:00")
|
||||||
|
|
||||||
cfg = Blocking{
|
cfg = Blocking{
|
||||||
BlockType: "ZEROIP",
|
BlockType: "ZEROIP",
|
||||||
BlockTTL: Duration(time.Minute),
|
BlockTTL: Duration(time.Minute),
|
||||||
|
@ -23,6 +25,15 @@ var _ = Describe("BlockingConfig", func() {
|
||||||
ClientGroupsBlock: map[string][]string{
|
ClientGroupsBlock: map[string][]string{
|
||||||
"default": {"gr1"},
|
"default": {"gr1"},
|
||||||
},
|
},
|
||||||
|
Schedules: Schedules{
|
||||||
|
"gr1": []Schedule{
|
||||||
|
{
|
||||||
|
Days: []day{day(time.Monday)},
|
||||||
|
HoursRanges: []hoursRange{hr},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"gr2": []Schedule{},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -57,6 +68,9 @@ var _ = Describe("BlockingConfig", func() {
|
||||||
|
|
||||||
Expect(hook.Calls).ShouldNot(BeEmpty())
|
Expect(hook.Calls).ShouldNot(BeEmpty())
|
||||||
Expect(hook.Messages[0]).Should(Equal("clientGroupsBlock:"))
|
Expect(hook.Messages[0]).Should(Equal("clientGroupsBlock:"))
|
||||||
|
Expect(hook.Messages).Should(ContainElement(Equal("schedules:")))
|
||||||
|
Expect(hook.Messages).Should(ContainElement(Equal(" - days: [Monday] hoursRanges: [09:00-17:00]")))
|
||||||
|
Expect(hook.Messages).Should(ContainElement(Equal(" !! gr2 not found in denylists, schedule will have no effect")))
|
||||||
Expect(hook.Messages).Should(ContainElement(Equal("blockType = ZEROIP")))
|
Expect(hook.Messages).Should(ContainElement(Equal("blockType = ZEROIP")))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,150 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
validHRParts = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
// Schedule of active blocking
|
||||||
|
type Schedule struct {
|
||||||
|
Days []day `yaml:"days"`
|
||||||
|
HoursRanges []hoursRange `yaml:"hoursRanges"`
|
||||||
|
Active bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type day time.Weekday
|
||||||
|
|
||||||
|
type hoursRange struct {
|
||||||
|
Start time.Time
|
||||||
|
End time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// String implements the fmt.Stringer interface
|
||||||
|
func (d day) String() string {
|
||||||
|
return time.Weekday(d).String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// String implements the fmt.Stringer interface
|
||||||
|
func (hr hoursRange) String() string {
|
||||||
|
return hr.Start.Format("15:04") + "-" + hr.End.Format("15:04")
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalText implements the encoding.TextUnmarshaler interface
|
||||||
|
func (d *day) UnmarshalText(text []byte) error {
|
||||||
|
input := string(text)
|
||||||
|
|
||||||
|
result, err := parseDay(input)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*d = day(result)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalText implements the encoding.TextUnmarshaler interface
|
||||||
|
func (hr *hoursRange) UnmarshalText(text []byte) error {
|
||||||
|
input := string(text)
|
||||||
|
|
||||||
|
result, err := parseHoursRange(input)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*hr = result
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hr *hoursRange) setRefTime() {
|
||||||
|
*hr = hoursRange{
|
||||||
|
Start: getRefTime(hr.Start),
|
||||||
|
End: getRefTime(hr.End),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseDay(day string) (time.Weekday, error) {
|
||||||
|
daysMap := map[string]time.Weekday{
|
||||||
|
"Mon": time.Monday, "Tue": time.Tuesday, "Wed": time.Wednesday,
|
||||||
|
"Thu": time.Thursday, "Fri": time.Friday, "Sat": time.Saturday, "Sun": time.Sunday,
|
||||||
|
}
|
||||||
|
|
||||||
|
if day, exists := daysMap[day]; exists {
|
||||||
|
return day, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return time.Sunday, fmt.Errorf("invalid day: %s", day)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseHoursRange(hours string) (hoursRange, error) {
|
||||||
|
parts := strings.Split(hours, "-")
|
||||||
|
|
||||||
|
if len(parts) != validHRParts {
|
||||||
|
return hoursRange{}, fmt.Errorf("invalid hours range format: %s", hours)
|
||||||
|
}
|
||||||
|
|
||||||
|
start, err := time.Parse("15:04", parts[0])
|
||||||
|
if err != nil {
|
||||||
|
return hoursRange{}, fmt.Errorf("invalid start hour: %s", hours)
|
||||||
|
}
|
||||||
|
|
||||||
|
end, err := time.Parse("15:04", parts[1])
|
||||||
|
if err != nil {
|
||||||
|
return hoursRange{}, fmt.Errorf("invalid end hour: %s", hours)
|
||||||
|
}
|
||||||
|
|
||||||
|
if start.After(end) {
|
||||||
|
return hoursRange{}, fmt.Errorf("start hour is after end hour: %s", hours)
|
||||||
|
}
|
||||||
|
|
||||||
|
return hoursRange{
|
||||||
|
Start: start,
|
||||||
|
End: end,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Schedule) isActive(nowFunc func() time.Time) bool {
|
||||||
|
now := nowFunc()
|
||||||
|
|
||||||
|
dayActive := false
|
||||||
|
curDay := now.Weekday()
|
||||||
|
|
||||||
|
for _, d := range s.Days {
|
||||||
|
if d == day(curDay) {
|
||||||
|
dayActive = true
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !dayActive {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
curTime := now
|
||||||
|
|
||||||
|
for _, hrsRange := range s.HoursRanges {
|
||||||
|
hrsRange.setRefTime()
|
||||||
|
|
||||||
|
refTime := getRefTime(curTime)
|
||||||
|
if refTime == hrsRange.Start {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if refTime.After(hrsRange.Start) && refTime.Before(hrsRange.End) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRefTime(t time.Time) time.Time {
|
||||||
|
return time.Date(0, 1, 1, t.Hour(), t.Minute(), t.Second(), 0, time.UTC)
|
||||||
|
}
|
|
@ -0,0 +1,141 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
. "github.com/onsi/ginkgo/v2"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = Describe("Schedule", func() {
|
||||||
|
var cfg Schedule
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
hr, _ := parseHoursRange("09:00-17:00")
|
||||||
|
|
||||||
|
cfg = Schedule{
|
||||||
|
Days: []day{day(time.Monday)},
|
||||||
|
HoursRanges: []hoursRange{hr},
|
||||||
|
Active: false,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
Describe("day stringer", func() {
|
||||||
|
When("valid day", func() {
|
||||||
|
It("should return the correct day string", func() {
|
||||||
|
day := cfg.Days[0].String()
|
||||||
|
Expect(day).To(Equal(time.Monday.String()))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Describe("hoursRange stringer", func() {
|
||||||
|
When("valid range", func() {
|
||||||
|
It("should return the correct range string", func() {
|
||||||
|
hr := cfg.HoursRanges[0].String()
|
||||||
|
Expect(hr).To(Equal("09:00-17:00"))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Describe("parseDay", func() {
|
||||||
|
When("valid day", func() {
|
||||||
|
It("should return the correct weekday", func() {
|
||||||
|
day, err := parseDay("Mon")
|
||||||
|
Expect(err).Should(Succeed())
|
||||||
|
Expect(day).To(Equal(time.Monday))
|
||||||
|
|
||||||
|
day, err = parseDay("Fri")
|
||||||
|
Expect(err).Should(Succeed())
|
||||||
|
Expect(day).To(Equal(time.Friday))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
When("invalid day", func() {
|
||||||
|
It("should return an error", func() {
|
||||||
|
_, err := parseDay("invalid_day")
|
||||||
|
Expect(err.Error()).To(Equal("invalid day: invalid_day"))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Describe("parseHoursRange", func() {
|
||||||
|
When("valid hour range", func() {
|
||||||
|
It("should return the correct hours range", func() {
|
||||||
|
hr, err := parseHoursRange("09:00-17:00")
|
||||||
|
Expect(err).Should(Succeed())
|
||||||
|
Expect(hr.Start.Format("15:04")).To(Equal("09:00"))
|
||||||
|
Expect(hr.End.Format("15:04")).To(Equal("17:00"))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
When("invalid hour range", func() {
|
||||||
|
It("should return an error", func() {
|
||||||
|
_, err := parseHoursRange("09-17:00")
|
||||||
|
Expect(err.Error()).To(Equal("invalid start hour: 09-17:00"))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
When("invalid hours range format", func() {
|
||||||
|
It("should return an error", func() {
|
||||||
|
_, err := parseHoursRange("18:00")
|
||||||
|
Expect(err.Error()).To(Equal("invalid hours range format: 18:00"))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
When("invalid start hour", func() {
|
||||||
|
It("should return an error", func() {
|
||||||
|
_, err := parseHoursRange("25:00-17:00")
|
||||||
|
Expect(err.Error()).To(Equal("invalid start hour: 25:00-17:00"))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
When("with invalid end hour", func() {
|
||||||
|
It("should return an error", func() {
|
||||||
|
_, err := parseHoursRange("09:00-17:60")
|
||||||
|
Expect(err.Error()).To(Equal("invalid end hour: 09:00-17:60"))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
When("with start hour after end hour", func() {
|
||||||
|
It("should return an error", func() {
|
||||||
|
_, err := parseHoursRange("20:00-08:00")
|
||||||
|
Expect(err.Error()).To(Equal("start hour is after end hour: 20:00-08:00"))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Describe("isActive", func() {
|
||||||
|
When("active schedule hour", func() {
|
||||||
|
It("should return true", func() {
|
||||||
|
fakeTime := getFakeTime(time.Monday, "10:00")
|
||||||
|
active := cfg.isActive(fakeTime)
|
||||||
|
Expect(active).To(BeTrue())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
When("active schedule start time hour ", func() {
|
||||||
|
It("should return true", func() {
|
||||||
|
fakeTime := getFakeTime(time.Monday, "09:00")
|
||||||
|
active := cfg.isActive(fakeTime)
|
||||||
|
Expect(active).To(BeTrue())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
When("inactive schedule hour", func() {
|
||||||
|
It("should return true", func() {
|
||||||
|
fakeTime := getFakeTime(time.Monday, "07:00")
|
||||||
|
active := cfg.isActive(fakeTime)
|
||||||
|
Expect(active).To(BeFalse())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
When("inactive schedule day", func() {
|
||||||
|
It("should return false", func() {
|
||||||
|
fakeTime := getFakeTime(time.Sunday, "09:00")
|
||||||
|
active := cfg.isActive(fakeTime)
|
||||||
|
Expect(active).To(BeFalse())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,107 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/0xERR0R/blocky/evt"
|
||||||
|
"github.com/0xERR0R/blocky/log"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
refreshInterval = time.Minute
|
||||||
|
)
|
||||||
|
|
||||||
|
// Schedules is the list of schedule for the defined blacklist
|
||||||
|
type Schedules map[string][]Schedule
|
||||||
|
|
||||||
|
// Refresh update Schedule to set the Active field boolean
|
||||||
|
func (s Schedules) Refresh(ctx context.Context, nowFunc func() time.Time) {
|
||||||
|
logger := log.PrefixedLog("refresh_schedules")
|
||||||
|
|
||||||
|
s.setActive(nowFunc, logger) // initial schedules refresh (after blocky start)
|
||||||
|
syncSchedulesWithSystemTime(s, nowFunc, logger)
|
||||||
|
|
||||||
|
ticker := time.NewTicker(refreshInterval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
s.setActive(nowFunc, logger)
|
||||||
|
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func syncSchedulesWithSystemTime(s Schedules, nowFunc func() time.Time, logger *logrus.Entry) {
|
||||||
|
waitTime := getWaitTimeBeforeNextMinute()
|
||||||
|
time.Sleep(waitTime)
|
||||||
|
|
||||||
|
s.setActive(nowFunc, logger) // now in sync with system time
|
||||||
|
}
|
||||||
|
|
||||||
|
func getWaitTimeBeforeNextMinute() time.Duration {
|
||||||
|
return time.Until(
|
||||||
|
time.Date(
|
||||||
|
time.Now().Year(),
|
||||||
|
time.Now().Month(),
|
||||||
|
time.Now().Day(),
|
||||||
|
time.Now().Hour(),
|
||||||
|
time.Now().Minute()+1, 0, 0,
|
||||||
|
time.Now().Location(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Schedules) setActive(nowFunc func() time.Time, logger *logrus.Entry) {
|
||||||
|
if nowFunc == nil {
|
||||||
|
nowFunc = time.Now
|
||||||
|
}
|
||||||
|
|
||||||
|
for group, schedList := range s {
|
||||||
|
for i, schedule := range schedList {
|
||||||
|
active := false
|
||||||
|
activeStr := "inactive"
|
||||||
|
|
||||||
|
if schedule.isActive(nowFunc) {
|
||||||
|
active = true
|
||||||
|
activeStr = "active"
|
||||||
|
}
|
||||||
|
|
||||||
|
if schedule.Active != active {
|
||||||
|
s[group][i] = Schedule{
|
||||||
|
Days: schedule.Days,
|
||||||
|
HoursRanges: schedule.HoursRanges,
|
||||||
|
Active: active,
|
||||||
|
}
|
||||||
|
|
||||||
|
activeFlag := 0
|
||||||
|
if active {
|
||||||
|
activeFlag = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
evt.Bus().Publish(evt.SchedulesActive, group, activeFlag)
|
||||||
|
logger.Infof("group %s is now %s", group, activeStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsActive checks if the schedules of the group is active at the current time
|
||||||
|
func (s Schedules) IsActive(group string, nowFunc func() time.Time) bool {
|
||||||
|
if nowFunc == nil {
|
||||||
|
nowFunc = time.Now
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, schedule := range s[group] {
|
||||||
|
if schedule.isActive(nowFunc) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
|
@ -0,0 +1,107 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
. "github.com/onsi/ginkgo/v2"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = Describe("Schedules", func() {
|
||||||
|
const (
|
||||||
|
group = "testGroup"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
schedules Schedules
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
)
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
schedules = make(Schedules)
|
||||||
|
ctx, cancel = context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
hr, _ := parseHoursRange("09:00-17:00")
|
||||||
|
|
||||||
|
schedules[group] = []Schedule{
|
||||||
|
{
|
||||||
|
Days: []day{day(time.Saturday)},
|
||||||
|
HoursRanges: []hoursRange{hr},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
AfterEach(func() {
|
||||||
|
cancel()
|
||||||
|
})
|
||||||
|
|
||||||
|
Describe("Refresh", func() {
|
||||||
|
When("refresh loop running", func() {
|
||||||
|
go schedules.Refresh(ctx, nil)
|
||||||
|
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
|
||||||
|
It("should set the Active field", func() {
|
||||||
|
now := time.Now()
|
||||||
|
weekDay := now.Weekday()
|
||||||
|
hour := now.Hour()
|
||||||
|
|
||||||
|
if weekDay == time.Saturday && hour >= 9 && hour < 17 {
|
||||||
|
Expect(schedules[group][0].Active).To(BeTrue())
|
||||||
|
} else {
|
||||||
|
Expect(schedules[group][0].Active).To(BeFalse())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should set the Active field to true when schedule is active", func() {
|
||||||
|
fakeTime := getFakeTime(time.Saturday, "10:00")
|
||||||
|
|
||||||
|
go schedules.Refresh(ctx, fakeTime)
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
|
||||||
|
Expect(schedules[group][0].Active).To(BeTrue())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should set the Active field to false when schedule is inactive", func() {
|
||||||
|
fakeTime := getFakeTime(time.Saturday, "19:00")
|
||||||
|
schedules[group][0].Active = true
|
||||||
|
|
||||||
|
go schedules.Refresh(ctx, fakeTime)
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
|
||||||
|
Expect(schedules[group][0].Active).To(BeFalse())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Describe("IsActive", func() {
|
||||||
|
When("schedule is active", func() {
|
||||||
|
It("should return true", func() {
|
||||||
|
fakeTime := getFakeTime(time.Saturday, "10:00")
|
||||||
|
isActive := schedules.IsActive(group, fakeTime)
|
||||||
|
Expect(isActive).To(BeTrue())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
When("schedule is inactive", func() {
|
||||||
|
It("should return false", func() {
|
||||||
|
fakeTime := getFakeTime(time.Sunday, "10:00")
|
||||||
|
isActive := schedules.IsActive(group, fakeTime)
|
||||||
|
Expect(isActive).To(BeFalse())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
func getFakeTime(wk time.Weekday, hr string) func() time.Time {
|
||||||
|
return func() time.Time {
|
||||||
|
t, err := time.Parse("15:04", hr)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return getRefTime(t).AddDate(0, 0, int(wk)+1)
|
||||||
|
}
|
||||||
|
}
|
|
@ -77,6 +77,8 @@ blocking:
|
||||||
*.example.com
|
*.example.com
|
||||||
special:
|
special:
|
||||||
- https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/fakenews/hosts
|
- https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/fakenews/hosts
|
||||||
|
youtube:
|
||||||
|
- https://raw.githubusercontent.com/gieljnssns/Social-media-Blocklists/master/pihole-youtube.txt
|
||||||
# definition of allowlist groups.
|
# definition of allowlist groups.
|
||||||
# Note: if the same group has both allow/denylists, allowlists take precedence. Meaning if a domain is both blocked and allowed, it will be allowed.
|
# Note: if the same group has both allow/denylists, allowlists take precedence. Meaning if a domain is both blocked and allowed, it will be allowed.
|
||||||
# If a group has only allowlist entries, only domains from this list are allowed, and all others be blocked.
|
# If a group has only allowlist entries, only domains from this list are allowed, and all others be blocked.
|
||||||
|
@ -89,6 +91,16 @@ blocking:
|
||||||
allowlistdomain.com
|
allowlistdomain.com
|
||||||
# this is a regex
|
# this is a regex
|
||||||
/^banners?[_.-]/
|
/^banners?[_.-]/
|
||||||
|
# definition of the blacklist schedules. These define the active schedule (blocking). When inactive, the list will be skipped.
|
||||||
|
schedules:
|
||||||
|
youtube:
|
||||||
|
- days: ["Mon", "Tue", "Wed", "Thu"]
|
||||||
|
hoursRanges:
|
||||||
|
- "06:45-08:30"
|
||||||
|
- "19:00-23:30"
|
||||||
|
- days: ["Sun"]
|
||||||
|
hoursRanges:
|
||||||
|
- "20:00-23:30"
|
||||||
# definition: which groups should be applied for which client
|
# definition: which groups should be applied for which client
|
||||||
clientGroupsBlock:
|
clientGroupsBlock:
|
||||||
# default will be used, if no special definition for a client name exists
|
# default will be used, if no special definition for a client name exists
|
||||||
|
@ -101,6 +113,7 @@ blocking:
|
||||||
- ads
|
- ads
|
||||||
192.168.178.1/24:
|
192.168.178.1/24:
|
||||||
- special
|
- special
|
||||||
|
- youtube
|
||||||
# which response will be sent, if query is blocked:
|
# which response will be sent, if query is blocked:
|
||||||
# zeroIp: 0.0.0.0 will be returned (default)
|
# zeroIp: 0.0.0.0 will be returned (default)
|
||||||
# nxDomain: return NXDOMAIN as return code
|
# nxDomain: return NXDOMAIN as return code
|
||||||
|
|
|
@ -89,7 +89,6 @@ This applies to all of them. The default strategy is blocking.
|
||||||
|
|
||||||
For `init.strategy`, the "init" is testing the given resolvers for each group. The potentially fatal error, depending on the strategy, is if a group has no functional resolvers.
|
For `init.strategy`, the "init" is testing the given resolvers for each group. The potentially fatal error, depending on the strategy, is if a group has no functional resolvers.
|
||||||
|
|
||||||
|
|
||||||
### Upstream Groups
|
### Upstream Groups
|
||||||
|
|
||||||
To resolve a DNS query, blocky needs external public or private DNS resolvers. Blocky supports DNS resolvers with
|
To resolve a DNS query, blocky needs external public or private DNS resolvers. Blocky supports DNS resolvers with
|
||||||
|
@ -198,7 +197,6 @@ Currently available strategies:
|
||||||
- 9.8.7.6
|
- 9.8.7.6
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## Bootstrap DNS configuration
|
## Bootstrap DNS configuration
|
||||||
|
|
||||||
These DNS servers are used to resolve upstream DoH and DoT servers that are specified as host names, and list domains.
|
These DNS servers are used to resolve upstream DoH and DoT servers that are specified as host names, and list domains.
|
||||||
|
@ -291,10 +289,11 @@ This configuration will also resolve any subdomain of the defined domain, recurs
|
||||||
CNAME records are supported by utilizing the `zone` parameter. The zone file is a multiline string containing a [DNS Zone File](https://en.wikipedia.org/wiki/Zone_file#Example_file).
|
CNAME records are supported by utilizing the `zone` parameter. The zone file is a multiline string containing a [DNS Zone File](https://en.wikipedia.org/wiki/Zone_file#Example_file).
|
||||||
For records defined using the `zone` parameter, the `customTTL` parameter is unused. Instead, the TTL is defined in the zone directly.
|
For records defined using the `zone` parameter, the `customTTL` parameter is unused. Instead, the TTL is defined in the zone directly.
|
||||||
The following directives are supported in the zone file:
|
The following directives are supported in the zone file:
|
||||||
* `$ORIGIN` - sets the origin for relative domain names
|
|
||||||
* `$TTL` - sets the default TTL for records in the zone
|
- `$ORIGIN` - sets the origin for relative domain names
|
||||||
* `$INCLUDE` - includes another zone file relative to the blocky executable
|
- `$TTL` - sets the default TTL for records in the zone
|
||||||
* `$GENERATE` - generates a range of records
|
- `$INCLUDE` - includes another zone file relative to the blocky executable
|
||||||
|
- `$GENERATE` - generates a range of records
|
||||||
|
|
||||||
With the optional parameter `rewrite` you can replace domain part of the query with the defined part **before** the
|
With the optional parameter `rewrite` you can replace domain part of the query with the defined part **before** the
|
||||||
resolver lookup is performed.
|
resolver lookup is performed.
|
||||||
|
@ -455,6 +454,54 @@ The supported list formats are:
|
||||||
!!! warning
|
!!! warning
|
||||||
You must also define a client group mapping, otherwise the allow/denylist definitions will have no effect.
|
You must also define a client group mapping, otherwise the allow/denylist definitions will have no effect.
|
||||||
|
|
||||||
|
#### Schedules for blacklists
|
||||||
|
|
||||||
|
It's possible to create schedules for your blacklists. When the schedule is active, the blocking will occur, when inactive, the blacklist will be skipped.
|
||||||
|
|
||||||
|
!!! example
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
blocking:
|
||||||
|
blackLists:
|
||||||
|
ads:
|
||||||
|
- https://s3.amazonaws.com/lists.disconnect.me/simple_ad.txt
|
||||||
|
- https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts
|
||||||
|
- |
|
||||||
|
# inline definition using YAML literal block scalar style
|
||||||
|
# content is in plain domain list format
|
||||||
|
someadsdomain.com
|
||||||
|
anotheradsdomain.com
|
||||||
|
*.wildcard.example.com # blocks wildcard.example.com and all subdomains
|
||||||
|
- |
|
||||||
|
# inline definition with a regex
|
||||||
|
/^banners?[_.-]/
|
||||||
|
special:
|
||||||
|
- https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/fakenews/hosts
|
||||||
|
youtube:
|
||||||
|
- https://raw.githubusercontent.com/gieljnssns/Social-media-Blocklists/master/pihole-youtube.txt
|
||||||
|
whiteLists:
|
||||||
|
ads:
|
||||||
|
- whitelist.txt
|
||||||
|
- /path/to/file.txt
|
||||||
|
- |
|
||||||
|
# inline definition with YAML literal block scalar style
|
||||||
|
whitelistdomain.com
|
||||||
|
schedules:
|
||||||
|
youtube:
|
||||||
|
- days: ["Mon", "Tue", "Wed", "Thu"]
|
||||||
|
hoursRanges:
|
||||||
|
- "06:45-08:30"
|
||||||
|
- "19:00-23:30"
|
||||||
|
- days: ["Sun"]
|
||||||
|
hoursRanges:
|
||||||
|
- "20:00-23:30"
|
||||||
|
```
|
||||||
|
|
||||||
|
In this example, the **youtube** blacklist will be active at the specified days, between the specified hours ranges.
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
Whitelists is respected when a sheduled blacklist is active.
|
||||||
|
|
||||||
#### Wildcard support
|
#### Wildcard support
|
||||||
|
|
||||||
You can use wildcards to block a domain and all its subdomains.
|
You can use wildcards to block a domain and all its subdomains.
|
||||||
|
@ -735,7 +782,6 @@ Configuration parameters:
|
||||||
strategy: fast
|
strategy: fast
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## Deliver EDE codes as EDNS0 option
|
## Deliver EDE codes as EDNS0 option
|
||||||
|
|
||||||
DNS responses can be extended with EDE codes according to [RFC8914](https://datatracker.ietf.org/doc/rfc8914/).
|
DNS responses can be extended with EDE codes according to [RFC8914](https://datatracker.ietf.org/doc/rfc8914/).
|
||||||
|
|
|
@ -123,6 +123,94 @@ var _ = Describe("External lists and query blocking", func() {
|
||||||
It("should download external list on startup and block queries", func(ctx context.Context) {
|
It("should download external list on startup and block queries", func(ctx context.Context) {
|
||||||
msg := util.NewMsgWithQuestion("blockeddomain.com.", A)
|
msg := util.NewMsgWithQuestion("blockeddomain.com.", A)
|
||||||
|
|
||||||
|
Expect(doDNSRequest(ctx, blocky, msg)).
|
||||||
|
Should(
|
||||||
|
SatisfyAll(
|
||||||
|
BeDNSRecord("blockeddomain.com.", A, "0.0.0.0"),
|
||||||
|
HaveTTL(BeNumerically("==", 6*60*60)),
|
||||||
|
))
|
||||||
|
|
||||||
|
Expect(getContainerLogs(ctx, blocky)).Should(BeEmpty())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
Describe("Query blocking against external blacklists with schedule inactive", func() {
|
||||||
|
When("external blacklists are defined and available", func() {
|
||||||
|
BeforeEach(func(ctx context.Context) {
|
||||||
|
e2eNet = getRandomNetwork(ctx)
|
||||||
|
_, err = createDNSMokkaContainer(ctx, "moka", e2eNet, `A blockeddomain.com/NOERROR("A 1.2.3.4 123")`)
|
||||||
|
Expect(err).Should(Succeed())
|
||||||
|
|
||||||
|
_, err = createHTTPServerContainer(ctx, "httpserver", e2eNet, "list.txt", "blockeddomain.com")
|
||||||
|
Expect(err).Should(Succeed())
|
||||||
|
|
||||||
|
blocky, err = createBlockyContainer(ctx, e2eNet,
|
||||||
|
"log:",
|
||||||
|
" level: warn",
|
||||||
|
"upstreams:",
|
||||||
|
" groups:",
|
||||||
|
" default:",
|
||||||
|
" - moka",
|
||||||
|
"blocking:",
|
||||||
|
" blackLists:",
|
||||||
|
" ads:",
|
||||||
|
" - http://httpserver:8080/list.txt",
|
||||||
|
" schedules:",
|
||||||
|
" ads:",
|
||||||
|
" - days: [\"Sun\",]",
|
||||||
|
" hoursRanges: [\"00:00-00:01\"]",
|
||||||
|
" clientGroupsBlock:",
|
||||||
|
" default:",
|
||||||
|
" - ads",
|
||||||
|
)
|
||||||
|
|
||||||
|
Expect(err).Should(Succeed())
|
||||||
|
})
|
||||||
|
It("should download external list on startup and resolve queries", func(ctx context.Context) {
|
||||||
|
msg := util.NewMsgWithQuestion("blockeddomain.com.", A)
|
||||||
|
|
||||||
|
Expect(doDNSRequest(ctx, blocky, msg)).
|
||||||
|
Should(
|
||||||
|
SatisfyAll(
|
||||||
|
BeDNSRecord("blockeddomain.com.", A, "1.2.3.4"),
|
||||||
|
HaveTTL(BeNumerically("==", 123)),
|
||||||
|
))
|
||||||
|
|
||||||
|
Expect(getContainerLogs(ctx, blocky)).Should(BeEmpty())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
Describe("Query blocking against external blacklists with schedule active", func() {
|
||||||
|
When("external blacklists are defined and available", func() {
|
||||||
|
BeforeEach(func(ctx context.Context) {
|
||||||
|
_, err = createHTTPServerContainer(ctx, "httpserver", e2eNet, "list.txt", "blockeddomain.com")
|
||||||
|
Expect(err).Should(Succeed())
|
||||||
|
|
||||||
|
blocky, err = createBlockyContainer(ctx, e2eNet,
|
||||||
|
"log:",
|
||||||
|
" level: warn",
|
||||||
|
"upstreams:",
|
||||||
|
" groups:",
|
||||||
|
" default:",
|
||||||
|
" - moka",
|
||||||
|
"blocking:",
|
||||||
|
" blackLists:",
|
||||||
|
" ads:",
|
||||||
|
" - http://httpserver:8080/list.txt",
|
||||||
|
" schedules:",
|
||||||
|
" ads:",
|
||||||
|
" - days: [\"Sun\", \"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\"]",
|
||||||
|
" hoursRanges: [\"00:00-23:59\"]",
|
||||||
|
" clientGroupsBlock:",
|
||||||
|
" default:",
|
||||||
|
" - ads",
|
||||||
|
)
|
||||||
|
|
||||||
|
Expect(err).Should(Succeed())
|
||||||
|
})
|
||||||
|
It("should download external list on startup and block queries", func(ctx context.Context) {
|
||||||
|
msg := util.NewMsgWithQuestion("blockeddomain.com.", A)
|
||||||
|
|
||||||
Expect(doDNSRequest(ctx, blocky, msg)).
|
Expect(doDNSRequest(ctx, blocky, msg)).
|
||||||
Should(
|
Should(
|
||||||
SatisfyAll(
|
SatisfyAll(
|
||||||
|
|
|
@ -34,6 +34,12 @@ const (
|
||||||
|
|
||||||
// ApplicationStarted fires on start of the application. Parameter: version number, build time
|
// ApplicationStarted fires on start of the application. Parameter: version number, build time
|
||||||
ApplicationStarted = "application:started"
|
ApplicationStarted = "application:started"
|
||||||
|
|
||||||
|
// SchedulesActive fires, if a schedule is active. Parameter: schedule name, active counter (0/1)
|
||||||
|
SchedulesActive = "schedules:active"
|
||||||
|
|
||||||
|
// schedulesTotal fires, if a schedules is enabled. Parameter: total number of schedules
|
||||||
|
SchedulesTotal = "schedules:total"
|
||||||
)
|
)
|
||||||
|
|
||||||
//nolint:gochecknoglobals
|
//nolint:gochecknoglobals
|
||||||
|
|
8
go.mod
8
go.mod
|
@ -6,7 +6,7 @@ require (
|
||||||
github.com/abice/go-enum v0.6.0
|
github.com/abice/go-enum v0.6.0
|
||||||
github.com/alicebob/miniredis/v2 v2.32.1
|
github.com/alicebob/miniredis/v2 v2.32.1
|
||||||
github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef
|
github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef
|
||||||
github.com/avast/retry-go/v4 v4.5.1
|
github.com/avast/retry-go/v4 v4.6.0
|
||||||
github.com/creasty/defaults v1.7.0
|
github.com/creasty/defaults v1.7.0
|
||||||
github.com/go-chi/chi/v5 v5.0.12
|
github.com/go-chi/chi/v5 v5.0.12
|
||||||
github.com/go-chi/cors v1.2.1
|
github.com/go-chi/cors v1.2.1
|
||||||
|
@ -17,10 +17,10 @@ require (
|
||||||
github.com/hashicorp/golang-lru v1.0.2
|
github.com/hashicorp/golang-lru v1.0.2
|
||||||
github.com/mattn/go-colorable v0.1.13
|
github.com/mattn/go-colorable v0.1.13
|
||||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||||
github.com/miekg/dns v1.1.58
|
github.com/miekg/dns v1.1.59
|
||||||
github.com/mroth/weightedrand/v2 v2.1.0
|
github.com/mroth/weightedrand/v2 v2.1.0
|
||||||
github.com/onsi/ginkgo/v2 v2.17.1
|
github.com/onsi/ginkgo/v2 v2.17.1
|
||||||
github.com/onsi/gomega v1.32.0
|
github.com/onsi/gomega v1.33.0
|
||||||
github.com/prometheus/client_golang v1.19.0
|
github.com/prometheus/client_golang v1.19.0
|
||||||
github.com/sirupsen/logrus v1.9.3
|
github.com/sirupsen/logrus v1.9.3
|
||||||
github.com/spf13/cobra v1.8.0
|
github.com/spf13/cobra v1.8.0
|
||||||
|
@ -38,7 +38,7 @@ require (
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.2
|
github.com/DATA-DOG/go-sqlmock v1.5.2
|
||||||
github.com/ThinkChaos/parcour v0.0.0-20230710171753-fbf917c9eaef
|
github.com/ThinkChaos/parcour v0.0.0-20230710171753-fbf917c9eaef
|
||||||
github.com/deepmap/oapi-codegen v1.16.2
|
github.com/deepmap/oapi-codegen v1.16.2
|
||||||
github.com/docker/docker v26.0.1+incompatible
|
github.com/docker/docker v26.1.0+incompatible
|
||||||
github.com/docker/go-connections v0.5.0
|
github.com/docker/go-connections v0.5.0
|
||||||
github.com/dosgo/zigtool v0.0.0-20210923085854-9c6fc1d62198
|
github.com/dosgo/zigtool v0.0.0-20210923085854-9c6fc1d62198
|
||||||
github.com/oapi-codegen/runtime v1.1.1
|
github.com/oapi-codegen/runtime v1.1.1
|
||||||
|
|
16
go.sum
16
go.sum
|
@ -29,8 +29,8 @@ github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7D
|
||||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
|
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
|
||||||
github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef h1:2JGTg6JapxP9/R33ZaagQtAM4EkkSYnIAlOG5EI8gkM=
|
github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef h1:2JGTg6JapxP9/R33ZaagQtAM4EkkSYnIAlOG5EI8gkM=
|
||||||
github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef/go.mod h1:JS7hed4L1fj0hXcyEejnW57/7LCetXggd+vwrRnYeII=
|
github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef/go.mod h1:JS7hed4L1fj0hXcyEejnW57/7LCetXggd+vwrRnYeII=
|
||||||
github.com/avast/retry-go/v4 v4.5.1 h1:AxIx0HGi4VZ3I02jr78j5lZ3M6x1E0Ivxa6b0pUUh7o=
|
github.com/avast/retry-go/v4 v4.6.0 h1:K9xNA+KeB8HHc2aWFuLb25Offp+0iVRXEvFx8IinRJA=
|
||||||
github.com/avast/retry-go/v4 v4.5.1/go.mod h1:/sipNsvNB3RRuT5iNcb6h73nw3IBmXJ/H3XrCQYSOpc=
|
github.com/avast/retry-go/v4 v4.6.0/go.mod h1:gvWlPhBVsvBbLkVGDg/KwvBv0bEkCOLRRSHKIr2PyOE=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
|
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
|
||||||
|
@ -64,8 +64,8 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0=
|
github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0=
|
||||||
github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||||
github.com/docker/docker v26.0.1+incompatible h1:t39Hm6lpXuXtgkF0dm1t9a5HkbUfdGy6XbWexmGr+hA=
|
github.com/docker/docker v26.1.0+incompatible h1:W1G9MPNbskA6VZWL7b3ZljTh0pXI68FpINx0GKaOdaM=
|
||||||
github.com/docker/docker v26.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
github.com/docker/docker v26.1.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||||
|
@ -193,8 +193,8 @@ github.com/mattn/goveralls v0.0.12 h1:PEEeF0k1SsTjOBQ8FOmrOAoCu4ytuMaWCnWe94zxbC
|
||||||
github.com/mattn/goveralls v0.0.12/go.mod h1:44ImGEUfmqH8bBtaMrYKsM65LXfNLWmwaxFGjZwgMSQ=
|
github.com/mattn/goveralls v0.0.12/go.mod h1:44ImGEUfmqH8bBtaMrYKsM65LXfNLWmwaxFGjZwgMSQ=
|
||||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
|
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
|
||||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||||
github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4=
|
github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs=
|
||||||
github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY=
|
github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk=
|
||||||
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
|
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
|
||||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||||
|
@ -225,8 +225,8 @@ github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||||
github.com/onsi/ginkgo/v2 v2.17.1 h1:V++EzdbhI4ZV4ev0UTIj0PzhzOcReJFyJaLjtSF55M8=
|
github.com/onsi/ginkgo/v2 v2.17.1 h1:V++EzdbhI4ZV4ev0UTIj0PzhzOcReJFyJaLjtSF55M8=
|
||||||
github.com/onsi/ginkgo/v2 v2.17.1/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs=
|
github.com/onsi/ginkgo/v2 v2.17.1/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs=
|
||||||
github.com/onsi/gomega v1.32.0 h1:JRYU78fJ1LPxlckP6Txi/EYqJvjtMrDC04/MM5XRHPk=
|
github.com/onsi/gomega v1.33.0 h1:snPCflnZrpMsy94p4lXVEkHo12lmPnc3vY5XBbreexE=
|
||||||
github.com/onsi/gomega v1.32.0/go.mod h1:a4x4gW6Pz2yK1MAmvluYme5lvYTn61afQ2ETw/8n4Lg=
|
github.com/onsi/gomega v1.33.0/go.mod h1:+925n5YtiFsLzzafLUHzVMBpvvRAzrydIBiSIxjX3wY=
|
||||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||||
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||||
|
|
|
@ -15,6 +15,7 @@ import (
|
||||||
func RegisterEventListeners() {
|
func RegisterEventListeners() {
|
||||||
registerBlockingEventListeners()
|
registerBlockingEventListeners()
|
||||||
registerCachingEventListeners()
|
registerCachingEventListeners()
|
||||||
|
registerSchedulesEventListeners()
|
||||||
registerApplicationEventListeners()
|
registerApplicationEventListeners()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,6 +39,22 @@ func versionNumberGauge() *prometheus.GaugeVec {
|
||||||
return denylistCnt
|
return denylistCnt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func registerSchedulesEventListeners() {
|
||||||
|
schedulesActiveGaugeVec := schedulesActiveGaugeVec()
|
||||||
|
schedulesTotalGauge := schedulesTotalGauge()
|
||||||
|
|
||||||
|
RegisterMetric(schedulesActiveGaugeVec)
|
||||||
|
RegisterMetric(schedulesTotalGauge)
|
||||||
|
|
||||||
|
subscribe(evt.SchedulesActive, func(group string, activeCnt int) {
|
||||||
|
schedulesActiveGaugeVec.With(prometheus.Labels{"group": group}).Set(float64(activeCnt))
|
||||||
|
})
|
||||||
|
|
||||||
|
subscribe(evt.SchedulesTotal, func(cnt int) {
|
||||||
|
schedulesTotalGauge.Set(float64(cnt))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func registerBlockingEventListeners() {
|
func registerBlockingEventListeners() {
|
||||||
enabledGauge := enabledGauge()
|
enabledGauge := enabledGauge()
|
||||||
|
|
||||||
|
@ -83,6 +100,24 @@ func enabledGauge() prometheus.Gauge {
|
||||||
return enabledGauge
|
return enabledGauge
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func schedulesActiveGaugeVec() *prometheus.GaugeVec {
|
||||||
|
schedulesActiveGaugeVec := prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||||
|
Name: "blocky_schedules_active",
|
||||||
|
Help: "Active schedules",
|
||||||
|
}, []string{"group"})
|
||||||
|
|
||||||
|
return schedulesActiveGaugeVec
|
||||||
|
}
|
||||||
|
|
||||||
|
func schedulesTotalGauge() prometheus.Gauge {
|
||||||
|
schedulesTotal := prometheus.NewGauge(prometheus.GaugeOpts{
|
||||||
|
Name: "blocky_schedules_total",
|
||||||
|
Help: "Total number of schedules",
|
||||||
|
})
|
||||||
|
|
||||||
|
return schedulesTotal
|
||||||
|
}
|
||||||
|
|
||||||
func denylistGauge() *prometheus.GaugeVec {
|
func denylistGauge() *prometheus.GaugeVec {
|
||||||
denylistCnt := prometheus.NewGaugeVec(
|
denylistCnt := prometheus.NewGaugeVec(
|
||||||
prometheus.GaugeOpts{
|
prometheus.GaugeOpts{
|
||||||
|
|
|
@ -131,6 +131,13 @@ func NewBlockingResolver(ctx context.Context,
|
||||||
cfg.Loading, cfg.Allowlists, downloader)
|
cfg.Loading, cfg.Allowlists, downloader)
|
||||||
allowlistOnlyGroups := determineAllowlistOnlyGroups(&cfg)
|
allowlistOnlyGroups := determineAllowlistOnlyGroups(&cfg)
|
||||||
|
|
||||||
|
schedulesCount := len(cfg.Schedules)
|
||||||
|
if schedulesCount > 0 {
|
||||||
|
evt.Bus().Publish(evt.SchedulesTotal, schedulesCount)
|
||||||
|
|
||||||
|
go cfg.Schedules.Refresh(ctx, nil)
|
||||||
|
}
|
||||||
|
|
||||||
err = multierror.Append(err, blErr, wlErr).ErrorOrNil()
|
err = multierror.Append(err, blErr, wlErr).ErrorOrNil()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -386,6 +393,23 @@ func (r *BlockingResolver) handleDenylist(ctx context.Context, groupsToCheck []s
|
||||||
}
|
}
|
||||||
|
|
||||||
if groups := r.matches(groupsToCheck, r.denylistMatcher, domain); len(groups) > 0 {
|
if groups := r.matches(groupsToCheck, r.denylistMatcher, domain); len(groups) > 0 {
|
||||||
|
for group := range r.cfg.Schedules {
|
||||||
|
if slices.Contains(groups, group) {
|
||||||
|
if r.cfg.Schedules.IsActive(group, time.Now) {
|
||||||
|
resp, err := r.handleBlocked(logger, request, question, fmt.Sprintf("BLOCKED (SCHEDULE %s active)",
|
||||||
|
strings.Join(groups, ",")))
|
||||||
|
|
||||||
|
return true, resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.WithField("groups", groups).Infof("domain is in an inactive schedule")
|
||||||
|
|
||||||
|
resp, err := r.next.Resolve(ctx, request)
|
||||||
|
|
||||||
|
return true, resp, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
resp, err := r.handleBlocked(logger, request, question, fmt.Sprintf("BLOCKED (%s)", strings.Join(groups, ",")))
|
resp, err := r.handleBlocked(logger, request, question, fmt.Sprintf("BLOCKED (%s)", strings.Join(groups, ",")))
|
||||||
|
|
||||||
return true, resp, err
|
return true, resp, err
|
||||||
|
|
18
trie/trie.go
18
trie/trie.go
|
@ -1,5 +1,10 @@
|
||||||
package trie
|
package trie
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/0xERR0R/blocky/log"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
// Trie stores a set of strings and can quickly check
|
// Trie stores a set of strings and can quickly check
|
||||||
// if it contains an element, or one of its parents.
|
// if it contains an element, or one of its parents.
|
||||||
//
|
//
|
||||||
|
@ -108,8 +113,12 @@ func (n *parent) insert(key string, split SplitFunc) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *parent) hasParentOf(key string, split SplitFunc) bool {
|
func (n *parent) hasParentOf(key string, split SplitFunc) bool {
|
||||||
|
searchString := key
|
||||||
|
rule := ""
|
||||||
|
|
||||||
for {
|
for {
|
||||||
label, rest := split(key)
|
label, rest := split(key)
|
||||||
|
rule = strings.Join([]string{label, rule}, ".")
|
||||||
|
|
||||||
child, ok := n.children[label]
|
child, ok := n.children[label]
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@ -132,7 +141,14 @@ func (n *parent) hasParentOf(key string, split SplitFunc) bool {
|
||||||
|
|
||||||
case terminal:
|
case terminal:
|
||||||
// Continue down the trie
|
// Continue down the trie
|
||||||
return child.hasParentOf(rest, split)
|
matched := child.hasParentOf(rest, split)
|
||||||
|
if matched {
|
||||||
|
rule = strings.Join([]string{child.String(), rule}, ".")
|
||||||
|
rule = strings.Trim(rule, ".")
|
||||||
|
log.PrefixedLog("trie").Debugf("wildcard block rule '%s' matched with '%s'", rule, searchString)
|
||||||
|
}
|
||||||
|
|
||||||
|
return matched
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue