diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..887fed8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +kvas2-go +config.yaml \ No newline at end of file diff --git a/config.yaml.example b/config.yaml.example new file mode 100644 index 0000000..b2349ea --- /dev/null +++ b/config.yaml.example @@ -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 diff --git a/go.mod b/go.mod index 38bbac0..5d5920f 100644 --- a/go.mod +++ b/go.mod @@ -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 ( diff --git a/kvas2.go b/kvas2.go index 1a5a995..286ebbb 100644 --- a/kvas2.go +++ b/kvas2.go @@ -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 @@ -80,7 +80,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") } } } @@ -287,15 +287,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 { - return ErrGroupIDConflict + 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) } @@ -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 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 +520,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 +547,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 } diff --git a/main.go b/main.go index 6122999..118653f 100644 --- a/main.go +++ b/main.go @@ -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") } diff --git a/models/config.go b/models/config.go new file mode 100644 index 0000000..3295bcd --- /dev/null +++ b/models/config.go @@ -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"` +} diff --git a/models/group.go b/models/group.go index 6c20c83..7569fa6 100644 --- a/models/group.go +++ b/models/group.go @@ -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"` } diff --git a/models/id.go b/models/id.go new file mode 100644 index 0000000..7bba100 --- /dev/null +++ b/models/id.go @@ -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 +} diff --git a/models/rule.go b/models/rule.go index c24b8e8..8d3cbec 100644 --- a/models/rule.go +++ b/models/rule.go @@ -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 }