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 ( 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

@ -159,6 +159,13 @@ func (g *Group) Sync(records *records.Records) error {
} }
func (g *Group) NetfilterDHook(table string) 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) return g.ipsetToLink.NetfilterDHook(table)
} }

114
kvas2.go
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
@ -63,25 +63,23 @@ type App struct {
} }
func (a *App) handleLink(event netlink.LinkUpdate) { func (a *App) handleLink(event netlink.LinkUpdate) {
log.Trace().
Str("interface", event.Link.Attrs().Name).
Str("operstatestr", event.Attrs().OperState.String()).
Int("operstate", int(event.Attrs().OperState)).
Int("change", int(event.Change)).
Msg("interface event")
switch event.Change { switch event.Change {
case 0x00000001: case 0x00000001:
log.Debug(). ifaceName := event.Link.Attrs().Name
Str("interface", event.Link.Attrs().Name). for _, group := range a.Groups {
Str("operstatestr", event.Attrs().OperState.String()). if group.Interface != ifaceName {
Int("operstate", int(event.Attrs().OperState)). continue
Msg("interface change") }
switch event.Attrs().OperState {
case netlink.OperUp:
ifaceName := event.Link.Attrs().Name
for _, group := range a.Groups {
if group.Interface != ifaceName {
continue
}
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")
}
} }
} }
case 0xFFFFFFFF: case 0xFFFFFFFF:
@ -242,9 +240,7 @@ func (a *App) start(ctx context.Context) (err error) {
if err != nil { if err != nil {
return fmt.Errorf("failed to subscribe to link updates: %w", err) return fmt.Errorf("failed to subscribe to link updates: %w", err)
} }
defer func() { defer close(linkUpdateDone)
close(linkUpdateDone)
}()
/* /*
Global loop Global loop
@ -287,15 +283,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 {
return ErrGroupIDConflict 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 { 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)
} }
@ -370,7 +378,7 @@ func (a *App) processCNameRecord(cNameRecord dns.CNAME) {
ttlDuration := cNameRecord.Hdr.Ttl + a.Config.AdditionalTTL 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 // TODO: Optimization
now := time.Now() 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 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 +516,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 +543,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
} }

View File

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

View File

@ -27,11 +27,18 @@ func (nh *NetfilterHelper) CleanIPTables(chainPrefix string) error {
} }
for _, rule := range rules { for _, rule := range rules {
if strings.Contains(rule, jumpToChainPrefix) { if !strings.Contains(rule, jumpToChainPrefix) {
err = nh.IPTables.Delete(table, chain, rule) continue
if err != nil { }
return fmt.Errorf("rule deletion error: %w", err)
} 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)
} }
} }
} }