Compare commits

...

8 Commits

Author SHA1 Message Date
04fd45fe97 fix route adding 2025-02-12 04:27:27 +03:00
849a584371 fix events 2025-02-12 04:11:28 +03:00
1441afb6e4 restore deleted rules 2025-02-12 04:07:45 +03:00
7a356867c3 catch "Link not found" error 2025-02-12 04:07:26 +03:00
23580da495 add FixProtect to NetfilterDHook 2025-02-12 04:07:09 +03:00
345b5ff80b add reminder 2025-02-12 01:14:28 +03:00
672464a29e fix cname 2025-02-12 01:14:03 +03:00
4a0c0938e6 config support 2025-02-12 00:34:42 +03:00
12 changed files with 218 additions and 59 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
kvas2-go
config.yaml

34
config.yaml.example Normal file
View File

@ -0,0 +1,34 @@
appConfig:
additionalTTL: 216000
chainPrefix: KVAS2_
ipsetPrefix: kvas2_
linkName: br0
targetDNSServerAddress: 127.0.0.1
targetDNSServerPort: 53
listenDNSPort: 3553
groups:
- id: d663876a
name: Example
interface: nwg0
fixProtect: false
rules:
- id: 6f34ee91
name: Wildcard Example
type: wildcard
rule: '*wildcard.example.com'
enable: true
- id: 00ae5f7c
name: RegEx Example
type: regex
rule: '^.*.regex.example.com$'
enable: true
- id: 6120dc8a
name: Domain Example
type: domain
rule: 'domain.example.com'
enable: true
- id: b9751782
name: Namespace Example
type: namespace
rule: 'namespace.example.com'
enable: true

2
go.mod
View File

