config support

This commit is contained in:
Vladimir Avtsenov 2025-02-12 00:34:14 +03:00
parent 60e1f4c540
commit 4a0c0938e6
9 changed files with 174 additions and 33 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 ( require (
github.com/IGLOU-EU/go-wildcard/v2 v2.0.2 github.com/IGLOU-EU/go-wildcard/v2 v2.0.2
github.com/coreos/go-iptables v0.7.0 github.com/coreos/go-iptables v0.7.0
github.com/google/uuid v1.6.0
github.com/miekg/dns v1.1.63 github.com/miekg/dns v1.1.63
github.com/rs/zerolog v1.33.0 github.com/rs/zerolog v1.33.0
github.com/vishvananda/netlink v1.3.0 github.com/vishvananda/netlink v1.3.0
gopkg.in/yaml.v3 v3.0.1
) )
require ( require (

View File

@ -3,7 +3,6 @@ package main
import ( import (
"context" "context"
"encoding/binary" "encoding/binary"
"encoding/hex"
"errors" "errors"
"fmt" "fmt"
"math/rand" "math/rand"
@ -28,6 +27,7 @@ import (
var ( var (
ErrAlreadyRunning = errors.New("already running") ErrAlreadyRunning = errors.New("already running")
ErrGroupIDConflict = errors.New("group id conflict") ErrGroupIDConflict = errors.New("group id conflict")
ErrRuleIDConflict = errors.New("rule id conflict")
) )
func randomId() [4]byte { func randomId() [4]byte {
@ -39,7 +39,7 @@ func randomId() [4]byte {
type Config struct { type Config struct {
AdditionalTTL uint32 AdditionalTTL uint32
ChainPrefix string ChainPrefix string
IpSetPrefix string IPSetPrefix string
LinkName string LinkName string
TargetDNSServerAddress string TargetDNSServerAddress string
TargetDNSServerPort uint16 TargetDNSServerPort uint16
@ -53,7 +53,7 @@ type App struct {
NetfilterHelper4 *netfilterHelper.NetfilterHelper NetfilterHelper4 *netfilterHelper.NetfilterHelper
NetfilterHelper6 *netfilterHelper.NetfilterHelper NetfilterHelper6 *netfilterHelper.NetfilterHelper
Records *records.Records Records *records.Records
Groups map[[4]byte]*group.Group Groups []*group.Group
Link netlink.Link Link netlink.Link
@ -80,7 +80,7 @@ func (a *App) handleLink(event netlink.LinkUpdate) {
err := group.LinkUpdateHook(event) err := group.LinkUpdateHook(event)
if err != nil { 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")
} }
} }
} }
@ -287,15 +287,27 @@ func (a *App) Start(ctx context.Context) (err error) {
} }
func (a *App) AddGroup(groupModel *models.Group) 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 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 { if err != nil {
return fmt.Errorf("failed to create group: %w", err) 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) return grp.Sync(a.Records)
} }
@ -423,12 +435,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 var err error
app := &App{} 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 = dnsMitmProxy.New()
app.DNSMITM.TargetDNSServerAddress = app.Config.TargetDNSServerAddress app.DNSMITM.TargetDNSServerAddress = app.Config.TargetDNSServerAddress
@ -468,7 +520,6 @@ func New(config Config) (*App, error) {
} }
app.Records = records.New() app.Records = records.New()
app.Groups = make(map[[4]byte]*group.Group)
link, err := netlink.LinkByName(app.Config.LinkName) link, err := netlink.LinkByName(app.Config.LinkName)
if err != nil { if err != nil {
@ -496,7 +547,12 @@ func New(config Config) (*App, error) {
return nil, fmt.Errorf("failed to clear iptables: %w", err) 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 return app, nil
} }

25
main.go
View File

@ -6,22 +6,29 @@ import (
"os/signal" "os/signal"
"syscall" "syscall"
"kvas2-go/models"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"gopkg.in/yaml.v3"
) )
func main() { func main() {
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
app, err := New(Config{ cfgFile, err := os.ReadFile("config.yaml")
AdditionalTTL: 216000, // 1 hour if err != nil {
ChainPrefix: "KVAS2_", log.Fatal().Err(err).Msg("failed to read config.yaml")
IpSetPrefix: "kvas2_", }
LinkName: "br0",
TargetDNSServerAddress: "127.0.0.1", cfg := models.ConfigFile{}
TargetDNSServerPort: 53, err = yaml.Unmarshal(cfgFile, &cfg)
ListenDNSPort: 3553, if err != nil {
}) log.Fatal().Err(err).Msg("failed to parse config.yaml")
}
app, err := New(cfg)
if err != nil { if err != nil {
log.Fatal().Err(err).Msg("failed to initialize application") 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 package models
type Group struct { type Group struct {
ID [4]byte ID ID `yaml:"id"`
Name string Name string `yaml:"name"`
Interface string Interface string `yaml:"interface"`
Rules []*Rule FixProtect bool `yaml:"fixProtect"`
FixProtect bool 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 ( import (
"regexp" "regexp"
"strings"
"github.com/IGLOU-EU/go-wildcard/v2" "github.com/IGLOU-EU/go-wildcard/v2"
) )
type Rule struct { type Rule struct {
ID [4]byte ID ID `yaml:"id"`
Name string Name string `yaml:"name"`
Type string Type string `yaml:"type"`
Rule string Rule string `yaml:"rule"`
Enable bool Enable bool `yaml:"enable"`
} }
func (d *Rule) IsEnabled() bool { func (d *Rule) IsEnabled() bool {
@ -25,8 +26,13 @@ func (d *Rule) IsMatch(domainName string) bool {
case "regex": case "regex":
ok, _ := regexp.MatchString(d.Rule, domainName) ok, _ := regexp.MatchString(d.Rule, domainName)
return ok return ok
case "plaintext": case "domain":
return domainName == d.Rule return domainName == d.Rule
case "namespace":
if domainName == d.Rule {
return true
}
return strings.HasSuffix(domainName, "."+d.Rule)
} }
return false return false
} }