diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..7266c3a --- /dev/null +++ b/.github/workflows/ci.yml @@ -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 unit-test \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1bac6db --- /dev/null +++ b/.gitignore @@ -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 \ No newline at end of file diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..6f2861e --- /dev/null +++ b/.golangci.yaml @@ -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 \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d7194a9 --- /dev/null +++ b/Makefile @@ -0,0 +1,27 @@ +.PHONY: unit-test +unit-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 \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..730a764 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +package dns_manager diff --git a/cmd/client/main.go b/cmd/client/main.go new file mode 100644 index 0000000..06ab7d0 --- /dev/null +++ b/cmd/client/main.go @@ -0,0 +1 @@ +package main diff --git a/cmd/server/main.go b/cmd/server/main.go new file mode 100644 index 0000000..06ab7d0 --- /dev/null +++ b/cmd/server/main.go @@ -0,0 +1 @@ +package main diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8ed4a2d --- /dev/null +++ b/go.mod @@ -0,0 +1,15 @@ +module github.com/ilindan-dev/dns-manager + +go 1.26.2 + +require ( + google.golang.org/grpc v1.81.0 + google.golang.org/protobuf v1.36.11 +) + +require ( + 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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..5045847 --- /dev/null +++ b/go.sum @@ -0,0 +1,38 @@ +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/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= +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= +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= diff --git a/internal/adapters/cli/cobra.go b/internal/adapters/cli/cobra.go new file mode 100644 index 0000000..7f1e458 --- /dev/null +++ b/internal/adapters/cli/cobra.go @@ -0,0 +1 @@ +package cli diff --git a/internal/adapters/grpc/client.go b/internal/adapters/grpc/client.go new file mode 100644 index 0000000..21e034e --- /dev/null +++ b/internal/adapters/grpc/client.go @@ -0,0 +1 @@ +package grpc diff --git a/internal/adapters/grpc/server.go b/internal/adapters/grpc/server.go new file mode 100644 index 0000000..21e034e --- /dev/null +++ b/internal/adapters/grpc/server.go @@ -0,0 +1 @@ +package grpc diff --git a/internal/adapters/resolvconf/file.go b/internal/adapters/resolvconf/file.go new file mode 100644 index 0000000..022894d --- /dev/null +++ b/internal/adapters/resolvconf/file.go @@ -0,0 +1 @@ +package resolvconf diff --git a/internal/core/domain/errors.go b/internal/core/domain/errors.go new file mode 100644 index 0000000..a9daa5c --- /dev/null +++ b/internal/core/domain/errors.go @@ -0,0 +1,34 @@ +// Package domain defines domain-level sentinel errors used by the core +// DNS manager logic. These exported error variables are intended for +// callers to compare using errors.Is so semantics remain stable across +// packages and versions. +package domain + +import "errors" + +// Sentinel errors used across domain logic. +var ( + // ErrInvalidIP indicates the provided DNS server address has an invalid + // textual IP format (neither a valid IPv4 nor IPv6). Use when validating + // client input for AddDNSServer. + ErrInvalidIP = errors.New("invalid IP address format") + + // ErrAlreadyExists signals an attempt to add a nameserver that is + // already present in the resolver configuration. Returning this allows + // callers to treat duplicate-add attempts as a recoverable, explicit + // condition instead of a generic success/failure. + ErrAlreadyExists = errors.New("DNS server already exists") + + // ErrNotFound indicates the requested DNS server address was not found + // when attempting removal. + ErrNotFound = errors.New("DNS server not found") + + // ErrPermissionDenied indicates the process lacks permissions to modify + // the resolver configuration (for example, writing /etc/resolv.conf). + ErrPermissionDenied = errors.New("permission denied while accessing resolver configuration") + + // ErrIO represents an unexpected I/O error while reading or writing the + // resolver configuration. When returning ErrIO, wrap the underlying + // error with fmt.Errorf("%w") so callers can inspect the cause. + ErrIO = errors.New("I/O error while accessing resolver configuration") +) diff --git a/internal/core/ports/dnsmanager.go b/internal/core/ports/dnsmanager.go new file mode 100644 index 0000000..3da5f11 --- /dev/null +++ b/internal/core/ports/dnsmanager.go @@ -0,0 +1,19 @@ +// Package ports defines the contracts that the core logic +// requires to interact with external adapters. +package ports + +import "context" + +// DNSManager defines a layer of use cases for managing DNS servers. +type DNSManager interface { + // AddDNSServer checks the validity of the IP address and adds it to the configuration, + // if it is not already there. + AddDNSServer(ctx context.Context, address string) error + + // RemoveDNSServer removes the IP address from the configuration. + // Should return an error if the address is not found. + RemoveDNSServer(ctx context.Context, address string) error + + // ListDNSServers returns a list of current DNS servers, preserving the original order. + ListDNSServers(ctx context.Context) ([]string, error) +} diff --git a/internal/core/ports/resolvconf.go b/internal/core/ports/resolvconf.go new file mode 100644 index 0000000..fc746e1 --- /dev/null +++ b/internal/core/ports/resolvconf.go @@ -0,0 +1,15 @@ +package ports + +import "context" + +// ResolvConf provides atomic operations for managing the system's resolver configuration. +type ResolvConf interface { + // GetServers reads the configuration and returns the ordered list + // of currently configured nameserver IPs. + GetServers(ctx context.Context) ([]string, error) + + // RewriteServers atomically replaces all existing nameservers with the provided list. + // The implementation MUST preserve all non-nameserver directives (e.g., search, options) + // and guarantee "all-or-nothing" execution to prevent configuration corruption. + RewriteServers(ctx context.Context, addresses []string) error +} diff --git a/internal/service/dns_manager.go b/internal/service/dns_manager.go new file mode 100644 index 0000000..6d43c33 --- /dev/null +++ b/internal/service/dns_manager.go @@ -0,0 +1 @@ +package service diff --git a/proto/dns/v1/dns.pb.go b/proto/dns/v1/dns.pb.go new file mode 100644 index 0000000..9e4be9c --- /dev/null +++ b/proto/dns/v1/dns.pb.go @@ -0,0 +1,244 @@ +// DNS Manager API (v1) +// +// This gRPC definition provides a minimal interface for managing the +// system's DNS servers. It is written to match the exercise described +// in TASK.md (see repository root). The server-side implementation is +// expected to apply changes to /etc/resolv.conf and validate input. +// Authentication is intentionally omitted per the task requirements. + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v3.21.12 +// source: proto/dns/v1/dns.proto + +package v1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + emptypb "google.golang.org/protobuf/types/known/emptypb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// AddDNSServerRequest carries a single DNS server address. Use a +// standard textual representation (IPv4/IPv6). The server should +// validate syntax and reject malformed addresses. +type AddDNSServerRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Address string `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"` // textual IP, e.g. "192.0.2.53" or "2001:db8::53" + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AddDNSServerRequest) Reset() { + *x = AddDNSServerRequest{} + mi := &file_proto_dns_v1_dns_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AddDNSServerRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddDNSServerRequest) ProtoMessage() {} + +func (x *AddDNSServerRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_dns_v1_dns_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AddDNSServerRequest.ProtoReflect.Descriptor instead. +func (*AddDNSServerRequest) Descriptor() ([]byte, []int) { + return file_proto_dns_v1_dns_proto_rawDescGZIP(), []int{0} +} + +func (x *AddDNSServerRequest) GetAddress() string { + if x != nil { + return x.Address + } + return "" +} + +// RemoveDNSServerRequest identifies a DNS server address to remove. +type RemoveDNSServerRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Address string `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"` // must match an existing entry to be removed + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RemoveDNSServerRequest) Reset() { + *x = RemoveDNSServerRequest{} + mi := &file_proto_dns_v1_dns_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RemoveDNSServerRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RemoveDNSServerRequest) ProtoMessage() {} + +func (x *RemoveDNSServerRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_dns_v1_dns_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RemoveDNSServerRequest.ProtoReflect.Descriptor instead. +func (*RemoveDNSServerRequest) Descriptor() ([]byte, []int) { + return file_proto_dns_v1_dns_proto_rawDescGZIP(), []int{1} +} + +func (x *RemoveDNSServerRequest) GetAddress() string { + if x != nil { + return x.Address + } + return "" +} + +// ListDNSServersResponse contains the ordered list of resolver +// addresses. The order should reflect the order they appear in +// /etc/resolv.conf. +type ListDNSServersResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Addresses []string `protobuf:"bytes,1,rep,name=addresses,proto3" json:"addresses,omitempty"` // ordered list of nameserver addresses + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListDNSServersResponse) Reset() { + *x = ListDNSServersResponse{} + mi := &file_proto_dns_v1_dns_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListDNSServersResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListDNSServersResponse) ProtoMessage() {} + +func (x *ListDNSServersResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_dns_v1_dns_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListDNSServersResponse.ProtoReflect.Descriptor instead. +func (*ListDNSServersResponse) Descriptor() ([]byte, []int) { + return file_proto_dns_v1_dns_proto_rawDescGZIP(), []int{2} +} + +func (x *ListDNSServersResponse) GetAddresses() []string { + if x != nil { + return x.Addresses + } + return nil +} + +var File_proto_dns_v1_dns_proto protoreflect.FileDescriptor + +const file_proto_dns_v1_dns_proto_rawDesc = "" + + "\n" + + "\x16proto/dns/v1/dns.proto\x12\x06dns.v1\x1a\x1bgoogle/protobuf/empty.proto\"/\n" + + "\x13AddDNSServerRequest\x12\x18\n" + + "\aaddress\x18\x01 \x01(\tR\aaddress\"2\n" + + "\x16RemoveDNSServerRequest\x12\x18\n" + + "\aaddress\x18\x01 \x01(\tR\aaddress\"6\n" + + "\x16ListDNSServersResponse\x12\x1c\n" + + "\taddresses\x18\x01 \x03(\tR\taddresses2\xe6\x01\n" + + "\n" + + "DNSManager\x12C\n" + + "\fAddDNSServer\x12\x1b.dns.v1.AddDNSServerRequest\x1a\x16.google.protobuf.Empty\x12I\n" + + "\x0fRemoveDNSServer\x12\x1e.dns.v1.RemoveDNSServerRequest\x1a\x16.google.protobuf.Empty\x12H\n" + + "\x0eListDNSServers\x12\x16.google.protobuf.Empty\x1a\x1e.dns.v1.ListDNSServersResponseB1Z/github.com/ilindan-dev/dns-manager/proto/dns/v1b\x06proto3" + +var ( + file_proto_dns_v1_dns_proto_rawDescOnce sync.Once + file_proto_dns_v1_dns_proto_rawDescData []byte +) + +func file_proto_dns_v1_dns_proto_rawDescGZIP() []byte { + file_proto_dns_v1_dns_proto_rawDescOnce.Do(func() { + file_proto_dns_v1_dns_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proto_dns_v1_dns_proto_rawDesc), len(file_proto_dns_v1_dns_proto_rawDesc))) + }) + return file_proto_dns_v1_dns_proto_rawDescData +} + +var file_proto_dns_v1_dns_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_proto_dns_v1_dns_proto_goTypes = []any{ + (*AddDNSServerRequest)(nil), // 0: dns.v1.AddDNSServerRequest + (*RemoveDNSServerRequest)(nil), // 1: dns.v1.RemoveDNSServerRequest + (*ListDNSServersResponse)(nil), // 2: dns.v1.ListDNSServersResponse + (*emptypb.Empty)(nil), // 3: google.protobuf.Empty +} +var file_proto_dns_v1_dns_proto_depIdxs = []int32{ + 0, // 0: dns.v1.DNSManager.AddDNSServer:input_type -> dns.v1.AddDNSServerRequest + 1, // 1: dns.v1.DNSManager.RemoveDNSServer:input_type -> dns.v1.RemoveDNSServerRequest + 3, // 2: dns.v1.DNSManager.ListDNSServers:input_type -> google.protobuf.Empty + 3, // 3: dns.v1.DNSManager.AddDNSServer:output_type -> google.protobuf.Empty + 3, // 4: dns.v1.DNSManager.RemoveDNSServer:output_type -> google.protobuf.Empty + 2, // 5: dns.v1.DNSManager.ListDNSServers:output_type -> dns.v1.ListDNSServersResponse + 3, // [3:6] is the sub-list for method output_type + 0, // [0:3] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_proto_dns_v1_dns_proto_init() } +func file_proto_dns_v1_dns_proto_init() { + if File_proto_dns_v1_dns_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_dns_v1_dns_proto_rawDesc), len(file_proto_dns_v1_dns_proto_rawDesc)), + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_proto_dns_v1_dns_proto_goTypes, + DependencyIndexes: file_proto_dns_v1_dns_proto_depIdxs, + MessageInfos: file_proto_dns_v1_dns_proto_msgTypes, + }.Build() + File_proto_dns_v1_dns_proto = out.File + file_proto_dns_v1_dns_proto_goTypes = nil + file_proto_dns_v1_dns_proto_depIdxs = nil +} diff --git a/proto/dns/v1/dns.proto b/proto/dns/v1/dns.proto new file mode 100644 index 0000000..7584e54 --- /dev/null +++ b/proto/dns/v1/dns.proto @@ -0,0 +1,57 @@ +// DNS Manager API (v1) +// +// This gRPC definition provides a minimal interface for managing the +// system's DNS servers. It is written to match the exercise described +// in TASK.md (see repository root). The server-side implementation is +// expected to apply changes to /etc/resolv.conf and validate input. +// Authentication is intentionally omitted per the task requirements. + +syntax = "proto3"; + +package dns.v1; + +import "google/protobuf/empty.proto"; + +option go_package = "github.com/ilindan-dev/dns-manager/proto/dns/v1"; + +// DNSManager exposes simple operations to add, remove and list DNS +// server addresses. The service assumes the server process will +// translate these RPCs into updates to /etc/resolv.conf and handle +// invalid input gracefully. +service DNSManager { + + // AddDNSServer adds a single DNS server address (typically an IP + // like "8.8.8.8") to the system resolver configuration. The server + // must validate the address format and return an error for invalid + // values. No payload is returned on success (google.protobuf.Empty). + rpc AddDNSServer(AddDNSServerRequest) returns (google.protobuf.Empty); + + // RemoveDNSServer removes the given DNS server address from the + // resolver configuration. If the address is not present, the server + // should return a clear, recoverable error. + rpc RemoveDNSServer(RemoveDNSServerRequest) returns (google.protobuf.Empty); + + // ListDNSServers returns the currently configured resolver addresses. + // This maps directly to the lines in /etc/resolv.conf that specify + // "nameserver" entries. + rpc ListDNSServers(google.protobuf.Empty) returns (ListDNSServersResponse); +} + +// AddDNSServerRequest carries a single DNS server address. Use a +// standard textual representation (IPv4/IPv6). The server should +// validate syntax and reject malformed addresses. +message AddDNSServerRequest { + string address = 1; // textual IP, e.g. "192.0.2.53" or "2001:db8::53" +} + +// RemoveDNSServerRequest identifies a DNS server address to remove. +message RemoveDNSServerRequest { + string address = 1; // must match an existing entry to be removed +} + +// ListDNSServersResponse contains the ordered list of resolver +// addresses. The order should reflect the order they appear in +// /etc/resolv.conf. +message ListDNSServersResponse { + repeated string addresses = 1; // ordered list of nameserver addresses +} diff --git a/proto/dns/v1/dns_grpc.pb.go b/proto/dns/v1/dns_grpc.pb.go new file mode 100644 index 0000000..5e3eb49 --- /dev/null +++ b/proto/dns/v1/dns_grpc.pb.go @@ -0,0 +1,236 @@ +// DNS Manager API (v1) +// +// This gRPC definition provides a minimal interface for managing the +// system's DNS servers. It is written to match the exercise described +// in TASK.md (see repository root). The server-side implementation is +// expected to apply changes to /etc/resolv.conf and validate input. +// Authentication is intentionally omitted per the task requirements. + +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.2 +// - protoc v3.21.12 +// source: proto/dns/v1/dns.proto + +package v1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + emptypb "google.golang.org/protobuf/types/known/emptypb" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + DNSManager_AddDNSServer_FullMethodName = "/dns.v1.DNSManager/AddDNSServer" + DNSManager_RemoveDNSServer_FullMethodName = "/dns.v1.DNSManager/RemoveDNSServer" + DNSManager_ListDNSServers_FullMethodName = "/dns.v1.DNSManager/ListDNSServers" +) + +// DNSManagerClient is the client API for DNSManager service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// DNSManager exposes simple operations to add, remove and list DNS +// server addresses. The service assumes the server process will +// translate these RPCs into updates to /etc/resolv.conf and handle +// invalid input gracefully. +type DNSManagerClient interface { + // AddDNSServer adds a single DNS server address (typically an IP + // like "8.8.8.8") to the system resolver configuration. The server + // must validate the address format and return an error for invalid + // values. No payload is returned on success (google.protobuf.Empty). + AddDNSServer(ctx context.Context, in *AddDNSServerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + // RemoveDNSServer removes the given DNS server address from the + // resolver configuration. If the address is not present, the server + // should return a clear, recoverable error. + RemoveDNSServer(ctx context.Context, in *RemoveDNSServerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + // ListDNSServers returns the currently configured resolver addresses. + // This maps directly to the lines in /etc/resolv.conf that specify + // "nameserver" entries. + ListDNSServers(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ListDNSServersResponse, error) +} + +type dNSManagerClient struct { + cc grpc.ClientConnInterface +} + +func NewDNSManagerClient(cc grpc.ClientConnInterface) DNSManagerClient { + return &dNSManagerClient{cc} +} + +func (c *dNSManagerClient) AddDNSServer(ctx context.Context, in *AddDNSServerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, DNSManager_AddDNSServer_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *dNSManagerClient) RemoveDNSServer(ctx context.Context, in *RemoveDNSServerRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, DNSManager_RemoveDNSServer_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *dNSManagerClient) ListDNSServers(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*ListDNSServersResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListDNSServersResponse) + err := c.cc.Invoke(ctx, DNSManager_ListDNSServers_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// DNSManagerServer is the server API for DNSManager service. +// All implementations must embed UnimplementedDNSManagerServer +// for forward compatibility. +// +// DNSManager exposes simple operations to add, remove and list DNS +// server addresses. The service assumes the server process will +// translate these RPCs into updates to /etc/resolv.conf and handle +// invalid input gracefully. +type DNSManagerServer interface { + // AddDNSServer adds a single DNS server address (typically an IP + // like "8.8.8.8") to the system resolver configuration. The server + // must validate the address format and return an error for invalid + // values. No payload is returned on success (google.protobuf.Empty). + AddDNSServer(context.Context, *AddDNSServerRequest) (*emptypb.Empty, error) + // RemoveDNSServer removes the given DNS server address from the + // resolver configuration. If the address is not present, the server + // should return a clear, recoverable error. + RemoveDNSServer(context.Context, *RemoveDNSServerRequest) (*emptypb.Empty, error) + // ListDNSServers returns the currently configured resolver addresses. + // This maps directly to the lines in /etc/resolv.conf that specify + // "nameserver" entries. + ListDNSServers(context.Context, *emptypb.Empty) (*ListDNSServersResponse, error) + mustEmbedUnimplementedDNSManagerServer() +} + +// UnimplementedDNSManagerServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedDNSManagerServer struct{} + +func (UnimplementedDNSManagerServer) AddDNSServer(context.Context, *AddDNSServerRequest) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method AddDNSServer not implemented") +} +func (UnimplementedDNSManagerServer) RemoveDNSServer(context.Context, *RemoveDNSServerRequest) (*emptypb.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method RemoveDNSServer not implemented") +} +func (UnimplementedDNSManagerServer) ListDNSServers(context.Context, *emptypb.Empty) (*ListDNSServersResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListDNSServers not implemented") +} +func (UnimplementedDNSManagerServer) mustEmbedUnimplementedDNSManagerServer() {} +func (UnimplementedDNSManagerServer) testEmbeddedByValue() {} + +// UnsafeDNSManagerServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to DNSManagerServer will +// result in compilation errors. +type UnsafeDNSManagerServer interface { + mustEmbedUnimplementedDNSManagerServer() +} + +func RegisterDNSManagerServer(s grpc.ServiceRegistrar, srv DNSManagerServer) { + // If the following call panics, it indicates UnimplementedDNSManagerServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&DNSManager_ServiceDesc, srv) +} + +func _DNSManager_AddDNSServer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AddDNSServerRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DNSManagerServer).AddDNSServer(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DNSManager_AddDNSServer_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DNSManagerServer).AddDNSServer(ctx, req.(*AddDNSServerRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _DNSManager_RemoveDNSServer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RemoveDNSServerRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DNSManagerServer).RemoveDNSServer(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DNSManager_RemoveDNSServer_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DNSManagerServer).RemoveDNSServer(ctx, req.(*RemoveDNSServerRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _DNSManager_ListDNSServers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(emptypb.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DNSManagerServer).ListDNSServers(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DNSManager_ListDNSServers_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DNSManagerServer).ListDNSServers(ctx, req.(*emptypb.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +// DNSManager_ServiceDesc is the grpc.ServiceDesc for DNSManager service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var DNSManager_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "dns.v1.DNSManager", + HandlerType: (*DNSManagerServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "AddDNSServer", + Handler: _DNSManager_AddDNSServer_Handler, + }, + { + MethodName: "RemoveDNSServer", + Handler: _DNSManager_RemoveDNSServer_Handler, + }, + { + MethodName: "ListDNSServers", + Handler: _DNSManager_ListDNSServers_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "proto/dns/v1/dns.proto", +}