@ -5,10 +5,10 @@ go 1.21
require (
github.com/IGLOU-EU/go-wildcard/v2 v2.0.2
github.com/coreos/go-iptables v0.7.0
github.com/google/uuid v1.6.0
github.com/miekg/dns v1.1.63
github.com/rs/zerolog v1.33.0
github.com/vishvananda/netlink v1.3.0
gopkg.in/yaml.v3 v3.0.1
)
require (

View File

@ -159,6 +159,13 @@ func (g *Group) Sync(records *records.Records) error {
}
func (g *Group) NetfilterDHook(table string) error {
if g.enabled && g.FixProtect && table == "filter" {
err := g.iptables.AppendUnique("filter", "_NDM_SL_FORWARD", "-o", g.Interface, "-m", "state", "--state", "NEW", "-j", "_NDM_SL_PROTECT")
if err != nil {
return fmt.Errorf("failed to fix protect: %w", err)
}
}
return g.ipsetToLink.NetfilterDHook(table)
}

View File

@ -3,7 +3,6 @@ package main
import (
"context"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"math/rand"
@ -28,6 +27,7 @@ import (
var (
ErrAlreadyRunning = errors.New("already running")
ErrGroupIDConflict = errors.New("group id conflict")
ErrRuleIDConflict = errors.New("rule id conflict")
)
func randomId() [4]byte {
@ -39,7 +39,7 @@ func randomId() [4]byte {
type Config struct {
AdditionalTTL uint32
ChainPrefix string
IpSetPrefix string
IPSetPrefix string
LinkName string
TargetDNSServerAddress string
TargetDNSServerPort uint16
@ -53,7 +53,7 @@ type App struct {
NetfilterHelper4 *netfilterHelper.NetfilterHelper
NetfilterHelper6 *netfilterHelper.NetfilterHelper
Records *records.Records
Groups map[[4]byte]*group.Group
Groups []*group.Group
Link netlink.Link
@ -63,15 +63,14 @@ type App struct {
}
func (a *App) handleLink(event netlink.LinkUpdate) {
switch event.Change {
case 0x00000001:
log.Debug().
log.Trace().
Str("interface", event.Link.Attrs().Name).
Str("operstatestr", event.Attrs().OperState.String()).
Int("operstate", int(event.Attrs().OperState)).
Msg("interface change")
switch event.Attrs().OperState {
case netlink.OperUp:
Int("change", int(event.Change)).
Msg("interface event")
switch event.Change {
case 0x00000001:
ifaceName := event.Link.Attrs().Name
for _, group := range a.Groups {
if group.Interface != ifaceName {
@ -80,8 +79,7 @@ func (a *App) handleLink(event netlink.LinkUpdate) {
err := group.LinkUpdateHook(event)
if err != nil {
log.Error().Str("group", hex.EncodeToString(group.ID[:])).Err(err).Msg("error while handling interface up")
}
log.Error().Str("group", group.ID.String()).Err(err).Msg("error while handling interface up")
}
}
case 0xFFFFFFFF:
@ -242,9 +240,7 @@ func (a *App) start(ctx context.Context) (err error) {
if err != nil {
return fmt.Errorf("failed to subscribe to link updates: %w", err)
}
defer func() {
close(linkUpdateDone)
}()
defer close(linkUpdateDone)
/*
Global loop
@ -287,15 +283,27 @@ func (a *App) Start(ctx context.Context) (err error) {
}
func (a *App) AddGroup(groupModel *models.Group) error {
if _, exists := a.Groups[groupModel.ID]; exists {
for _, group := range a.Groups {
if groupModel.ID == group.ID {
return ErrGroupIDConflict
}
}
dup := make(map[[4]byte]struct{})
for _, rule := range groupModel.Rules {
if _, exists := dup[rule.ID]; exists {
return ErrRuleIDConflict
}
dup[rule.ID] = struct{}{}
}
grp, err := group.NewGroup(groupModel, a.NetfilterHelper4, a.Config.ChainPrefix, a.Config.IpSetPrefix)
grp, err := group.NewGroup(groupModel, a.NetfilterHelper4, a.Config.ChainPrefix, a.Config.IPSetPrefix)
if err != nil {
return fmt.Errorf("failed to create group: %w", err)
}
a.Groups[grp.ID] = grp
a.Groups = append(a.Groups, grp)
log.Trace().Str("id", grp.ID.String()).Str("name", grp.Name).Msg("added group")
return grp.Sync(a.Records)
}
@ -370,7 +378,7 @@ func (a *App) processCNameRecord(cNameRecord dns.CNAME) {
ttlDuration := cNameRecord.Hdr.Ttl + a.Config.AdditionalTTL
a.Records.AddCNameRecord(cNameRecord.Hdr.Name[:len(cNameRecord.Hdr.Name)-1], cNameRecord.Target, ttlDuration)
a.Records.AddCNameRecord(cNameRecord.Hdr.Name[:len(cNameRecord.Hdr.Name)-1], cNameRecord.Target[:len(cNameRecord.Target)-1], ttlDuration)
// TODO: Optimization
now := time.Now()
@ -423,12 +431,52 @@ func (a *App) handleMessage(msg dns.Msg) {
}
}
func New(config Config) (*App, error) {
func (a *App) ImportConfig(cfg models.ConfigFile) error {
a.Config = Config{
AdditionalTTL: cfg.AppConfig.AdditionalTTL,
ChainPrefix: cfg.AppConfig.ChainPrefix,
IPSetPrefix: cfg.AppConfig.IPSetPrefix,
LinkName: cfg.AppConfig.LinkName,
TargetDNSServerAddress: cfg.AppConfig.TargetDNSServerAddress,
TargetDNSServerPort: cfg.AppConfig.TargetDNSServerPort,
ListenDNSPort: cfg.AppConfig.ListenDNSPort,
}
return nil
}
func (a *App) ExportConfig() models.ConfigFile {
groups := make([]models.Group, len(a.Groups))
for idx, group := range a.Groups {
groups[idx] = *group.Group
}
return models.ConfigFile{
AppConfig: models.AppConfig{
AdditionalTTL: a.Config.AdditionalTTL,
ChainPrefix: a.Config.ChainPrefix,
IPSetPrefix: a.Config.IPSetPrefix,
LinkName: a.Config.LinkName,
TargetDNSServerAddress: a.Config.TargetDNSServerAddress,
TargetDNSServerPort: a.Config.TargetDNSServerPort,
ListenDNSPort: a.Config.ListenDNSPort,
},
Groups: groups,
}
}
func New(config models.ConfigFile) (*App, error) {
var err error
app := &App{}
app.Config = config
app.Config = Config{
AdditionalTTL: config.AppConfig.AdditionalTTL,
ChainPrefix: config.AppConfig.ChainPrefix,
IPSetPrefix: config.AppConfig.IPSetPrefix,
LinkName: config.AppConfig.LinkName,
TargetDNSServerAddress: config.AppConfig.TargetDNSServerAddress,
TargetDNSServerPort: config.AppConfig.TargetDNSServerPort,
ListenDNSPort: config.AppConfig.ListenDNSPort,
}
app.DNSMITM = dnsMitmProxy.New()
app.DNSMITM.TargetDNSServerAddress = app.Config.TargetDNSServerAddress
@ -468,7 +516,6 @@ func New(config Config) (*App, error) {
}
app.Records = records.New()
app.Groups = make(map[[4]byte]*group.Group)
link, err := netlink.LinkByName(app.Config.LinkName)
if err != nil {
@ -496,7 +543,12 @@ func New(config Config) (*App, error) {
return nil, fmt.Errorf("failed to clear iptables: %w", err)
}
app.Groups = make(map[[4]byte]*group.Group)
for _, group := range config.Groups {
err = app.AddGroup(&group)
if err != nil {
return nil, err
}
}
return app, nil
}

25
main.go
View File

@ -6,22 +6,29 @@ import (
"os/signal"
"syscall"
"kvas2-go/models"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"gopkg.in/yaml.v3"
)
func main() {
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
app, err := New(Config{
AdditionalTTL: 216000, // 1 hour
ChainPrefix: "KVAS2_",
IpSetPrefix: "kvas2_",
LinkName: "br0",
TargetDNSServerAddress: "127.0.0.1",
TargetDNSServerPort: 53,
ListenDNSPort: 3553,
})
cfgFile, err := os.ReadFile("config.yaml")
if err != nil {
log.Fatal().Err(err).Msg("failed to read config.yaml")
}
cfg := models.ConfigFile{}
err = yaml.Unmarshal(cfgFile, &cfg)
if err != nil {
log.Fatal().Err(err).Msg("failed to parse config.yaml")
}
app, err := New(cfg)
if err != nil {
log.Fatal().Err(err).Msg("failed to initialize application")
}

16
models/config.go Normal file
View File

@ -0,0 +1,16 @@
package models
type ConfigFile struct {
AppConfig AppConfig `yaml:"appConfig"`
Groups []Group `yaml:"groups"`
}
type AppConfig struct {
AdditionalTTL uint32 `yaml:"additionalTTL"`
ChainPrefix string `yaml:"chainPrefix"`
IPSetPrefix string `yaml:"ipsetPrefix"`
LinkName string `yaml:"linkName"`
TargetDNSServerAddress string `yaml:"targetDNSServerAddress"`
TargetDNSServerPort uint16 `yaml:"targetDNSServerPort"`
ListenDNSPort uint16 `yaml:"listenDNSPort"`
}

View File

@ -1,9 +1,9 @@
package models
type Group struct {
ID [4]byte
Name string
Interface string
Rules []*Rule
FixProtect bool
ID ID `yaml:"id"`
Name string `yaml:"name"`
Interface string `yaml:"interface"`
FixProtect bool `yaml:"fixProtect"`
Rules []*Rule `yaml:"rules"`
}

20
models/id.go Normal file
View File

@ -0,0 +1,20 @@
package models
import (
"encoding/hex"
)
type ID [4]byte
func (id *ID) String() string {
return hex.EncodeToString(id[:])
}
func (id *ID) MarshalText() ([]byte, error) {
return []byte(id.String()), nil
}
func (id *ID) UnmarshalText(data []byte) error {
_, err := hex.Decode(id[:], data)
return err
}

View File

@ -2,16 +2,17 @@ package models
import (
"regexp"
"strings"
"github.com/IGLOU-EU/go-wildcard/v2"
)
type Rule struct {
ID [4]byte
Name string
Type string
Rule string
Enable bool
ID ID `yaml:"id"`
Name string `yaml:"name"`
Type string `yaml:"type"`
Rule string `yaml:"rule"`
Enable bool `yaml:"enable"`
}
func (d *Rule) IsEnabled() bool {
@ -25,8 +26,13 @@ func (d *Rule) IsMatch(domainName string) bool {
case "regex":
ok, _ := regexp.MatchString(d.Rule, domainName)
return ok
case "plaintext":
case "domain":
return domainName == d.Rule
case "namespace":
if domainName == d.Rule {
return true
}
return strings.HasSuffix(domainName, "."+d.Rule)
}
return false
}

View File

@ -131,6 +131,11 @@ func (r *IPSetToLink) insertIPRoute() error {
// Find interface
iface, err := netlink.LinkByName(r.IfaceName)
if err != nil {
// TODO: Нормально отлавливать ошибку
if err.Error() == "Link not found" {
// TODO: Логи
return nil
}
return fmt.Errorf("error while getting interface: %w", err)
}
@ -141,9 +146,12 @@ func (r *IPSetToLink) insertIPRoute() error {
Dst: &net.IPNet{IP: []byte{0, 0, 0, 0}, Mask: []byte{0, 0, 0, 0}},
}
// Delete rule if exists
_ = netlink.RouteDel(route)
err = netlink.RouteAdd(route)
if err != nil {
// TODO: Нормально отлавливать ошибку
if err.Error() == "file exists" {
return nil
}
return fmt.Errorf("error while mapping iface with table: %w", err)
}
r.ipRoute = route
@ -273,7 +281,7 @@ func (r *IPSetToLink) NetfilterDHook(table string) error {
}
func (r *IPSetToLink) LinkUpdateHook(event netlink.LinkUpdate) error {
if !r.enabled || event.Change != 1 || event.Link.Attrs().Name != r.IfaceName || event.Attrs().OperState != netlink.OperUp {
if !r.enabled || event.Change != 1 || event.Link.Attrs().Name != r.IfaceName {
return nil
}
return r.insertIPRoute()

View File

@ -27,14 +27,21 @@ func (nh *NetfilterHelper) CleanIPTables(chainPrefix string) error {
}
for _, rule := range rules {
if strings.Contains(rule, jumpToChainPrefix) {
err = nh.IPTables.Delete(table, chain, rule)
if !strings.Contains(rule, jumpToChainPrefix) {
continue
}
ruleSlice := strings.Split(rule, " ")
if len(ruleSlice) < 2 || ruleSlice[0] != "-A" || ruleSlice[1] != chain {
continue
}
err = nh.IPTables.Delete(table, chain, ruleSlice[2:]...)
if err != nil {
return fmt.Errorf("rule deletion error: %w", err)
}
}
}
}
for _, chain := range chainListToDelete {
err = nh.IPTables.ClearAndDeleteChain(table, chain)