diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3b95071..8bec7b8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,7 +7,7 @@ on: jobs: build: - run-name: Build ${{ matrix.arch }} + name: Build ${{ matrix.arch }} runs-on: ubuntu-latest strategy: matrix: @@ -46,33 +46,12 @@ jobs: sudo apt-get update sudo apt-get install -y fakeroot - - name: Build binary + - name: Build and Package run: | - go mod tidy - GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} GOMIPS=${{ matrix.gomips }} GOARM=${{ matrix.goarm }} go build -o kvas2-${{ matrix.arch }} -v -a -trimpath -ldflags="-w -s" . - - - name: Package as OPKG - run: | - mkdir -p build/${{ matrix.arch }}/control - echo 'Package: kvas2' > build/${{ matrix.arch }}/control/control - echo 'Version: 0.0.1' >> build/${{ matrix.arch }}/control/control - echo 'Architecture: ${{ matrix.arch }}' >> build/${{ matrix.arch }}/control/control - echo 'Maintainer: Vladimir Avtsenov ' >> build/${{ matrix.arch }}/control/control - echo 'Description: kvas2' >> build/${{ matrix.arch }}/control/control - echo 'Section: base' >> build/${{ matrix.arch }}/control/control - echo 'Priority: optional' >> build/${{ matrix.arch }}/control/control - echo 'Depends: libc, iptables, socat' >> build/${{ matrix.arch }}/control/control - mkdir -p build/${{ matrix.arch }}/data/opt/usr/bin - cp kvas2-${{ matrix.arch }} build/${{ matrix.arch }}/data/opt/usr/bin/kvas2 - cp -r opt build/${{ matrix.arch }}/data/ - chmod +x build/${{ matrix.arch }}/data/opt/usr/bin/kvas2 - fakeroot sh -c "tar -C build/${{ matrix.arch }}/control -cvf build/${{ matrix.arch }}/control.tar ." - fakeroot sh -c "tar -C build/${{ matrix.arch }}/data -cvf build/${{ matrix.arch }}/data.tar ." - echo '2.0' > build/${{ matrix.arch }}/debian-binary - ar r build/kvas2_${{ matrix.arch }}.ipk build/${{ matrix.arch }}/debian-binary build/${{ matrix.arch }}/control.tar build/${{ matrix.arch }}/data.tar + ARCH=${{ matrix.arch }} GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} GOMIPS=${{ matrix.gomips }} GOARM=${{ matrix.goarm }} make - name: Upload artifact uses: actions/upload-artifact@v3 with: name: kvas2_${{ matrix.arch }}.ipk - path: build/kvas2_${{ matrix.arch }}.ipk + path: .build/kvas2_${{ matrix.arch }}.ipk diff --git a/.gitignore b/.gitignore index 887fed8..6ec76ed 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1 @@ -kvas2-go -config.yaml \ No newline at end of file +/.build diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d688cb5 --- /dev/null +++ b/Makefile @@ -0,0 +1,40 @@ +APP_NAME = kvas2 +APP_DESCRIPTION = DNS-based routing application +APP_MAINTAINER = Vladimir Avtsenov + +TAG = $(shell git describe --tags --abbrev=0) +COMMIT = $(shell git rev-parse --short HEAD) +COMMITS_SINCE_TAG = $(shell git rev-list ${TAG}..HEAD --count || echo "0") +VERSION ?= $(TAG) + +ARCH ?= mipsel +GOOS ?= linux +GOARCH ?= mipsle +GOMIPS ?= softfloat +GOARM ?= + +PKG_DIR = ./.build/$(ARCH) +BIN_DIR = $(PKG_DIR)/data/opt/usr/bin +PARAMS = -v -a -trimpath -ldflags="-X 'kvas2/constant.Version=$(VERSION)' -X 'kvas2/constant.Commit=$(COMMIT)' -w -s" + +all: build_daemon package + +build_daemon: + GOOS=$(GOOS) GOARCH=$(GOARCH) GOMIPS=$(GOMIPS) GOARM=$(GOARM) go build $(PARAMS) -o $(BIN_DIR)/kvas2d ./cmd/kvas2d + +package: + @mkdir -p $(PKG_DIR)/control + @echo '2.0' > $(PKG_DIR)/debian-binary + @echo 'Package: $(APP_NAME)' > $(PKG_DIR)/control/control + @echo 'Version: $(VERSION)-$(COMMITS_SINCE_TAG)' >> $(PKG_DIR)/control/control + @echo 'Architecture: $(ARCH)' >> $(PKG_DIR)/control/control + @echo 'Maintainer: $(APP_MAINTAINER)' >> $(PKG_DIR)/control/control + @echo 'Description: $(APP_DESCRIPTION)' >> $(PKG_DIR)/control/control + @echo 'Section: base' >> $(PKG_DIR)/control/control + @echo 'Priority: optional' >> $(PKG_DIR)/control/control + @echo 'Depends: libc, iptables, socat' >> $(PKG_DIR)/control/control + @mkdir -p $(PKG_DIR)/data/opt/usr/bin + @cp -r ./opt/* $(PKG_DIR)/data/ + @fakeroot sh -c "tar -C $(PKG_DIR)/control -cvf $(PKG_DIR)/control.tar ." + @fakeroot sh -c "tar -C $(PKG_DIR)/data -cvf $(PKG_DIR)/data.tar ." + @ar r $(PKG_DIR)/$(APP_NAME)_$(ARCH).ipk $(PKG_DIR)/debian-binary $(PKG_DIR)/control.tar $(PKG_DIR)/data.tar diff --git a/README.md b/README.md index 3b583ea..4b6914d 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,3 @@ -# kvas2-go +# kvas2 Better implementation of [KVAS](https://github.com/qzeleza/kvas) - -Realized features: -- [x] DNS Proxy (UDP) -- [x] DNS Proxy (TCP) -- [x] Records memory -- [x] IPTables rules for rebind DNS server port -- [X] IPSet integration -- [X] IP integration -- [X] IPTables rules to IPSet -- [X] Catch interface up/down -- [X] Catch `netfilter.d` event -- [X] Rule composer (CRUD) -- [ ] GORM integration -- [X] Listing of interfaces -- [ ] HTTP API -- [ ] HTTP GUI -- [ ] CLI -- [X] (Keenetic) Support for custom interfaces -- [ ] It is not a concept now... REFACTORING TIME!!! -- [ ] (Keenetic) Getting readable names of interfaces from Keenetic NDMS -- [ ] HTTP Auth -- [ ] IPv6 support diff --git a/build_mipsel.sh b/build_mipsel.sh deleted file mode 100644 index edcc7ff..0000000 --- a/build_mipsel.sh +++ /dev/null @@ -1 +0,0 @@ -GOOS=linux GOMIPS=softfloat GOARCH=mipsle go build -v -a -trimpath -ldflags="-w -s" . diff --git a/cmd/kvas2d/main.go b/cmd/kvas2d/main.go new file mode 100644 index 0000000..bb51f8c --- /dev/null +++ b/cmd/kvas2d/main.go @@ -0,0 +1,173 @@ +package main + +import ( + "context" + "errors" + "fmt" + "os" + "os/signal" + "strconv" + "sync" + "syscall" + + "kvas2" + "kvas2/constant" + "kvas2/models" + + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "gopkg.in/yaml.v3" +) + +const cfgFolderLocation = "/opt/var/lib/kvas2" +const cfgFileLocation = cfgFolderLocation + "/config.yaml" +const pidFileLocation = "/opt/var/run/kvas2.pid" + +func checkPIDFile() error { + data, err := os.ReadFile(pidFileLocation) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return err + } + + pid, err := strconv.Atoi(string(data)) + if err != nil { + return errors.New("invalid PID file content") + } + + if err := syscall.Kill(pid, 0); err == nil { + return fmt.Errorf("process %d is already running", pid) + } + + _ = os.Remove(pidFileLocation) + return nil +} + +func createPIDFile() error { + pid := os.Getpid() + return os.WriteFile(pidFileLocation, []byte(strconv.Itoa(pid)), 0644) +} + +func removePIDFile() { + _ = os.Remove(pidFileLocation) +} + +func main() { + log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) + log.Info(). + Str("version", constant.Version). + Str("commit", constant.Commit). + Msg("starting kvas2 daemon") + + if err := checkPIDFile(); err != nil { + log.Fatal().Err(err).Msg("failed to start kvas2 daemon") + } + + if err := createPIDFile(); err != nil { + log.Fatal().Err(err).Msg("failed to create PID file") + } + defer removePIDFile() + + cfg := models.ConfigFile{} + + cfgFile, err := os.ReadFile(cfgFileLocation) + if err == nil { + err = yaml.Unmarshal(cfgFile, &cfg) + if err != nil { + log.Fatal().Err(err).Msg("failed to parse config.yaml") + } + + } else { + if !errors.Is(err, os.ErrNotExist) { + log.Fatal().Err(err).Msg("failed to read config.yaml") + } + cfg = models.ConfigFile{ + AppConfig: models.AppConfig{ + LogLevel: "info", + AdditionalTTL: 216000, + ChainPrefix: "KVAS2_", + IPSetPrefix: "kvas2_", + LinkName: "br0", + TargetDNSServerAddress: "127.0.0.1", + TargetDNSServerPort: 53, + ListenDNSPort: 3553, + }, + } + out, err := yaml.Marshal(cfg) + if err != nil { + log.Fatal().Err(err).Msg("failed to serialize config.yaml") + } + err = os.MkdirAll(cfgFolderLocation, os.ModePerm) + if err != nil { + log.Fatal().Err(err).Msg("failed to create config directory") + } + err = os.WriteFile(cfgFileLocation, out, 0600) + if err != nil { + log.Fatal().Err(err).Msg("failed to save config.yaml") + } + } + + switch cfg.AppConfig.LogLevel { + case "trace": + zerolog.SetGlobalLevel(zerolog.TraceLevel) + case "debug": + zerolog.SetGlobalLevel(zerolog.DebugLevel) + case "info": + zerolog.SetGlobalLevel(zerolog.InfoLevel) + case "warn": + zerolog.SetGlobalLevel(zerolog.WarnLevel) + case "error": + zerolog.SetGlobalLevel(zerolog.ErrorLevel) + case "fatal": + zerolog.SetGlobalLevel(zerolog.FatalLevel) + case "panic": + zerolog.SetGlobalLevel(zerolog.PanicLevel) + case "nolevel": + zerolog.SetGlobalLevel(zerolog.NoLevel) + case "disabled": + zerolog.SetGlobalLevel(zerolog.Disabled) + default: + zerolog.SetGlobalLevel(zerolog.InfoLevel) + } + + app, err := kvas2.New(cfg) + if err != nil { + log.Fatal().Err(err).Msg("failed to initialize application") + } + + ctx, cancel := context.WithCancel(context.Background()) + + log.Info().Msg("starting service") + + /* + Starting app with graceful shutdown + */ + appResult := make(chan error) + go func() { + appResult <- app.Start(ctx) + }() + + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + + var once sync.Once + closeEvent := func() { + log.Info().Msg("shutting down service") + cancel() + } + + for { + select { + case err, _ := <-appResult: + if err != nil { + log.Error().Err(err).Msg("failed to start application") + } + log.Info().Msg("exiting application") + return + case <-c: + once.Do(closeEvent) + } + } +} diff --git a/constant/version.go b/constant/version.go new file mode 100644 index 0000000..ab04266 --- /dev/null +++ b/constant/version.go @@ -0,0 +1,6 @@ +package constant + +var ( + Version = "unattached" + Commit = "undef" +) diff --git a/go.mod b/go.mod index 5d5920f..2c1dd53 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module kvas2-go +module kvas2 go 1.21 diff --git a/group/group.go b/group/group.go index e62db19..8b17e0f 100644 --- a/group/group.go +++ b/group/group.go @@ -5,9 +5,9 @@ import ( "net" "time" - "kvas2-go/models" - "kvas2-go/netfilter-helper" - "kvas2-go/records" + "kvas2/models" + "kvas2/netfilter-helper" + "kvas2/records" "github.com/coreos/go-iptables/iptables" "github.com/rs/zerolog/log" diff --git a/kvas2.go b/kvas2.go index 723c316..47232a6 100644 --- a/kvas2.go +++ b/kvas2.go @@ -1,4 +1,4 @@ -package main +package kvas2 import ( "context" @@ -12,11 +12,11 @@ import ( "strings" "time" - "kvas2-go/dns-mitm-proxy" - "kvas2-go/group" - "kvas2-go/models" - "kvas2-go/netfilter-helper" - "kvas2-go/records" + "kvas2/dns-mitm-proxy" + "kvas2/group" + "kvas2/models" + "kvas2/netfilter-helper" + "kvas2/records" "github.com/miekg/dns" "github.com/rs/zerolog/log" @@ -63,14 +63,13 @@ type App struct { } 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 { case 0x00000001: + log.Trace(). + Str("interface", event.Link.Attrs().Name). + Int("change", int(event.Change)). + Msg("interface event") ifaceName := event.Link.Attrs().Name for _, group := range a.Groups { if group.Interface != ifaceName { @@ -172,7 +171,7 @@ func (a *App) start(ctx context.Context) (err error) { /* Socket (for netfilter.d events) */ - socketPath := "/opt/var/run/kvas2-go.sock" + socketPath := "/opt/var/run/kvas2.sock" err = os.Remove(socketPath) if err != nil && !errors.Is(err, os.ErrNotExist) { return fmt.Errorf("failed to remove existed UNIX socket: %w", err) @@ -302,7 +301,7 @@ func (a *App) AddGroup(groupModel *models.Group) error { } a.Groups = append(a.Groups, grp) - log.Trace().Str("id", grp.ID.String()).Str("name", grp.Name).Msg("added group") + log.Debug().Str("id", grp.ID.String()).Str("name", grp.Name).Msg("added group") return grp.Sync(a.Records) } @@ -326,11 +325,20 @@ func (a *App) ListInterfaces() ([]net.Interface, error) { return interfaceNames, nil } -func (a *App) processARecord(aRecord dns.A) { +func (a *App) processARecord(aRecord dns.A, clientAddr net.Addr, network *string) { + var clientAddrStr, networkStr string + if clientAddr != nil { + clientAddrStr = clientAddr.String() + } + if network != nil { + networkStr = *network + } log.Trace(). Str("name", aRecord.Hdr.Name). Str("address", aRecord.A.String()). Int("ttl", int(aRecord.Hdr.Ttl)). + Str("clientAddr", clientAddrStr). + Str("network", networkStr). Msg("processing a record") ttlDuration := aRecord.Hdr.Ttl + a.Config.AdditionalTTL @@ -356,11 +364,10 @@ func (a *App) processARecord(aRecord dns.A) { Err(err). Msg("failed to add address") } else { - log.Trace(). + log.Debug(). Str("address", aRecord.A.String()). Str("aRecordDomain", aRecord.Hdr.Name). Str("cNameDomain", name). - Err(err). Msg("add address") } break Rule @@ -369,11 +376,20 @@ func (a *App) processARecord(aRecord dns.A) { } } -func (a *App) processCNameRecord(cNameRecord dns.CNAME) { +func (a *App) processCNameRecord(cNameRecord dns.CNAME, clientAddr net.Addr, network *string) { + var clientAddrStr, networkStr string + if clientAddr != nil { + clientAddrStr = clientAddr.String() + } + if network != nil { + networkStr = *network + } log.Trace(). Str("name", cNameRecord.Hdr.Name). Str("cname", cNameRecord.Target). Int("ttl", int(cNameRecord.Hdr.Ttl)). + Str("clientAddr", clientAddrStr). + Str("network", networkStr). Msg("processing cname record") ttlDuration := cNameRecord.Hdr.Ttl + a.Config.AdditionalTTL @@ -402,10 +418,9 @@ func (a *App) processCNameRecord(cNameRecord dns.CNAME) { Err(err). Msg("failed to add address") } else { - log.Trace(). + log.Debug(). Str("address", aRecord.Address.String()). Str("cNameDomain", name). - Err(err). Msg("add address") } } @@ -415,19 +430,19 @@ func (a *App) processCNameRecord(cNameRecord dns.CNAME) { } } -func (a *App) handleRecord(rr dns.RR) { +func (a *App) handleRecord(rr dns.RR, clientAddr net.Addr, network *string) { switch v := rr.(type) { case *dns.A: - a.processARecord(*v) + a.processARecord(*v, clientAddr, network) case *dns.CNAME: - a.processCNameRecord(*v) + a.processCNameRecord(*v, clientAddr, network) default: } } -func (a *App) handleMessage(msg dns.Msg) { +func (a *App) handleMessage(msg dns.Msg, clientAddr net.Addr, network *string) { for _, rr := range msg.Answer { - a.handleRecord(rr) + a.handleRecord(rr, clientAddr, network) } } @@ -510,7 +525,7 @@ func New(config models.ConfigFile) (*App, error) { } respMsg.Answer = respMsg.Answer[:idx] - app.handleMessage(respMsg) + app.handleMessage(respMsg, clientAddr, &network) return &respMsg, nil } diff --git a/main.go b/main.go deleted file mode 100644 index 118653f..0000000 --- a/main.go +++ /dev/null @@ -1,64 +0,0 @@ -package main - -import ( - "context" - "os" - "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}) - - 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") - } - - ctx, cancel := context.WithCancel(context.Background()) - - log.Info().Msg("starting service") - - /* - Starting app with graceful shutdown - */ - appResult := make(chan error) - go func() { - appResult <- app.Start(ctx) - }() - - c := make(chan os.Signal, 1) - signal.Notify(c, os.Interrupt, syscall.SIGTERM) - - for { - select { - case err, _ := <-appResult: - if err != nil { - log.Error().Err(err).Msg("failed to start application") - } - log.Info().Msg("exiting application") - return - case <-c: - log.Info().Msg("shutting down service") - cancel() - } - } -} diff --git a/models/config.go b/models/config.go index 3295bcd..e86bd25 100644 --- a/models/config.go +++ b/models/config.go @@ -6,6 +6,7 @@ type ConfigFile struct { } type AppConfig struct { + LogLevel string `yaml:"logLevel"` AdditionalTTL uint32 `yaml:"additionalTTL"` ChainPrefix string `yaml:"chainPrefix"` IPSetPrefix string `yaml:"ipsetPrefix"` diff --git a/opt/etc/ndm/netfilter.d/100-kvas2 b/opt/etc/ndm/netfilter.d/100-kvas2 index 0370e59..52d46c5 100755 --- a/opt/etc/ndm/netfilter.d/100-kvas2 +++ b/opt/etc/ndm/netfilter.d/100-kvas2 @@ -1,5 +1,5 @@ #!/bin/sh -SOCKET_PATH="/opt/var/run/kvas2-go.sock" +SOCKET_PATH="/opt/var/run/kvas2.sock" if [ ! -S "$SOCKET_PATH" ]; then exit fi diff --git a/config.yaml.example b/opt/var/lib/kvas2/config.yaml.example similarity index 100% rename from config.yaml.example rename to opt/var/lib/kvas2/config.yaml.example