Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
f08c93c
build: setup project infrastructure, CI/CD and linters
ilindan-dev May 12, 2026
3e70fec
feat(api): define v1 protobuf contract and project layout
ilindan-dev May 12, 2026
8aabcfc
Merge pull request #1 from ilindan-dev/feature/init-project
ilindan-dev May 12, 2026
df6871c
feat(domain): add sentinel errors used across domain logic
ilindan-dev May 13, 2026
dd8004f
feat(ports): defines the contracts to interact with external adapters
ilindan-dev May 13, 2026
ebe8420
Merge pull request #3 from ilindan-dev/feature/core-domain
ilindan-dev May 13, 2026
f9338ee
feat(resolvconf): implements utilities to read and update system reso…
ilindan-dev May 13, 2026
606638c
Merge pull request #4 from ilindan-dev/feature/resolve-conf
ilindan-dev May 13, 2026
405ca9a
feat(service): implement DNSManager and test that
ilindan-dev May 13, 2026
231b05d
Merge pull request #5 from ilindan-dev/feature/dns-manager
ilindan-dev May 13, 2026
71ed0fd
feat(grpc): add gRPC providers for client and server, test server pro…
ilindan-dev May 13, 2026
44a1eb8
Merge pull request #6 from ilindan-dev/feature/adapter-grpc
ilindan-dev May 13, 2026
93f0909
fix(resolvconf): use pointer for slog.Logger in FileAdapter
ilindan-dev May 14, 2026
bfaee10
feat(cli): add docs, improve cli interaction and add integration test…
ilindan-dev May 14, 2026
bded82c
chore(cmd): remove old cmd/client and cmd/server main packages that w…
ilindan-dev May 14, 2026
bbeb0c2
chore(Makefile): update target unit-test to test
ilindan-dev May 14, 2026
77c9ec8
ci: update GitHub Actions workflow
ilindan-dev May 14, 2026
06f7842
docs: add comprehensive README with setup and architecture details
ilindan-dev May 14, 2026
afc3a3c
Merge pull request #7 from ilindan-dev/feature/adapter-cli
ilindan-dev May 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: CI

on:
push:
branches: [ main, dev ]
pull_request:
branches: [ main, dev ]

jobs:
lint-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: '1.26'
cache: true

- name: Run golangci-lint
uses: golangci/golangci-lint-action@v9
with:
version: latest

- name: Run tests
run: make test
27 changes: 27 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Binaries
bin/
build/

# Test output and profiles
*.test
*.out
cover.html

# Environment variables
.env
.env.local

# IDEs and OS specific
.idea/
.vscode/
.DS_Store

# Go workspaces
go.work
go.work.sum

# Vendor directory
vendor/

# MD documentation
TASK.md
86 changes: 86 additions & 0 deletions .golangci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
version: "2"

run:
timeout: 5m
tests: true
modules-download-mode: readonly

formatters:
enable:
- gofumpt
- goimports
settings:
goimports:
local-prefixes:
- github.com/ilindan-dev/dns-manager

linters:
default: none

enable:
- errcheck
- govet
- staticcheck
- unused
- ineffassign

- bodyclose
- sqlclosecheck
- rowserrcheck
- copyloopvar
- makezero
- prealloc

- gosec
- gocritic
- errorlint
- nilerr
- nilnil
- revive
- lll
- gocyclo
- gocognit
- funlen
- nestif

settings:
lll:
line-length: 120
tab-width: 4
gocyclo:
min-complexity: 10
gocognit:
min-complexity: 15
nestif:
min-complexity: 4
funlen:
lines: 70
statements: 40

gocritic:
enabled-tags:
- diagnostic
- style
- performance

revive:
enable-default-rules: true
rules:
- name: exported
disabled: true
- name: package-comments
disabled: true

exclusions:
generated: lax
rules:
- path: _test\.go
linters:
- errcheck
- gosec
- bodyclose
- prealloc

issues:
max-issues-per-linter: 0
max-same-issues: 0
27 changes: 27 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
.PHONY: test
test:
go test -v -race ./...

.PHONY: lint
lint:
golangci-lint run ./...

.PHONY: tools
tools:
go install github.com/yoheimuta/protolint/cmd/protolint@latest
go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b $$(go env GOPATH)/bin v2.12.0
@echo "checking protobuf compiler, if it fails follow guide at https://protobuf.dev/installation/"
@which -s protoc && echo OK || exit 1

.PHONY: protolint
protolint:
protolint .

.PHONY: protobuf
protobuf:
protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
proto/dns/v1/dns.proto
105 changes: 105 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# DNS Manager

![Go Version](https://img.shields.io/badge/Go-1.26-00ADD8?style=flat&logo=go)
![Build Status](https://img.shields.io/badge/build-passing-success)
![Lint](https://img.shields.io/badge/lint-golangci--lint-blue)
![Architecture](https://img.shields.io/badge/architecture-hexagonal-orange)

**DNS Manager** - это легковесное клиент-серверное приложение на Go для безопасного управления DNS-серверами
операционной системы посредством редактирования конфигурации `/etc/resolv.conf`.

Проект реализован в виде **единого бинарного файла** (Unified Binary), который предоставляет CLI-интерфейс как для
запуска gRPC-сервера, так и для клиентского управления им.

## Ключевые особенности

* **Атомарная запись:** Обновление файла конфигурации происходит через временный файл и системный вызов `os.Rename`.
Система гарантированно не останется с поврежденным конфигом при сбоях питания или падении процесса.
* **Сохранность данных:** Парсер изменяет исключительно директивы `nameserver`, оставляя пользовательские комментарии и
системные настройки (например, `search`, `options`) в исходном виде.
* **Потокобезопасность:** Вся бизнес-логика защищена мьютексами (`sync.RWMutex`), что исключает состояние гонки при
конкурентных запросах к gRPC API.
* **Graceful Shutdown:** Сервер корректно обрабатывает системные сигналы (`SIGTERM`, `SIGINT`) и дожидается завершения
активных соединений перед остановкой.
* **Чистая Архитектура:** Разделение на порты и адаптеры (Hexagonal Architecture) позволяет легко тестировать
бизнес-логику изолированно от сети и файловой системы.

## Подготовка и сборка

Для работы с проектом потребуется **Go 1.26** и утилита **make**.

### Управление проектом через Makefile

В проекте настроен `Makefile` для автоматизации рутинных задач. Описание доступных команд:

* `make tools` - устанавливает все необходимые зависимости для разработки и кодогенерации (плагины `protoc`,
`protolint`, `grpcurl` и `golangci-lint`). *Убедитесь, что у вас установлен системный компилятор `protoc`*.
* `make protobuf` - компилирует файлы контрактов `proto/dns/v1/dns.proto` в исходный код Go (создает gRPC-клиенты и
стабы сервера).
* `make protolint` - проверяет стиль написания `.proto` файлов с помощью `protolint`.
* `make lint` - запускает строгую проверку исходного кода линтером `golangci-lint` в соответствии с конфигурацией
`.golangci.yaml`.
* `make test` - запускает все юнит-тесты и интеграционные E2E тесты с включенным детектором гонки данных (`-race`).

### Быстрый старт сборки

```bash
# 1. Установите инструменты и зависимости
make tools

# 2. Сгенерируйте gRPC код
make protobuf

# 3. Соберите бинарный файл
go build -o dns-manager ./cmd/dns-manager
```

## Использование

Приложение предоставляет единую точку входа со встроенной справочной системой (Cobra CLI). Для получения справки по
любой команде используйте флаг --help.

### Запуск сервера

По умолчанию сервер пытается изменять системный `/etc/resolv.conf`, для чего требуются права суперпользователя (`sudo`).
Для локального тестирования предусмотрен флаг `--config`.

```Bash
# Запуск в на порту 50051 (требует root)
sudo ./dns-manager server 50051

# Запуск с файлом конфига, куда будем писать
touch test_resolv.conf
./dns-manager server 50051 --config ./test_resolv.conf
```

### Команды клиента

Клиент позволяет управлять запущенным сервером по сети. По умолчанию клиент обращается к `localhost:50051`, но адрес
можно переопределить глобальным флагом `-s` или `--server`.

```Bash
# Добавить новый DNS-сервер
./dns-manager client add 8.8.8.8

# Удалить DNS-сервер
./dns-manager client remove 8.8.8.8

# Получить список текущих серверов
./dns-manager client list

# Пример управления удаленным сервером
./dns-manager client list --server 192.168.1.100:50051
```

## Тестирование

Проект покрыт изолированными юнит-тестами и полноценным интеграционным E2E-тестом.
Интеграционный тест разворачивает gRPC-сервер в памяти на динамическом порту, подключает к нему клиента и проводит
полный цикл проверки бизнес-логики над временным файлом.

Для запуска тестов используйте:

```Bash
make test
```
12 changes: 12 additions & 0 deletions cmd/dns-manager/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Package main is the entry point for the dns-manager unified binary.
// It delegates all execution logic to the CLI adapter, which handles
// command routing, flag parsing, and the application lifecycle.
package main

import "github.com/ilindan-dev/dns-manager/internal/adapters/cli"

func main() {
// Execute the root Cobra command. All setup, execution, and error handling
// (including os.Exit calls on fatal failures) are managed within the cli package.
cli.Execute()
}
18 changes: 18 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
module github.com/ilindan-dev/dns-manager

go 1.26.2

require (
github.com/spf13/cobra v1.10.2
google.golang.org/grpc v1.81.0
google.golang.org/protobuf v1.36.11
)

require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/spf13/pflag v1.0.9 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.34.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
)
48 changes: 48 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw=
google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Loading
Loading