Implement MCNS v1: custom Go DNS server replacing CoreDNS

Replace the CoreDNS precursor with a purpose-built authoritative DNS
server. Zones and records (A, AAAA, CNAME) are stored in SQLite and
managed via synchronized gRPC + REST APIs authenticated through MCIAS.
Non-authoritative queries are forwarded to upstream resolvers with
in-memory caching.

Key components:
- DNS server (miekg/dns) with authoritative zone handling and forwarding
- gRPC + REST management APIs with MCIAS auth (mcdsl integration)
- SQLite storage with CNAME exclusivity enforcement and auto SOA serials
- 30 tests covering database CRUD, DNS resolution, and caching

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-26 18:37:14 -07:00
parent a545fec658
commit f9635578e0
48 changed files with 6015 additions and 87 deletions

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
/mcns
srv/
*.db
*.db-wal
*.db-shm

90
.golangci.yaml Normal file
View File

@@ -0,0 +1,90 @@
# golangci-lint v2 configuration for mcns.
# Principle: fail loudly. Security and correctness issues are errors, not warnings.
version: "2"
run:
timeout: 5m
tests: true
linters:
default: none
enable:
# --- Correctness ---
- errcheck
- govet
- ineffassign
- unused
# --- Error handling ---
- errorlint
# --- Security ---
- gosec
- staticcheck
# --- Style / conventions ---
- revive
settings:
errcheck:
check-blank: false
check-type-assertions: true
govet:
enable-all: true
disable:
- shadow
- fieldalignment
gosec:
severity: medium
confidence: medium
excludes:
- G104
errorlint:
errorf: true
asserts: true
comparison: true
revive:
rules:
- name: error-return
severity: error
- name: unexported-return
severity: error
- name: error-strings
severity: warning
- name: if-return
severity: warning
- name: increment-decrement
severity: warning
- name: var-naming
severity: warning
- name: range
severity: warning
- name: time-naming
severity: warning
- name: indent-error-flow
severity: warning
- name: early-return
severity: warning
formatters:
enable:
- gofmt
- goimports
issues:
max-issues-per-linter: 0
max-same-issues: 0
exclusions:
paths:
- vendor
rules:
- path: "_test\\.go"
linters:
- gosec
text: "G101"

View File

@@ -1,20 +0,0 @@
# Internal zone for Metacircular service discovery.
# Authoritative for svc.mcp.metacircular.net and mcp.metacircular.net.
# Everything else forwards to public resolvers.
svc.mcp.metacircular.net {
file /etc/coredns/zones/svc.mcp.metacircular.net.zone
log
}
mcp.metacircular.net {
file /etc/coredns/zones/mcp.metacircular.net.zone
log
}
. {
forward . 1.1.1.1 8.8.8.8
cache 30
log
errors
}

23
Dockerfile Normal file
View File

@@ -0,0 +1,23 @@
FROM golang:1.25-alpine AS builder
ARG VERSION=dev
RUN apk add --no-cache git
WORKDIR /build
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w -X main.version=${VERSION}" -o mcns ./cmd/mcns
FROM alpine:3.21
RUN addgroup -S mcns && adduser -S mcns -G mcns
COPY --from=builder /build/mcns /usr/local/bin/mcns
USER mcns
EXPOSE 53/udp 53/tcp 8443 9443
ENTRYPOINT ["mcns"]
CMD ["server", "--config", "/srv/mcns/mcns.toml"]

40
Makefile Normal file
View File

@@ -0,0 +1,40 @@
.PHONY: build test vet lint proto proto-lint clean docker all devserver
LDFLAGS := -trimpath -ldflags="-s -w -X main.version=$(shell git describe --tags --always --dirty)"
mcns:
CGO_ENABLED=0 go build $(LDFLAGS) -o mcns ./cmd/mcns
build:
go build ./...
test:
go test ./...
vet:
go vet ./...
lint:
golangci-lint run ./...
proto:
protoc --go_out=. --go_opt=module=git.wntrmute.dev/kyle/mcns \
--go-grpc_out=. --go-grpc_opt=module=git.wntrmute.dev/kyle/mcns \
proto/mcns/v1/*.proto
proto-lint:
buf lint
buf breaking --against '.git#branch=master,subdir=proto'
clean:
rm -f mcns
docker:
docker build --build-arg VERSION=$(shell git describe --tags --always --dirty) -t mcns -f Dockerfile .
devserver: mcns
@mkdir -p srv
@if [ ! -f srv/mcns.toml ]; then cp deploy/examples/mcns.toml srv/mcns.toml; echo "Created srv/mcns.toml from example — edit before running."; fi
./mcns server --config srv/mcns.toml
all: vet lint test mcns

9
buf.yaml Normal file
View File

@@ -0,0 +1,9 @@
version: v2
modules:
- path: proto
lint:
use:
- STANDARD
breaking:
use:
- FILE

278
cmd/mcns/main.go Normal file
View File

@@ -0,0 +1,278 @@
package main
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"log/slog"
"net"
"net/http"
"os"
"os/signal"
"path/filepath"
"syscall"
"time"
"github.com/spf13/cobra"
mcdslauth "git.wntrmute.dev/kyle/mcdsl/auth"
mcdsldb "git.wntrmute.dev/kyle/mcdsl/db"
"git.wntrmute.dev/kyle/mcns/internal/config"
"git.wntrmute.dev/kyle/mcns/internal/db"
mcnsdns "git.wntrmute.dev/kyle/mcns/internal/dns"
"git.wntrmute.dev/kyle/mcns/internal/grpcserver"
"git.wntrmute.dev/kyle/mcns/internal/server"
)
var version = "dev"
func main() {
root := &cobra.Command{
Use: "mcns",
Short: "Metacircular Networking Service",
Version: version,
}
root.AddCommand(serverCmd())
root.AddCommand(statusCmd())
root.AddCommand(snapshotCmd())
if err := root.Execute(); err != nil {
os.Exit(1)
}
}
func serverCmd() *cobra.Command {
var configPath string
cmd := &cobra.Command{
Use: "server",
Short: "Start the DNS and API servers",
RunE: func(_ *cobra.Command, _ []string) error {
return runServer(configPath)
},
}
cmd.Flags().StringVarP(&configPath, "config", "c", "mcns.toml", "path to configuration file")
return cmd
}
func runServer(configPath string) error {
cfg, err := config.Load(configPath)
if err != nil {
return fmt.Errorf("load config: %w", err)
}
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
Level: parseLogLevel(cfg.Log.Level),
}))
// Open and migrate the database.
database, err := db.Open(cfg.Database.Path)
if err != nil {
return fmt.Errorf("open database: %w", err)
}
defer database.Close()
if err := database.Migrate(); err != nil {
return fmt.Errorf("migrate database: %w", err)
}
// Create auth client for MCIAS integration.
authClient, err := mcdslauth.New(mcdslauth.Config{
ServerURL: cfg.MCIAS.ServerURL,
CACert: cfg.MCIAS.CACert,
ServiceName: cfg.MCIAS.ServiceName,
Tags: cfg.MCIAS.Tags,
}, logger)
if err != nil {
return fmt.Errorf("create auth client: %w", err)
}
// Start DNS server.
dnsServer := mcnsdns.New(database, cfg.DNS.Upstreams, logger)
// Build REST API router.
router := server.NewRouter(server.Deps{
DB: database,
Auth: authClient,
Logger: logger,
})
// TLS configuration.
cert, err := tls.LoadX509KeyPair(cfg.Server.TLSCert, cfg.Server.TLSKey)
if err != nil {
return fmt.Errorf("load TLS cert: %w", err)
}
tlsCfg := &tls.Config{
MinVersion: tls.VersionTLS13,
Certificates: []tls.Certificate{cert},
}
// HTTP server.
httpServer := &http.Server{
Addr: cfg.Server.ListenAddr,
Handler: router,
TLSConfig: tlsCfg,
ReadTimeout: cfg.Server.ReadTimeout.Duration,
WriteTimeout: cfg.Server.WriteTimeout.Duration,
IdleTimeout: cfg.Server.IdleTimeout.Duration,
}
// Start gRPC server if configured.
var grpcSrv *grpcserver.Server
var grpcLis net.Listener
if cfg.Server.GRPCAddr != "" {
grpcSrv, err = grpcserver.New(cfg.Server.TLSCert, cfg.Server.TLSKey, grpcserver.Deps{
DB: database,
Authenticator: authClient,
}, logger)
if err != nil {
return fmt.Errorf("create gRPC server: %w", err)
}
grpcLis, err = net.Listen("tcp", cfg.Server.GRPCAddr)
if err != nil {
return fmt.Errorf("listen gRPC on %s: %w", cfg.Server.GRPCAddr, err)
}
}
// Graceful shutdown on SIGINT/SIGTERM.
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
errCh := make(chan error, 3)
// Start DNS server.
go func() {
errCh <- dnsServer.ListenAndServe(cfg.DNS.ListenAddr)
}()
// Start gRPC server.
if grpcSrv != nil {
go func() {
logger.Info("gRPC server listening", "addr", grpcLis.Addr())
errCh <- grpcSrv.Serve(grpcLis)
}()
}
// Start HTTP server.
go func() {
logger.Info("mcns starting",
"version", version,
"addr", cfg.Server.ListenAddr,
"dns_addr", cfg.DNS.ListenAddr,
)
errCh <- httpServer.ListenAndServeTLS("", "")
}()
select {
case err := <-errCh:
return fmt.Errorf("server error: %w", err)
case <-ctx.Done():
logger.Info("shutting down")
dnsServer.Shutdown()
if grpcSrv != nil {
grpcSrv.GracefulStop()
}
shutdownTimeout := 30 * time.Second
if cfg.Server.ShutdownTimeout.Duration > 0 {
shutdownTimeout = cfg.Server.ShutdownTimeout.Duration
}
shutdownCtx, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
defer cancel()
if err := httpServer.Shutdown(shutdownCtx); err != nil {
return fmt.Errorf("shutdown: %w", err)
}
logger.Info("mcns stopped")
return nil
}
}
func statusCmd() *cobra.Command {
var addr, caCert string
cmd := &cobra.Command{
Use: "status",
Short: "Check MCNS health",
RunE: func(_ *cobra.Command, _ []string) error {
tlsCfg := &tls.Config{MinVersion: tls.VersionTLS13}
if caCert != "" {
pemData, err := os.ReadFile(caCert)
if err != nil {
return fmt.Errorf("read CA cert: %w", err)
}
pool := x509.NewCertPool()
if !pool.AppendCertsFromPEM(pemData) {
return fmt.Errorf("no valid certificates in %s", caCert)
}
tlsCfg.RootCAs = pool
}
client := &http.Client{
Transport: &http.Transport{TLSClientConfig: tlsCfg},
Timeout: 5 * time.Second,
}
resp, err := client.Get(addr + "/v1/health")
if err != nil {
return fmt.Errorf("health check: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("health check: status %d", resp.StatusCode)
}
fmt.Println("ok")
return nil
},
}
cmd.Flags().StringVar(&addr, "addr", "https://localhost:8443", "server address")
cmd.Flags().StringVar(&caCert, "ca-cert", "", "CA certificate for TLS verification")
return cmd
}
func snapshotCmd() *cobra.Command {
var configPath string
cmd := &cobra.Command{
Use: "snapshot",
Short: "Database backup via VACUUM INTO",
RunE: func(_ *cobra.Command, _ []string) error {
cfg, err := config.Load(configPath)
if err != nil {
return fmt.Errorf("load config: %w", err)
}
database, err := db.Open(cfg.Database.Path)
if err != nil {
return fmt.Errorf("open database: %w", err)
}
defer database.Close()
backupDir := filepath.Join(filepath.Dir(cfg.Database.Path), "backups")
snapName := fmt.Sprintf("mcns-%s.db", time.Now().Format("20060102-150405"))
snapPath := filepath.Join(backupDir, snapName)
if err := mcdsldb.Snapshot(database.DB, snapPath); err != nil {
return fmt.Errorf("snapshot: %w", err)
}
fmt.Printf("Snapshot saved to %s\n", snapPath)
return nil
},
}
cmd.Flags().StringVarP(&configPath, "config", "c", "mcns.toml", "path to configuration file")
return cmd
}
func parseLogLevel(s string) slog.Level {
switch s {
case "debug":
return slog.LevelDebug
case "warn":
return slog.LevelWarn
case "error":
return slog.LevelError
default:
return slog.LevelInfo
}
}

View File

@@ -1,25 +1,18 @@
# CoreDNS on rift — MCNS precursor.
#
# Serves the svc.mcp.metacircular.net and mcp.metacircular.net zones.
# Forwards everything else to 1.1.1.1 and 8.8.8.8.
# MCNS on rift — authoritative DNS + management API.
#
# Usage:
# docker compose -f deploy/docker/docker-compose-rift.yml up -d
#
# To use as the network's DNS server, point clients or the router at
# rift's IP (192.168.88.181) on port 53.
services:
coredns:
image: coredns/coredns:1.12.1
container_name: mcns-coredns
mcns:
image: mcr.svc.mcp.metacircular.net:8443/mcns:latest
container_name: mcns
restart: unless-stopped
command: -conf /etc/coredns/Corefile
command: ["server", "--config", "/srv/mcns/mcns.toml"]
ports:
- "192.168.88.181:53:53/udp"
- "192.168.88.181:53:53/tcp"
- "100.95.252.120:53:53/udp"
- "100.95.252.120:53:53/tcp"
volumes:
- ../../Corefile:/etc/coredns/Corefile:ro
- ../../zones:/etc/coredns/zones:ro
- /srv/mcns:/srv/mcns

21
deploy/examples/mcns.toml Normal file
View File

@@ -0,0 +1,21 @@
[server]
listen_addr = ":8443"
grpc_addr = ":9443"
tls_cert = "/srv/mcns/certs/cert.pem"
tls_key = "/srv/mcns/certs/key.pem"
[database]
path = "/srv/mcns/mcns.db"
[dns]
listen_addr = ":53"
upstreams = ["1.1.1.1:53", "8.8.8.8:53"]
[mcias]
server_url = "https://svc.metacircular.net:8443"
ca_cert = ""
service_name = "mcns"
tags = []
[log]
level = "info"

164
gen/mcns/v1/admin.pb.go Normal file
View File

@@ -0,0 +1,164 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc v6.32.1
// source: proto/mcns/v1/admin.proto
package v1
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
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)
)
type HealthRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *HealthRequest) Reset() {
*x = HealthRequest{}
mi := &file_proto_mcns_v1_admin_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *HealthRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*HealthRequest) ProtoMessage() {}
func (x *HealthRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcns_v1_admin_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 HealthRequest.ProtoReflect.Descriptor instead.
func (*HealthRequest) Descriptor() ([]byte, []int) {
return file_proto_mcns_v1_admin_proto_rawDescGZIP(), []int{0}
}
type HealthResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Status string `protobuf:"bytes,1,opt,name=status,proto3" json:"status,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *HealthResponse) Reset() {
*x = HealthResponse{}
mi := &file_proto_mcns_v1_admin_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *HealthResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*HealthResponse) ProtoMessage() {}
func (x *HealthResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcns_v1_admin_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 HealthResponse.ProtoReflect.Descriptor instead.
func (*HealthResponse) Descriptor() ([]byte, []int) {
return file_proto_mcns_v1_admin_proto_rawDescGZIP(), []int{1}
}
func (x *HealthResponse) GetStatus() string {
if x != nil {
return x.Status
}
return ""
}
var File_proto_mcns_v1_admin_proto protoreflect.FileDescriptor
const file_proto_mcns_v1_admin_proto_rawDesc = "" +
"\n" +
"\x19proto/mcns/v1/admin.proto\x12\amcns.v1\"\x0f\n" +
"\rHealthRequest\"(\n" +
"\x0eHealthResponse\x12\x16\n" +
"\x06status\x18\x01 \x01(\tR\x06status2I\n" +
"\fAdminService\x129\n" +
"\x06Health\x12\x16.mcns.v1.HealthRequest\x1a\x17.mcns.v1.HealthResponseB(Z&git.wntrmute.dev/kyle/mcns/gen/mcns/v1b\x06proto3"
var (
file_proto_mcns_v1_admin_proto_rawDescOnce sync.Once
file_proto_mcns_v1_admin_proto_rawDescData []byte
)
func file_proto_mcns_v1_admin_proto_rawDescGZIP() []byte {
file_proto_mcns_v1_admin_proto_rawDescOnce.Do(func() {
file_proto_mcns_v1_admin_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proto_mcns_v1_admin_proto_rawDesc), len(file_proto_mcns_v1_admin_proto_rawDesc)))
})
return file_proto_mcns_v1_admin_proto_rawDescData
}
var file_proto_mcns_v1_admin_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_proto_mcns_v1_admin_proto_goTypes = []any{
(*HealthRequest)(nil), // 0: mcns.v1.HealthRequest
(*HealthResponse)(nil), // 1: mcns.v1.HealthResponse
}
var file_proto_mcns_v1_admin_proto_depIdxs = []int32{
0, // 0: mcns.v1.AdminService.Health:input_type -> mcns.v1.HealthRequest
1, // 1: mcns.v1.AdminService.Health:output_type -> mcns.v1.HealthResponse
1, // [1:2] is the sub-list for method output_type
0, // [0:1] 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_mcns_v1_admin_proto_init() }
func file_proto_mcns_v1_admin_proto_init() {
if File_proto_mcns_v1_admin_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_mcns_v1_admin_proto_rawDesc), len(file_proto_mcns_v1_admin_proto_rawDesc)),
NumEnums: 0,
NumMessages: 2,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_proto_mcns_v1_admin_proto_goTypes,
DependencyIndexes: file_proto_mcns_v1_admin_proto_depIdxs,
MessageInfos: file_proto_mcns_v1_admin_proto_msgTypes,
}.Build()
File_proto_mcns_v1_admin_proto = out.File
file_proto_mcns_v1_admin_proto_goTypes = nil
file_proto_mcns_v1_admin_proto_depIdxs = nil
}

View File

@@ -0,0 +1,121 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.6.1
// - protoc v6.32.1
// source: proto/mcns/v1/admin.proto
package v1
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// 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 (
AdminService_Health_FullMethodName = "/mcns.v1.AdminService/Health"
)
// AdminServiceClient is the client API for AdminService 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.
type AdminServiceClient interface {
Health(ctx context.Context, in *HealthRequest, opts ...grpc.CallOption) (*HealthResponse, error)
}
type adminServiceClient struct {
cc grpc.ClientConnInterface
}
func NewAdminServiceClient(cc grpc.ClientConnInterface) AdminServiceClient {
return &adminServiceClient{cc}
}
func (c *adminServiceClient) Health(ctx context.Context, in *HealthRequest, opts ...grpc.CallOption) (*HealthResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(HealthResponse)
err := c.cc.Invoke(ctx, AdminService_Health_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// AdminServiceServer is the server API for AdminService service.
// All implementations must embed UnimplementedAdminServiceServer
// for forward compatibility.
type AdminServiceServer interface {
Health(context.Context, *HealthRequest) (*HealthResponse, error)
mustEmbedUnimplementedAdminServiceServer()
}
// UnimplementedAdminServiceServer 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 UnimplementedAdminServiceServer struct{}
func (UnimplementedAdminServiceServer) Health(context.Context, *HealthRequest) (*HealthResponse, error) {
return nil, status.Error(codes.Unimplemented, "method Health not implemented")
}
func (UnimplementedAdminServiceServer) mustEmbedUnimplementedAdminServiceServer() {}
func (UnimplementedAdminServiceServer) testEmbeddedByValue() {}
// UnsafeAdminServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to AdminServiceServer will
// result in compilation errors.
type UnsafeAdminServiceServer interface {
mustEmbedUnimplementedAdminServiceServer()
}
func RegisterAdminServiceServer(s grpc.ServiceRegistrar, srv AdminServiceServer) {
// If the following call panics, it indicates UnimplementedAdminServiceServer 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(&AdminService_ServiceDesc, srv)
}
func _AdminService_Health_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(HealthRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AdminServiceServer).Health(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AdminService_Health_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AdminServiceServer).Health(ctx, req.(*HealthRequest))
}
return interceptor(ctx, in, info, handler)
}
// AdminService_ServiceDesc is the grpc.ServiceDesc for AdminService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var AdminService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "mcns.v1.AdminService",
HandlerType: (*AdminServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "Health",
Handler: _AdminService_Health_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "proto/mcns/v1/admin.proto",
}

279
gen/mcns/v1/auth.pb.go Normal file
View File

@@ -0,0 +1,279 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc v6.32.1
// source: proto/mcns/v1/auth.proto
package v1
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
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)
)
type LoginRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"`
Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"`
TotpCode string `protobuf:"bytes,3,opt,name=totp_code,json=totpCode,proto3" json:"totp_code,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *LoginRequest) Reset() {
*x = LoginRequest{}
mi := &file_proto_mcns_v1_auth_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *LoginRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*LoginRequest) ProtoMessage() {}
func (x *LoginRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcns_v1_auth_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 LoginRequest.ProtoReflect.Descriptor instead.
func (*LoginRequest) Descriptor() ([]byte, []int) {
return file_proto_mcns_v1_auth_proto_rawDescGZIP(), []int{0}
}
func (x *LoginRequest) GetUsername() string {
if x != nil {
return x.Username
}
return ""
}
func (x *LoginRequest) GetPassword() string {
if x != nil {
return x.Password
}
return ""
}
func (x *LoginRequest) GetTotpCode() string {
if x != nil {
return x.TotpCode
}
return ""
}
type LoginResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *LoginResponse) Reset() {
*x = LoginResponse{}
mi := &file_proto_mcns_v1_auth_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *LoginResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*LoginResponse) ProtoMessage() {}
func (x *LoginResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcns_v1_auth_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 LoginResponse.ProtoReflect.Descriptor instead.
func (*LoginResponse) Descriptor() ([]byte, []int) {
return file_proto_mcns_v1_auth_proto_rawDescGZIP(), []int{1}
}
func (x *LoginResponse) GetToken() string {
if x != nil {
return x.Token
}
return ""
}
type LogoutRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *LogoutRequest) Reset() {
*x = LogoutRequest{}
mi := &file_proto_mcns_v1_auth_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *LogoutRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*LogoutRequest) ProtoMessage() {}
func (x *LogoutRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcns_v1_auth_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 LogoutRequest.ProtoReflect.Descriptor instead.
func (*LogoutRequest) Descriptor() ([]byte, []int) {
return file_proto_mcns_v1_auth_proto_rawDescGZIP(), []int{2}
}
func (x *LogoutRequest) GetToken() string {
if x != nil {
return x.Token
}
return ""
}
type LogoutResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *LogoutResponse) Reset() {
*x = LogoutResponse{}
mi := &file_proto_mcns_v1_auth_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *LogoutResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*LogoutResponse) ProtoMessage() {}
func (x *LogoutResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcns_v1_auth_proto_msgTypes[3]
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 LogoutResponse.ProtoReflect.Descriptor instead.
func (*LogoutResponse) Descriptor() ([]byte, []int) {
return file_proto_mcns_v1_auth_proto_rawDescGZIP(), []int{3}
}
var File_proto_mcns_v1_auth_proto protoreflect.FileDescriptor
const file_proto_mcns_v1_auth_proto_rawDesc = "" +
"\n" +
"\x18proto/mcns/v1/auth.proto\x12\amcns.v1\"c\n" +
"\fLoginRequest\x12\x1a\n" +
"\busername\x18\x01 \x01(\tR\busername\x12\x1a\n" +
"\bpassword\x18\x02 \x01(\tR\bpassword\x12\x1b\n" +
"\ttotp_code\x18\x03 \x01(\tR\btotpCode\"%\n" +
"\rLoginResponse\x12\x14\n" +
"\x05token\x18\x01 \x01(\tR\x05token\"%\n" +
"\rLogoutRequest\x12\x14\n" +
"\x05token\x18\x01 \x01(\tR\x05token\"\x10\n" +
"\x0eLogoutResponse2\x80\x01\n" +
"\vAuthService\x126\n" +
"\x05Login\x12\x15.mcns.v1.LoginRequest\x1a\x16.mcns.v1.LoginResponse\x129\n" +
"\x06Logout\x12\x16.mcns.v1.LogoutRequest\x1a\x17.mcns.v1.LogoutResponseB(Z&git.wntrmute.dev/kyle/mcns/gen/mcns/v1b\x06proto3"
var (
file_proto_mcns_v1_auth_proto_rawDescOnce sync.Once
file_proto_mcns_v1_auth_proto_rawDescData []byte
)
func file_proto_mcns_v1_auth_proto_rawDescGZIP() []byte {
file_proto_mcns_v1_auth_proto_rawDescOnce.Do(func() {
file_proto_mcns_v1_auth_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proto_mcns_v1_auth_proto_rawDesc), len(file_proto_mcns_v1_auth_proto_rawDesc)))
})
return file_proto_mcns_v1_auth_proto_rawDescData
}
var file_proto_mcns_v1_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
var file_proto_mcns_v1_auth_proto_goTypes = []any{
(*LoginRequest)(nil), // 0: mcns.v1.LoginRequest
(*LoginResponse)(nil), // 1: mcns.v1.LoginResponse
(*LogoutRequest)(nil), // 2: mcns.v1.LogoutRequest
(*LogoutResponse)(nil), // 3: mcns.v1.LogoutResponse
}
var file_proto_mcns_v1_auth_proto_depIdxs = []int32{
0, // 0: mcns.v1.AuthService.Login:input_type -> mcns.v1.LoginRequest
2, // 1: mcns.v1.AuthService.Logout:input_type -> mcns.v1.LogoutRequest
1, // 2: mcns.v1.AuthService.Login:output_type -> mcns.v1.LoginResponse
3, // 3: mcns.v1.AuthService.Logout:output_type -> mcns.v1.LogoutResponse
2, // [2:4] is the sub-list for method output_type
0, // [0:2] 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_mcns_v1_auth_proto_init() }
func file_proto_mcns_v1_auth_proto_init() {
if File_proto_mcns_v1_auth_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_mcns_v1_auth_proto_rawDesc), len(file_proto_mcns_v1_auth_proto_rawDesc)),
NumEnums: 0,
NumMessages: 4,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_proto_mcns_v1_auth_proto_goTypes,
DependencyIndexes: file_proto_mcns_v1_auth_proto_depIdxs,
MessageInfos: file_proto_mcns_v1_auth_proto_msgTypes,
}.Build()
File_proto_mcns_v1_auth_proto = out.File
file_proto_mcns_v1_auth_proto_goTypes = nil
file_proto_mcns_v1_auth_proto_depIdxs = nil
}

159
gen/mcns/v1/auth_grpc.pb.go Normal file
View File

@@ -0,0 +1,159 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.6.1
// - protoc v6.32.1
// source: proto/mcns/v1/auth.proto
package v1
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// 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 (
AuthService_Login_FullMethodName = "/mcns.v1.AuthService/Login"
AuthService_Logout_FullMethodName = "/mcns.v1.AuthService/Logout"
)
// AuthServiceClient is the client API for AuthService 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.
type AuthServiceClient interface {
Login(ctx context.Context, in *LoginRequest, opts ...grpc.CallOption) (*LoginResponse, error)
Logout(ctx context.Context, in *LogoutRequest, opts ...grpc.CallOption) (*LogoutResponse, error)
}
type authServiceClient struct {
cc grpc.ClientConnInterface
}
func NewAuthServiceClient(cc grpc.ClientConnInterface) AuthServiceClient {
return &authServiceClient{cc}
}
func (c *authServiceClient) Login(ctx context.Context, in *LoginRequest, opts ...grpc.CallOption) (*LoginResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(LoginResponse)
err := c.cc.Invoke(ctx, AuthService_Login_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *authServiceClient) Logout(ctx context.Context, in *LogoutRequest, opts ...grpc.CallOption) (*LogoutResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(LogoutResponse)
err := c.cc.Invoke(ctx, AuthService_Logout_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// AuthServiceServer is the server API for AuthService service.
// All implementations must embed UnimplementedAuthServiceServer
// for forward compatibility.
type AuthServiceServer interface {
Login(context.Context, *LoginRequest) (*LoginResponse, error)
Logout(context.Context, *LogoutRequest) (*LogoutResponse, error)
mustEmbedUnimplementedAuthServiceServer()
}
// UnimplementedAuthServiceServer 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 UnimplementedAuthServiceServer struct{}
func (UnimplementedAuthServiceServer) Login(context.Context, *LoginRequest) (*LoginResponse, error) {
return nil, status.Error(codes.Unimplemented, "method Login not implemented")
}
func (UnimplementedAuthServiceServer) Logout(context.Context, *LogoutRequest) (*LogoutResponse, error) {
return nil, status.Error(codes.Unimplemented, "method Logout not implemented")
}
func (UnimplementedAuthServiceServer) mustEmbedUnimplementedAuthServiceServer() {}
func (UnimplementedAuthServiceServer) testEmbeddedByValue() {}
// UnsafeAuthServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to AuthServiceServer will
// result in compilation errors.
type UnsafeAuthServiceServer interface {
mustEmbedUnimplementedAuthServiceServer()
}
func RegisterAuthServiceServer(s grpc.ServiceRegistrar, srv AuthServiceServer) {
// If the following call panics, it indicates UnimplementedAuthServiceServer 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(&AuthService_ServiceDesc, srv)
}
func _AuthService_Login_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(LoginRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AuthServiceServer).Login(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AuthService_Login_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AuthServiceServer).Login(ctx, req.(*LoginRequest))
}
return interceptor(ctx, in, info, handler)
}
func _AuthService_Logout_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(LogoutRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AuthServiceServer).Logout(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AuthService_Logout_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AuthServiceServer).Logout(ctx, req.(*LogoutRequest))
}
return interceptor(ctx, in, info, handler)
}
// AuthService_ServiceDesc is the grpc.ServiceDesc for AuthService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var AuthService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "mcns.v1.AuthService",
HandlerType: (*AuthServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "Login",
Handler: _AuthService_Login_Handler,
},
{
MethodName: "Logout",
Handler: _AuthService_Logout_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "proto/mcns/v1/auth.proto",
}

618
gen/mcns/v1/record.pb.go Normal file
View File

@@ -0,0 +1,618 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc v6.32.1
// source: proto/mcns/v1/record.proto
package v1
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
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)
)
type Record struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
Zone string `protobuf:"bytes,2,opt,name=zone,proto3" json:"zone,omitempty"`
Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"`
Type string `protobuf:"bytes,4,opt,name=type,proto3" json:"type,omitempty"`
Value string `protobuf:"bytes,5,opt,name=value,proto3" json:"value,omitempty"`
Ttl int32 `protobuf:"varint,6,opt,name=ttl,proto3" json:"ttl,omitempty"`
CreatedAt *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Record) Reset() {
*x = Record{}
mi := &file_proto_mcns_v1_record_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Record) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Record) ProtoMessage() {}
func (x *Record) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcns_v1_record_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 Record.ProtoReflect.Descriptor instead.
func (*Record) Descriptor() ([]byte, []int) {
return file_proto_mcns_v1_record_proto_rawDescGZIP(), []int{0}
}
func (x *Record) GetId() int64 {
if x != nil {
return x.Id
}
return 0
}
func (x *Record) GetZone() string {
if x != nil {
return x.Zone
}
return ""
}
func (x *Record) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *Record) GetType() string {
if x != nil {
return x.Type
}
return ""
}
func (x *Record) GetValue() string {
if x != nil {
return x.Value
}
return ""
}
func (x *Record) GetTtl() int32 {
if x != nil {
return x.Ttl
}
return 0
}
func (x *Record) GetCreatedAt() *timestamppb.Timestamp {
if x != nil {
return x.CreatedAt
}
return nil
}
func (x *Record) GetUpdatedAt() *timestamppb.Timestamp {
if x != nil {
return x.UpdatedAt
}
return nil
}
type ListRecordsRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Zone string `protobuf:"bytes,1,opt,name=zone,proto3" json:"zone,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
Type string `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListRecordsRequest) Reset() {
*x = ListRecordsRequest{}
mi := &file_proto_mcns_v1_record_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListRecordsRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListRecordsRequest) ProtoMessage() {}
func (x *ListRecordsRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcns_v1_record_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 ListRecordsRequest.ProtoReflect.Descriptor instead.
func (*ListRecordsRequest) Descriptor() ([]byte, []int) {
return file_proto_mcns_v1_record_proto_rawDescGZIP(), []int{1}
}
func (x *ListRecordsRequest) GetZone() string {
if x != nil {
return x.Zone
}
return ""
}
func (x *ListRecordsRequest) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *ListRecordsRequest) GetType() string {
if x != nil {
return x.Type
}
return ""
}
type ListRecordsResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Records []*Record `protobuf:"bytes,1,rep,name=records,proto3" json:"records,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListRecordsResponse) Reset() {
*x = ListRecordsResponse{}
mi := &file_proto_mcns_v1_record_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListRecordsResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListRecordsResponse) ProtoMessage() {}
func (x *ListRecordsResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcns_v1_record_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 ListRecordsResponse.ProtoReflect.Descriptor instead.
func (*ListRecordsResponse) Descriptor() ([]byte, []int) {
return file_proto_mcns_v1_record_proto_rawDescGZIP(), []int{2}
}
func (x *ListRecordsResponse) GetRecords() []*Record {
if x != nil {
return x.Records
}
return nil
}
type CreateRecordRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Zone string `protobuf:"bytes,1,opt,name=zone,proto3" json:"zone,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
Type string `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"`
Value string `protobuf:"bytes,4,opt,name=value,proto3" json:"value,omitempty"`
Ttl int32 `protobuf:"varint,5,opt,name=ttl,proto3" json:"ttl,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *CreateRecordRequest) Reset() {
*x = CreateRecordRequest{}
mi := &file_proto_mcns_v1_record_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *CreateRecordRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CreateRecordRequest) ProtoMessage() {}
func (x *CreateRecordRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcns_v1_record_proto_msgTypes[3]
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 CreateRecordRequest.ProtoReflect.Descriptor instead.
func (*CreateRecordRequest) Descriptor() ([]byte, []int) {
return file_proto_mcns_v1_record_proto_rawDescGZIP(), []int{3}
}
func (x *CreateRecordRequest) GetZone() string {
if x != nil {
return x.Zone
}
return ""
}
func (x *CreateRecordRequest) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *CreateRecordRequest) GetType() string {
if x != nil {
return x.Type
}
return ""
}
func (x *CreateRecordRequest) GetValue() string {
if x != nil {
return x.Value
}
return ""
}
func (x *CreateRecordRequest) GetTtl() int32 {
if x != nil {
return x.Ttl
}
return 0
}
type GetRecordRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetRecordRequest) Reset() {
*x = GetRecordRequest{}
mi := &file_proto_mcns_v1_record_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetRecordRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetRecordRequest) ProtoMessage() {}
func (x *GetRecordRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcns_v1_record_proto_msgTypes[4]
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 GetRecordRequest.ProtoReflect.Descriptor instead.
func (*GetRecordRequest) Descriptor() ([]byte, []int) {
return file_proto_mcns_v1_record_proto_rawDescGZIP(), []int{4}
}
func (x *GetRecordRequest) GetId() int64 {
if x != nil {
return x.Id
}
return 0
}
type UpdateRecordRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
Type string `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"`
Value string `protobuf:"bytes,4,opt,name=value,proto3" json:"value,omitempty"`
Ttl int32 `protobuf:"varint,5,opt,name=ttl,proto3" json:"ttl,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *UpdateRecordRequest) Reset() {
*x = UpdateRecordRequest{}
mi := &file_proto_mcns_v1_record_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *UpdateRecordRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*UpdateRecordRequest) ProtoMessage() {}
func (x *UpdateRecordRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcns_v1_record_proto_msgTypes[5]
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 UpdateRecordRequest.ProtoReflect.Descriptor instead.
func (*UpdateRecordRequest) Descriptor() ([]byte, []int) {
return file_proto_mcns_v1_record_proto_rawDescGZIP(), []int{5}
}
func (x *UpdateRecordRequest) GetId() int64 {
if x != nil {
return x.Id
}
return 0
}
func (x *UpdateRecordRequest) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *UpdateRecordRequest) GetType() string {
if x != nil {
return x.Type
}
return ""
}
func (x *UpdateRecordRequest) GetValue() string {
if x != nil {
return x.Value
}
return ""
}
func (x *UpdateRecordRequest) GetTtl() int32 {
if x != nil {
return x.Ttl
}
return 0
}
type DeleteRecordRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DeleteRecordRequest) Reset() {
*x = DeleteRecordRequest{}
mi := &file_proto_mcns_v1_record_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DeleteRecordRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DeleteRecordRequest) ProtoMessage() {}
func (x *DeleteRecordRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcns_v1_record_proto_msgTypes[6]
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 DeleteRecordRequest.ProtoReflect.Descriptor instead.
func (*DeleteRecordRequest) Descriptor() ([]byte, []int) {
return file_proto_mcns_v1_record_proto_rawDescGZIP(), []int{6}
}
func (x *DeleteRecordRequest) GetId() int64 {
if x != nil {
return x.Id
}
return 0
}
type DeleteRecordResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DeleteRecordResponse) Reset() {
*x = DeleteRecordResponse{}
mi := &file_proto_mcns_v1_record_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DeleteRecordResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DeleteRecordResponse) ProtoMessage() {}
func (x *DeleteRecordResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcns_v1_record_proto_msgTypes[7]
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 DeleteRecordResponse.ProtoReflect.Descriptor instead.
func (*DeleteRecordResponse) Descriptor() ([]byte, []int) {
return file_proto_mcns_v1_record_proto_rawDescGZIP(), []int{7}
}
var File_proto_mcns_v1_record_proto protoreflect.FileDescriptor
const file_proto_mcns_v1_record_proto_rawDesc = "" +
"\n" +
"\x1aproto/mcns/v1/record.proto\x12\amcns.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"\xf2\x01\n" +
"\x06Record\x12\x0e\n" +
"\x02id\x18\x01 \x01(\x03R\x02id\x12\x12\n" +
"\x04zone\x18\x02 \x01(\tR\x04zone\x12\x12\n" +
"\x04name\x18\x03 \x01(\tR\x04name\x12\x12\n" +
"\x04type\x18\x04 \x01(\tR\x04type\x12\x14\n" +
"\x05value\x18\x05 \x01(\tR\x05value\x12\x10\n" +
"\x03ttl\x18\x06 \x01(\x05R\x03ttl\x129\n" +
"\n" +
"created_at\x18\a \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x129\n" +
"\n" +
"updated_at\x18\b \x01(\v2\x1a.google.protobuf.TimestampR\tupdatedAt\"P\n" +
"\x12ListRecordsRequest\x12\x12\n" +
"\x04zone\x18\x01 \x01(\tR\x04zone\x12\x12\n" +
"\x04name\x18\x02 \x01(\tR\x04name\x12\x12\n" +
"\x04type\x18\x03 \x01(\tR\x04type\"@\n" +
"\x13ListRecordsResponse\x12)\n" +
"\arecords\x18\x01 \x03(\v2\x0f.mcns.v1.RecordR\arecords\"y\n" +
"\x13CreateRecordRequest\x12\x12\n" +
"\x04zone\x18\x01 \x01(\tR\x04zone\x12\x12\n" +
"\x04name\x18\x02 \x01(\tR\x04name\x12\x12\n" +
"\x04type\x18\x03 \x01(\tR\x04type\x12\x14\n" +
"\x05value\x18\x04 \x01(\tR\x05value\x12\x10\n" +
"\x03ttl\x18\x05 \x01(\x05R\x03ttl\"\"\n" +
"\x10GetRecordRequest\x12\x0e\n" +
"\x02id\x18\x01 \x01(\x03R\x02id\"u\n" +
"\x13UpdateRecordRequest\x12\x0e\n" +
"\x02id\x18\x01 \x01(\x03R\x02id\x12\x12\n" +
"\x04name\x18\x02 \x01(\tR\x04name\x12\x12\n" +
"\x04type\x18\x03 \x01(\tR\x04type\x12\x14\n" +
"\x05value\x18\x04 \x01(\tR\x05value\x12\x10\n" +
"\x03ttl\x18\x05 \x01(\x05R\x03ttl\"%\n" +
"\x13DeleteRecordRequest\x12\x0e\n" +
"\x02id\x18\x01 \x01(\x03R\x02id\"\x16\n" +
"\x14DeleteRecordResponse2\xdd\x02\n" +
"\rRecordService\x12H\n" +
"\vListRecords\x12\x1b.mcns.v1.ListRecordsRequest\x1a\x1c.mcns.v1.ListRecordsResponse\x12=\n" +
"\fCreateRecord\x12\x1c.mcns.v1.CreateRecordRequest\x1a\x0f.mcns.v1.Record\x127\n" +
"\tGetRecord\x12\x19.mcns.v1.GetRecordRequest\x1a\x0f.mcns.v1.Record\x12=\n" +
"\fUpdateRecord\x12\x1c.mcns.v1.UpdateRecordRequest\x1a\x0f.mcns.v1.Record\x12K\n" +
"\fDeleteRecord\x12\x1c.mcns.v1.DeleteRecordRequest\x1a\x1d.mcns.v1.DeleteRecordResponseB(Z&git.wntrmute.dev/kyle/mcns/gen/mcns/v1b\x06proto3"
var (
file_proto_mcns_v1_record_proto_rawDescOnce sync.Once
file_proto_mcns_v1_record_proto_rawDescData []byte
)
func file_proto_mcns_v1_record_proto_rawDescGZIP() []byte {
file_proto_mcns_v1_record_proto_rawDescOnce.Do(func() {
file_proto_mcns_v1_record_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proto_mcns_v1_record_proto_rawDesc), len(file_proto_mcns_v1_record_proto_rawDesc)))
})
return file_proto_mcns_v1_record_proto_rawDescData
}
var file_proto_mcns_v1_record_proto_msgTypes = make([]protoimpl.MessageInfo, 8)
var file_proto_mcns_v1_record_proto_goTypes = []any{
(*Record)(nil), // 0: mcns.v1.Record
(*ListRecordsRequest)(nil), // 1: mcns.v1.ListRecordsRequest
(*ListRecordsResponse)(nil), // 2: mcns.v1.ListRecordsResponse
(*CreateRecordRequest)(nil), // 3: mcns.v1.CreateRecordRequest
(*GetRecordRequest)(nil), // 4: mcns.v1.GetRecordRequest
(*UpdateRecordRequest)(nil), // 5: mcns.v1.UpdateRecordRequest
(*DeleteRecordRequest)(nil), // 6: mcns.v1.DeleteRecordRequest
(*DeleteRecordResponse)(nil), // 7: mcns.v1.DeleteRecordResponse
(*timestamppb.Timestamp)(nil), // 8: google.protobuf.Timestamp
}
var file_proto_mcns_v1_record_proto_depIdxs = []int32{
8, // 0: mcns.v1.Record.created_at:type_name -> google.protobuf.Timestamp
8, // 1: mcns.v1.Record.updated_at:type_name -> google.protobuf.Timestamp
0, // 2: mcns.v1.ListRecordsResponse.records:type_name -> mcns.v1.Record
1, // 3: mcns.v1.RecordService.ListRecords:input_type -> mcns.v1.ListRecordsRequest
3, // 4: mcns.v1.RecordService.CreateRecord:input_type -> mcns.v1.CreateRecordRequest
4, // 5: mcns.v1.RecordService.GetRecord:input_type -> mcns.v1.GetRecordRequest
5, // 6: mcns.v1.RecordService.UpdateRecord:input_type -> mcns.v1.UpdateRecordRequest
6, // 7: mcns.v1.RecordService.DeleteRecord:input_type -> mcns.v1.DeleteRecordRequest
2, // 8: mcns.v1.RecordService.ListRecords:output_type -> mcns.v1.ListRecordsResponse
0, // 9: mcns.v1.RecordService.CreateRecord:output_type -> mcns.v1.Record
0, // 10: mcns.v1.RecordService.GetRecord:output_type -> mcns.v1.Record
0, // 11: mcns.v1.RecordService.UpdateRecord:output_type -> mcns.v1.Record
7, // 12: mcns.v1.RecordService.DeleteRecord:output_type -> mcns.v1.DeleteRecordResponse
8, // [8:13] is the sub-list for method output_type
3, // [3:8] is the sub-list for method input_type
3, // [3:3] is the sub-list for extension type_name
3, // [3:3] is the sub-list for extension extendee
0, // [0:3] is the sub-list for field type_name
}
func init() { file_proto_mcns_v1_record_proto_init() }
func file_proto_mcns_v1_record_proto_init() {
if File_proto_mcns_v1_record_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_mcns_v1_record_proto_rawDesc), len(file_proto_mcns_v1_record_proto_rawDesc)),
NumEnums: 0,
NumMessages: 8,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_proto_mcns_v1_record_proto_goTypes,
DependencyIndexes: file_proto_mcns_v1_record_proto_depIdxs,
MessageInfos: file_proto_mcns_v1_record_proto_msgTypes,
}.Build()
File_proto_mcns_v1_record_proto = out.File
file_proto_mcns_v1_record_proto_goTypes = nil
file_proto_mcns_v1_record_proto_depIdxs = nil
}

View File

@@ -0,0 +1,273 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.6.1
// - protoc v6.32.1
// source: proto/mcns/v1/record.proto
package v1
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// 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 (
RecordService_ListRecords_FullMethodName = "/mcns.v1.RecordService/ListRecords"
RecordService_CreateRecord_FullMethodName = "/mcns.v1.RecordService/CreateRecord"
RecordService_GetRecord_FullMethodName = "/mcns.v1.RecordService/GetRecord"
RecordService_UpdateRecord_FullMethodName = "/mcns.v1.RecordService/UpdateRecord"
RecordService_DeleteRecord_FullMethodName = "/mcns.v1.RecordService/DeleteRecord"
)
// RecordServiceClient is the client API for RecordService 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.
type RecordServiceClient interface {
ListRecords(ctx context.Context, in *ListRecordsRequest, opts ...grpc.CallOption) (*ListRecordsResponse, error)
CreateRecord(ctx context.Context, in *CreateRecordRequest, opts ...grpc.CallOption) (*Record, error)
GetRecord(ctx context.Context, in *GetRecordRequest, opts ...grpc.CallOption) (*Record, error)
UpdateRecord(ctx context.Context, in *UpdateRecordRequest, opts ...grpc.CallOption) (*Record, error)
DeleteRecord(ctx context.Context, in *DeleteRecordRequest, opts ...grpc.CallOption) (*DeleteRecordResponse, error)
}
type recordServiceClient struct {
cc grpc.ClientConnInterface
}
func NewRecordServiceClient(cc grpc.ClientConnInterface) RecordServiceClient {
return &recordServiceClient{cc}
}
func (c *recordServiceClient) ListRecords(ctx context.Context, in *ListRecordsRequest, opts ...grpc.CallOption) (*ListRecordsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListRecordsResponse)
err := c.cc.Invoke(ctx, RecordService_ListRecords_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *recordServiceClient) CreateRecord(ctx context.Context, in *CreateRecordRequest, opts ...grpc.CallOption) (*Record, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(Record)
err := c.cc.Invoke(ctx, RecordService_CreateRecord_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *recordServiceClient) GetRecord(ctx context.Context, in *GetRecordRequest, opts ...grpc.CallOption) (*Record, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(Record)
err := c.cc.Invoke(ctx, RecordService_GetRecord_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *recordServiceClient) UpdateRecord(ctx context.Context, in *UpdateRecordRequest, opts ...grpc.CallOption) (*Record, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(Record)
err := c.cc.Invoke(ctx, RecordService_UpdateRecord_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *recordServiceClient) DeleteRecord(ctx context.Context, in *DeleteRecordRequest, opts ...grpc.CallOption) (*DeleteRecordResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(DeleteRecordResponse)
err := c.cc.Invoke(ctx, RecordService_DeleteRecord_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// RecordServiceServer is the server API for RecordService service.
// All implementations must embed UnimplementedRecordServiceServer
// for forward compatibility.
type RecordServiceServer interface {
ListRecords(context.Context, *ListRecordsRequest) (*ListRecordsResponse, error)
CreateRecord(context.Context, *CreateRecordRequest) (*Record, error)
GetRecord(context.Context, *GetRecordRequest) (*Record, error)
UpdateRecord(context.Context, *UpdateRecordRequest) (*Record, error)
DeleteRecord(context.Context, *DeleteRecordRequest) (*DeleteRecordResponse, error)
mustEmbedUnimplementedRecordServiceServer()
}
// UnimplementedRecordServiceServer 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 UnimplementedRecordServiceServer struct{}
func (UnimplementedRecordServiceServer) ListRecords(context.Context, *ListRecordsRequest) (*ListRecordsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListRecords not implemented")
}
func (UnimplementedRecordServiceServer) CreateRecord(context.Context, *CreateRecordRequest) (*Record, error) {
return nil, status.Error(codes.Unimplemented, "method CreateRecord not implemented")
}
func (UnimplementedRecordServiceServer) GetRecord(context.Context, *GetRecordRequest) (*Record, error) {
return nil, status.Error(codes.Unimplemented, "method GetRecord not implemented")
}
func (UnimplementedRecordServiceServer) UpdateRecord(context.Context, *UpdateRecordRequest) (*Record, error) {
return nil, status.Error(codes.Unimplemented, "method UpdateRecord not implemented")
}
func (UnimplementedRecordServiceServer) DeleteRecord(context.Context, *DeleteRecordRequest) (*DeleteRecordResponse, error) {
return nil, status.Error(codes.Unimplemented, "method DeleteRecord not implemented")
}
func (UnimplementedRecordServiceServer) mustEmbedUnimplementedRecordServiceServer() {}
func (UnimplementedRecordServiceServer) testEmbeddedByValue() {}
// UnsafeRecordServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to RecordServiceServer will
// result in compilation errors.
type UnsafeRecordServiceServer interface {
mustEmbedUnimplementedRecordServiceServer()
}
func RegisterRecordServiceServer(s grpc.ServiceRegistrar, srv RecordServiceServer) {
// If the following call panics, it indicates UnimplementedRecordServiceServer 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(&RecordService_ServiceDesc, srv)
}
func _RecordService_ListRecords_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListRecordsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(RecordServiceServer).ListRecords(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: RecordService_ListRecords_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(RecordServiceServer).ListRecords(ctx, req.(*ListRecordsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _RecordService_CreateRecord_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CreateRecordRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(RecordServiceServer).CreateRecord(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: RecordService_CreateRecord_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(RecordServiceServer).CreateRecord(ctx, req.(*CreateRecordRequest))
}
return interceptor(ctx, in, info, handler)
}
func _RecordService_GetRecord_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetRecordRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(RecordServiceServer).GetRecord(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: RecordService_GetRecord_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(RecordServiceServer).GetRecord(ctx, req.(*GetRecordRequest))
}
return interceptor(ctx, in, info, handler)
}
func _RecordService_UpdateRecord_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(UpdateRecordRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(RecordServiceServer).UpdateRecord(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: RecordService_UpdateRecord_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(RecordServiceServer).UpdateRecord(ctx, req.(*UpdateRecordRequest))
}
return interceptor(ctx, in, info, handler)
}
func _RecordService_DeleteRecord_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(DeleteRecordRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(RecordServiceServer).DeleteRecord(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: RecordService_DeleteRecord_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(RecordServiceServer).DeleteRecord(ctx, req.(*DeleteRecordRequest))
}
return interceptor(ctx, in, info, handler)
}
// RecordService_ServiceDesc is the grpc.ServiceDesc for RecordService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var RecordService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "mcns.v1.RecordService",
HandlerType: (*RecordServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "ListRecords",
Handler: _RecordService_ListRecords_Handler,
},
{
MethodName: "CreateRecord",
Handler: _RecordService_CreateRecord_Handler,
},
{
MethodName: "GetRecord",
Handler: _RecordService_GetRecord_Handler,
},
{
MethodName: "UpdateRecord",
Handler: _RecordService_UpdateRecord_Handler,
},
{
MethodName: "DeleteRecord",
Handler: _RecordService_DeleteRecord_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "proto/mcns/v1/record.proto",
}

667
gen/mcns/v1/zone.pb.go Normal file
View File

@@ -0,0 +1,667 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc v6.32.1
// source: proto/mcns/v1/zone.proto
package v1
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
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)
)
type Zone struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
PrimaryNs string `protobuf:"bytes,3,opt,name=primary_ns,json=primaryNs,proto3" json:"primary_ns,omitempty"`
AdminEmail string `protobuf:"bytes,4,opt,name=admin_email,json=adminEmail,proto3" json:"admin_email,omitempty"`
Refresh int32 `protobuf:"varint,5,opt,name=refresh,proto3" json:"refresh,omitempty"`
Retry int32 `protobuf:"varint,6,opt,name=retry,proto3" json:"retry,omitempty"`
Expire int32 `protobuf:"varint,7,opt,name=expire,proto3" json:"expire,omitempty"`
MinimumTtl int32 `protobuf:"varint,8,opt,name=minimum_ttl,json=minimumTtl,proto3" json:"minimum_ttl,omitempty"`
Serial int64 `protobuf:"varint,9,opt,name=serial,proto3" json:"serial,omitempty"`
CreatedAt *timestamppb.Timestamp `protobuf:"bytes,10,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
UpdatedAt *timestamppb.Timestamp `protobuf:"bytes,11,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Zone) Reset() {
*x = Zone{}
mi := &file_proto_mcns_v1_zone_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Zone) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Zone) ProtoMessage() {}
func (x *Zone) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcns_v1_zone_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 Zone.ProtoReflect.Descriptor instead.
func (*Zone) Descriptor() ([]byte, []int) {
return file_proto_mcns_v1_zone_proto_rawDescGZIP(), []int{0}
}
func (x *Zone) GetId() int64 {
if x != nil {
return x.Id
}
return 0
}
func (x *Zone) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *Zone) GetPrimaryNs() string {
if x != nil {
return x.PrimaryNs
}
return ""
}
func (x *Zone) GetAdminEmail() string {
if x != nil {
return x.AdminEmail
}
return ""
}
func (x *Zone) GetRefresh() int32 {
if x != nil {
return x.Refresh
}
return 0
}
func (x *Zone) GetRetry() int32 {
if x != nil {
return x.Retry
}
return 0
}
func (x *Zone) GetExpire() int32 {
if x != nil {
return x.Expire
}
return 0
}
func (x *Zone) GetMinimumTtl() int32 {
if x != nil {
return x.MinimumTtl
}
return 0
}
func (x *Zone) GetSerial() int64 {
if x != nil {
return x.Serial
}
return 0
}
func (x *Zone) GetCreatedAt() *timestamppb.Timestamp {
if x != nil {
return x.CreatedAt
}
return nil
}
func (x *Zone) GetUpdatedAt() *timestamppb.Timestamp {
if x != nil {
return x.UpdatedAt
}
return nil
}
type ListZonesRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListZonesRequest) Reset() {
*x = ListZonesRequest{}
mi := &file_proto_mcns_v1_zone_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListZonesRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListZonesRequest) ProtoMessage() {}
func (x *ListZonesRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcns_v1_zone_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 ListZonesRequest.ProtoReflect.Descriptor instead.
func (*ListZonesRequest) Descriptor() ([]byte, []int) {
return file_proto_mcns_v1_zone_proto_rawDescGZIP(), []int{1}
}
type ListZonesResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Zones []*Zone `protobuf:"bytes,1,rep,name=zones,proto3" json:"zones,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListZonesResponse) Reset() {
*x = ListZonesResponse{}
mi := &file_proto_mcns_v1_zone_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListZonesResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListZonesResponse) ProtoMessage() {}
func (x *ListZonesResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcns_v1_zone_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 ListZonesResponse.ProtoReflect.Descriptor instead.
func (*ListZonesResponse) Descriptor() ([]byte, []int) {
return file_proto_mcns_v1_zone_proto_rawDescGZIP(), []int{2}
}
func (x *ListZonesResponse) GetZones() []*Zone {
if x != nil {
return x.Zones
}
return nil
}
type CreateZoneRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
PrimaryNs string `protobuf:"bytes,2,opt,name=primary_ns,json=primaryNs,proto3" json:"primary_ns,omitempty"`
AdminEmail string `protobuf:"bytes,3,opt,name=admin_email,json=adminEmail,proto3" json:"admin_email,omitempty"`
Refresh int32 `protobuf:"varint,4,opt,name=refresh,proto3" json:"refresh,omitempty"`
Retry int32 `protobuf:"varint,5,opt,name=retry,proto3" json:"retry,omitempty"`
Expire int32 `protobuf:"varint,6,opt,name=expire,proto3" json:"expire,omitempty"`
MinimumTtl int32 `protobuf:"varint,7,opt,name=minimum_ttl,json=minimumTtl,proto3" json:"minimum_ttl,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *CreateZoneRequest) Reset() {
*x = CreateZoneRequest{}
mi := &file_proto_mcns_v1_zone_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *CreateZoneRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CreateZoneRequest) ProtoMessage() {}
func (x *CreateZoneRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcns_v1_zone_proto_msgTypes[3]
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 CreateZoneRequest.ProtoReflect.Descriptor instead.
func (*CreateZoneRequest) Descriptor() ([]byte, []int) {
return file_proto_mcns_v1_zone_proto_rawDescGZIP(), []int{3}
}
func (x *CreateZoneRequest) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *CreateZoneRequest) GetPrimaryNs() string {
if x != nil {
return x.PrimaryNs
}
return ""
}
func (x *CreateZoneRequest) GetAdminEmail() string {
if x != nil {
return x.AdminEmail
}
return ""
}
func (x *CreateZoneRequest) GetRefresh() int32 {
if x != nil {
return x.Refresh
}
return 0
}
func (x *CreateZoneRequest) GetRetry() int32 {
if x != nil {
return x.Retry
}
return 0
}
func (x *CreateZoneRequest) GetExpire() int32 {
if x != nil {
return x.Expire
}
return 0
}
func (x *CreateZoneRequest) GetMinimumTtl() int32 {
if x != nil {
return x.MinimumTtl
}
return 0
}
type GetZoneRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetZoneRequest) Reset() {
*x = GetZoneRequest{}
mi := &file_proto_mcns_v1_zone_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetZoneRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetZoneRequest) ProtoMessage() {}
func (x *GetZoneRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcns_v1_zone_proto_msgTypes[4]
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 GetZoneRequest.ProtoReflect.Descriptor instead.
func (*GetZoneRequest) Descriptor() ([]byte, []int) {
return file_proto_mcns_v1_zone_proto_rawDescGZIP(), []int{4}
}
func (x *GetZoneRequest) GetName() string {
if x != nil {
return x.Name
}
return ""
}
type UpdateZoneRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
PrimaryNs string `protobuf:"bytes,2,opt,name=primary_ns,json=primaryNs,proto3" json:"primary_ns,omitempty"`
AdminEmail string `protobuf:"bytes,3,opt,name=admin_email,json=adminEmail,proto3" json:"admin_email,omitempty"`
Refresh int32 `protobuf:"varint,4,opt,name=refresh,proto3" json:"refresh,omitempty"`
Retry int32 `protobuf:"varint,5,opt,name=retry,proto3" json:"retry,omitempty"`
Expire int32 `protobuf:"varint,6,opt,name=expire,proto3" json:"expire,omitempty"`
MinimumTtl int32 `protobuf:"varint,7,opt,name=minimum_ttl,json=minimumTtl,proto3" json:"minimum_ttl,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *UpdateZoneRequest) Reset() {
*x = UpdateZoneRequest{}
mi := &file_proto_mcns_v1_zone_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *UpdateZoneRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*UpdateZoneRequest) ProtoMessage() {}
func (x *UpdateZoneRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcns_v1_zone_proto_msgTypes[5]
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 UpdateZoneRequest.ProtoReflect.Descriptor instead.
func (*UpdateZoneRequest) Descriptor() ([]byte, []int) {
return file_proto_mcns_v1_zone_proto_rawDescGZIP(), []int{5}
}
func (x *UpdateZoneRequest) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *UpdateZoneRequest) GetPrimaryNs() string {
if x != nil {
return x.PrimaryNs
}
return ""
}
func (x *UpdateZoneRequest) GetAdminEmail() string {
if x != nil {
return x.AdminEmail
}
return ""
}
func (x *UpdateZoneRequest) GetRefresh() int32 {
if x != nil {
return x.Refresh
}
return 0
}
func (x *UpdateZoneRequest) GetRetry() int32 {
if x != nil {
return x.Retry
}
return 0
}
func (x *UpdateZoneRequest) GetExpire() int32 {
if x != nil {
return x.Expire
}
return 0
}
func (x *UpdateZoneRequest) GetMinimumTtl() int32 {
if x != nil {
return x.MinimumTtl
}
return 0
}
type DeleteZoneRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DeleteZoneRequest) Reset() {
*x = DeleteZoneRequest{}
mi := &file_proto_mcns_v1_zone_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DeleteZoneRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DeleteZoneRequest) ProtoMessage() {}
func (x *DeleteZoneRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcns_v1_zone_proto_msgTypes[6]
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 DeleteZoneRequest.ProtoReflect.Descriptor instead.
func (*DeleteZoneRequest) Descriptor() ([]byte, []int) {
return file_proto_mcns_v1_zone_proto_rawDescGZIP(), []int{6}
}
func (x *DeleteZoneRequest) GetName() string {
if x != nil {
return x.Name
}
return ""
}
type DeleteZoneResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DeleteZoneResponse) Reset() {
*x = DeleteZoneResponse{}
mi := &file_proto_mcns_v1_zone_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DeleteZoneResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DeleteZoneResponse) ProtoMessage() {}
func (x *DeleteZoneResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_mcns_v1_zone_proto_msgTypes[7]
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 DeleteZoneResponse.ProtoReflect.Descriptor instead.
func (*DeleteZoneResponse) Descriptor() ([]byte, []int) {
return file_proto_mcns_v1_zone_proto_rawDescGZIP(), []int{7}
}
var File_proto_mcns_v1_zone_proto protoreflect.FileDescriptor
const file_proto_mcns_v1_zone_proto_rawDesc = "" +
"\n" +
"\x18proto/mcns/v1/zone.proto\x12\amcns.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"\xe1\x02\n" +
"\x04Zone\x12\x0e\n" +
"\x02id\x18\x01 \x01(\x03R\x02id\x12\x12\n" +
"\x04name\x18\x02 \x01(\tR\x04name\x12\x1d\n" +
"\n" +
"primary_ns\x18\x03 \x01(\tR\tprimaryNs\x12\x1f\n" +
"\vadmin_email\x18\x04 \x01(\tR\n" +
"adminEmail\x12\x18\n" +
"\arefresh\x18\x05 \x01(\x05R\arefresh\x12\x14\n" +
"\x05retry\x18\x06 \x01(\x05R\x05retry\x12\x16\n" +
"\x06expire\x18\a \x01(\x05R\x06expire\x12\x1f\n" +
"\vminimum_ttl\x18\b \x01(\x05R\n" +
"minimumTtl\x12\x16\n" +
"\x06serial\x18\t \x01(\x03R\x06serial\x129\n" +
"\n" +
"created_at\x18\n" +
" \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x129\n" +
"\n" +
"updated_at\x18\v \x01(\v2\x1a.google.protobuf.TimestampR\tupdatedAt\"\x12\n" +
"\x10ListZonesRequest\"8\n" +
"\x11ListZonesResponse\x12#\n" +
"\x05zones\x18\x01 \x03(\v2\r.mcns.v1.ZoneR\x05zones\"\xd0\x01\n" +
"\x11CreateZoneRequest\x12\x12\n" +
"\x04name\x18\x01 \x01(\tR\x04name\x12\x1d\n" +
"\n" +
"primary_ns\x18\x02 \x01(\tR\tprimaryNs\x12\x1f\n" +
"\vadmin_email\x18\x03 \x01(\tR\n" +
"adminEmail\x12\x18\n" +
"\arefresh\x18\x04 \x01(\x05R\arefresh\x12\x14\n" +
"\x05retry\x18\x05 \x01(\x05R\x05retry\x12\x16\n" +
"\x06expire\x18\x06 \x01(\x05R\x06expire\x12\x1f\n" +
"\vminimum_ttl\x18\a \x01(\x05R\n" +
"minimumTtl\"$\n" +
"\x0eGetZoneRequest\x12\x12\n" +
"\x04name\x18\x01 \x01(\tR\x04name\"\xd0\x01\n" +
"\x11UpdateZoneRequest\x12\x12\n" +
"\x04name\x18\x01 \x01(\tR\x04name\x12\x1d\n" +
"\n" +
"primary_ns\x18\x02 \x01(\tR\tprimaryNs\x12\x1f\n" +
"\vadmin_email\x18\x03 \x01(\tR\n" +
"adminEmail\x12\x18\n" +
"\arefresh\x18\x04 \x01(\x05R\arefresh\x12\x14\n" +
"\x05retry\x18\x05 \x01(\x05R\x05retry\x12\x16\n" +
"\x06expire\x18\x06 \x01(\x05R\x06expire\x12\x1f\n" +
"\vminimum_ttl\x18\a \x01(\x05R\n" +
"minimumTtl\"'\n" +
"\x11DeleteZoneRequest\x12\x12\n" +
"\x04name\x18\x01 \x01(\tR\x04name\"\x14\n" +
"\x12DeleteZoneResponse2\xbd\x02\n" +
"\vZoneService\x12B\n" +
"\tListZones\x12\x19.mcns.v1.ListZonesRequest\x1a\x1a.mcns.v1.ListZonesResponse\x127\n" +
"\n" +
"CreateZone\x12\x1a.mcns.v1.CreateZoneRequest\x1a\r.mcns.v1.Zone\x121\n" +
"\aGetZone\x12\x17.mcns.v1.GetZoneRequest\x1a\r.mcns.v1.Zone\x127\n" +
"\n" +
"UpdateZone\x12\x1a.mcns.v1.UpdateZoneRequest\x1a\r.mcns.v1.Zone\x12E\n" +
"\n" +
"DeleteZone\x12\x1a.mcns.v1.DeleteZoneRequest\x1a\x1b.mcns.v1.DeleteZoneResponseB(Z&git.wntrmute.dev/kyle/mcns/gen/mcns/v1b\x06proto3"
var (
file_proto_mcns_v1_zone_proto_rawDescOnce sync.Once
file_proto_mcns_v1_zone_proto_rawDescData []byte
)
func file_proto_mcns_v1_zone_proto_rawDescGZIP() []byte {
file_proto_mcns_v1_zone_proto_rawDescOnce.Do(func() {
file_proto_mcns_v1_zone_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proto_mcns_v1_zone_proto_rawDesc), len(file_proto_mcns_v1_zone_proto_rawDesc)))
})
return file_proto_mcns_v1_zone_proto_rawDescData
}
var file_proto_mcns_v1_zone_proto_msgTypes = make([]protoimpl.MessageInfo, 8)
var file_proto_mcns_v1_zone_proto_goTypes = []any{
(*Zone)(nil), // 0: mcns.v1.Zone
(*ListZonesRequest)(nil), // 1: mcns.v1.ListZonesRequest
(*ListZonesResponse)(nil), // 2: mcns.v1.ListZonesResponse
(*CreateZoneRequest)(nil), // 3: mcns.v1.CreateZoneRequest
(*GetZoneRequest)(nil), // 4: mcns.v1.GetZoneRequest
(*UpdateZoneRequest)(nil), // 5: mcns.v1.UpdateZoneRequest
(*DeleteZoneRequest)(nil), // 6: mcns.v1.DeleteZoneRequest
(*DeleteZoneResponse)(nil), // 7: mcns.v1.DeleteZoneResponse
(*timestamppb.Timestamp)(nil), // 8: google.protobuf.Timestamp
}
var file_proto_mcns_v1_zone_proto_depIdxs = []int32{
8, // 0: mcns.v1.Zone.created_at:type_name -> google.protobuf.Timestamp
8, // 1: mcns.v1.Zone.updated_at:type_name -> google.protobuf.Timestamp
0, // 2: mcns.v1.ListZonesResponse.zones:type_name -> mcns.v1.Zone
1, // 3: mcns.v1.ZoneService.ListZones:input_type -> mcns.v1.ListZonesRequest
3, // 4: mcns.v1.ZoneService.CreateZone:input_type -> mcns.v1.CreateZoneRequest
4, // 5: mcns.v1.ZoneService.GetZone:input_type -> mcns.v1.GetZoneRequest
5, // 6: mcns.v1.ZoneService.UpdateZone:input_type -> mcns.v1.UpdateZoneRequest
6, // 7: mcns.v1.ZoneService.DeleteZone:input_type -> mcns.v1.DeleteZoneRequest
2, // 8: mcns.v1.ZoneService.ListZones:output_type -> mcns.v1.ListZonesResponse
0, // 9: mcns.v1.ZoneService.CreateZone:output_type -> mcns.v1.Zone
0, // 10: mcns.v1.ZoneService.GetZone:output_type -> mcns.v1.Zone
0, // 11: mcns.v1.ZoneService.UpdateZone:output_type -> mcns.v1.Zone
7, // 12: mcns.v1.ZoneService.DeleteZone:output_type -> mcns.v1.DeleteZoneResponse
8, // [8:13] is the sub-list for method output_type
3, // [3:8] is the sub-list for method input_type
3, // [3:3] is the sub-list for extension type_name
3, // [3:3] is the sub-list for extension extendee
0, // [0:3] is the sub-list for field type_name
}
func init() { file_proto_mcns_v1_zone_proto_init() }
func file_proto_mcns_v1_zone_proto_init() {
if File_proto_mcns_v1_zone_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_mcns_v1_zone_proto_rawDesc), len(file_proto_mcns_v1_zone_proto_rawDesc)),
NumEnums: 0,
NumMessages: 8,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_proto_mcns_v1_zone_proto_goTypes,
DependencyIndexes: file_proto_mcns_v1_zone_proto_depIdxs,
MessageInfos: file_proto_mcns_v1_zone_proto_msgTypes,
}.Build()
File_proto_mcns_v1_zone_proto = out.File
file_proto_mcns_v1_zone_proto_goTypes = nil
file_proto_mcns_v1_zone_proto_depIdxs = nil
}

273
gen/mcns/v1/zone_grpc.pb.go Normal file
View File

@@ -0,0 +1,273 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.6.1
// - protoc v6.32.1
// source: proto/mcns/v1/zone.proto
package v1
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// 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 (
ZoneService_ListZones_FullMethodName = "/mcns.v1.ZoneService/ListZones"
ZoneService_CreateZone_FullMethodName = "/mcns.v1.ZoneService/CreateZone"
ZoneService_GetZone_FullMethodName = "/mcns.v1.ZoneService/GetZone"
ZoneService_UpdateZone_FullMethodName = "/mcns.v1.ZoneService/UpdateZone"
ZoneService_DeleteZone_FullMethodName = "/mcns.v1.ZoneService/DeleteZone"
)
// ZoneServiceClient is the client API for ZoneService 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.
type ZoneServiceClient interface {
ListZones(ctx context.Context, in *ListZonesRequest, opts ...grpc.CallOption) (*ListZonesResponse, error)
CreateZone(ctx context.Context, in *CreateZoneRequest, opts ...grpc.CallOption) (*Zone, error)
GetZone(ctx context.Context, in *GetZoneRequest, opts ...grpc.CallOption) (*Zone, error)
UpdateZone(ctx context.Context, in *UpdateZoneRequest, opts ...grpc.CallOption) (*Zone, error)
DeleteZone(ctx context.Context, in *DeleteZoneRequest, opts ...grpc.CallOption) (*DeleteZoneResponse, error)
}
type zoneServiceClient struct {
cc grpc.ClientConnInterface
}
func NewZoneServiceClient(cc grpc.ClientConnInterface) ZoneServiceClient {
return &zoneServiceClient{cc}
}
func (c *zoneServiceClient) ListZones(ctx context.Context, in *ListZonesRequest, opts ...grpc.CallOption) (*ListZonesResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListZonesResponse)
err := c.cc.Invoke(ctx, ZoneService_ListZones_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *zoneServiceClient) CreateZone(ctx context.Context, in *CreateZoneRequest, opts ...grpc.CallOption) (*Zone, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(Zone)
err := c.cc.Invoke(ctx, ZoneService_CreateZone_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *zoneServiceClient) GetZone(ctx context.Context, in *GetZoneRequest, opts ...grpc.CallOption) (*Zone, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(Zone)
err := c.cc.Invoke(ctx, ZoneService_GetZone_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *zoneServiceClient) UpdateZone(ctx context.Context, in *UpdateZoneRequest, opts ...grpc.CallOption) (*Zone, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(Zone)
err := c.cc.Invoke(ctx, ZoneService_UpdateZone_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *zoneServiceClient) DeleteZone(ctx context.Context, in *DeleteZoneRequest, opts ...grpc.CallOption) (*DeleteZoneResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(DeleteZoneResponse)
err := c.cc.Invoke(ctx, ZoneService_DeleteZone_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// ZoneServiceServer is the server API for ZoneService service.
// All implementations must embed UnimplementedZoneServiceServer
// for forward compatibility.
type ZoneServiceServer interface {
ListZones(context.Context, *ListZonesRequest) (*ListZonesResponse, error)
CreateZone(context.Context, *CreateZoneRequest) (*Zone, error)
GetZone(context.Context, *GetZoneRequest) (*Zone, error)
UpdateZone(context.Context, *UpdateZoneRequest) (*Zone, error)
DeleteZone(context.Context, *DeleteZoneRequest) (*DeleteZoneResponse, error)
mustEmbedUnimplementedZoneServiceServer()
}
// UnimplementedZoneServiceServer 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 UnimplementedZoneServiceServer struct{}
func (UnimplementedZoneServiceServer) ListZones(context.Context, *ListZonesRequest) (*ListZonesResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListZones not implemented")
}
func (UnimplementedZoneServiceServer) CreateZone(context.Context, *CreateZoneRequest) (*Zone, error) {
return nil, status.Error(codes.Unimplemented, "method CreateZone not implemented")
}
func (UnimplementedZoneServiceServer) GetZone(context.Context, *GetZoneRequest) (*Zone, error) {
return nil, status.Error(codes.Unimplemented, "method GetZone not implemented")
}
func (UnimplementedZoneServiceServer) UpdateZone(context.Context, *UpdateZoneRequest) (*Zone, error) {
return nil, status.Error(codes.Unimplemented, "method UpdateZone not implemented")
}
func (UnimplementedZoneServiceServer) DeleteZone(context.Context, *DeleteZoneRequest) (*DeleteZoneResponse, error) {
return nil, status.Error(codes.Unimplemented, "method DeleteZone not implemented")
}
func (UnimplementedZoneServiceServer) mustEmbedUnimplementedZoneServiceServer() {}
func (UnimplementedZoneServiceServer) testEmbeddedByValue() {}
// UnsafeZoneServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to ZoneServiceServer will
// result in compilation errors.
type UnsafeZoneServiceServer interface {
mustEmbedUnimplementedZoneServiceServer()
}
func RegisterZoneServiceServer(s grpc.ServiceRegistrar, srv ZoneServiceServer) {
// If the following call panics, it indicates UnimplementedZoneServiceServer 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(&ZoneService_ServiceDesc, srv)
}
func _ZoneService_ListZones_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListZonesRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ZoneServiceServer).ListZones(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: ZoneService_ListZones_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ZoneServiceServer).ListZones(ctx, req.(*ListZonesRequest))
}
return interceptor(ctx, in, info, handler)
}
func _ZoneService_CreateZone_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CreateZoneRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ZoneServiceServer).CreateZone(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: ZoneService_CreateZone_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ZoneServiceServer).CreateZone(ctx, req.(*CreateZoneRequest))
}
return interceptor(ctx, in, info, handler)
}
func _ZoneService_GetZone_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetZoneRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ZoneServiceServer).GetZone(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: ZoneService_GetZone_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ZoneServiceServer).GetZone(ctx, req.(*GetZoneRequest))
}
return interceptor(ctx, in, info, handler)
}
func _ZoneService_UpdateZone_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(UpdateZoneRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ZoneServiceServer).UpdateZone(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: ZoneService_UpdateZone_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ZoneServiceServer).UpdateZone(ctx, req.(*UpdateZoneRequest))
}
return interceptor(ctx, in, info, handler)
}
func _ZoneService_DeleteZone_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(DeleteZoneRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ZoneServiceServer).DeleteZone(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: ZoneService_DeleteZone_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ZoneServiceServer).DeleteZone(ctx, req.(*DeleteZoneRequest))
}
return interceptor(ctx, in, info, handler)
}
// ZoneService_ServiceDesc is the grpc.ServiceDesc for ZoneService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var ZoneService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "mcns.v1.ZoneService",
HandlerType: (*ZoneServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "ListZones",
Handler: _ZoneService_ListZones_Handler,
},
{
MethodName: "CreateZone",
Handler: _ZoneService_CreateZone_Handler,
},
{
MethodName: "GetZone",
Handler: _ZoneService_GetZone_Handler,
},
{
MethodName: "UpdateZone",
Handler: _ZoneService_UpdateZone_Handler,
},
{
MethodName: "DeleteZone",
Handler: _ZoneService_DeleteZone_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "proto/mcns/v1/zone.proto",
}

34
go.mod Normal file
View File

@@ -0,0 +1,34 @@
module git.wntrmute.dev/kyle/mcns
go 1.25.7
require (
git.wntrmute.dev/kyle/mcdsl v1.0.0
github.com/go-chi/chi/v5 v5.2.5
github.com/miekg/dns v1.1.66
github.com/spf13/cobra v1.10.2
google.golang.org/grpc v1.79.3
google.golang.org/protobuf v1.36.11
)
require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/pelletier/go-toml/v2 v2.3.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/spf13/pflag v1.0.9 // indirect
golang.org/x/mod v0.33.0 // indirect
golang.org/x/net v0.50.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.34.0 // indirect
golang.org/x/tools v0.42.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
modernc.org/libc v1.70.0 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.47.0 // indirect
)

103
go.sum Normal file
View File

@@ -0,0 +1,103 @@
git.wntrmute.dev/kyle/mcdsl v1.0.0 h1:YB7dx4gdNYKKcVySpL6UkwHqdCJ9Nl1yS0+eHk0hNtk=
git.wntrmute.dev/kyle/mcdsl v1.0.0/go.mod h1:wo0tGfUAxci3XnOe4/rFmR0RjUElKdYUazc+Np986sg=
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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
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/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
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/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE=
github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM=
github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
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.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
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=
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw=
modernc.org/ccgo/v4 v4.32.0/go.mod h1:6F08EBCx5uQc38kMGl+0Nm0oWczoo1c7cgpzEry7Uc0=
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw=
modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.47.0 h1:R1XyaNpoW4Et9yly+I2EeX7pBza/w+pmYee/0HJDyKk=
modernc.org/sqlite v1.47.0/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=

49
internal/config/config.go Normal file
View File

@@ -0,0 +1,49 @@
package config
import (
"fmt"
mcdslconfig "git.wntrmute.dev/kyle/mcdsl/config"
)
// Config is the top-level MCNS configuration.
type Config struct {
mcdslconfig.Base
DNS DNSConfig `toml:"dns"`
}
// DNSConfig holds the DNS server settings.
type DNSConfig struct {
ListenAddr string `toml:"listen_addr"`
Upstreams []string `toml:"upstreams"`
}
// Load reads a TOML config file, applies environment variable overrides
// (MCNS_ prefix), sets defaults, and validates required fields.
func Load(path string) (*Config, error) {
cfg, err := mcdslconfig.Load[Config](path, "MCNS")
if err != nil {
return nil, err
}
// Apply DNS defaults.
if cfg.DNS.ListenAddr == "" {
cfg.DNS.ListenAddr = ":53"
}
if len(cfg.DNS.Upstreams) == 0 {
cfg.DNS.Upstreams = []string{"1.1.1.1:53", "8.8.8.8:53"}
}
return cfg, nil
}
// Validate implements the mcdsl config.Validator interface.
func (c *Config) Validate() error {
if c.Database.Path == "" {
return fmt.Errorf("database.path is required")
}
if c.MCIAS.ServerURL == "" {
return fmt.Errorf("mcias.server_url is required")
}
return nil
}

23
internal/db/db.go Normal file
View File

@@ -0,0 +1,23 @@
package db
import (
"database/sql"
"fmt"
mcdsldb "git.wntrmute.dev/kyle/mcdsl/db"
)
// DB wraps a SQLite database connection.
type DB struct {
*sql.DB
}
// Open opens (or creates) a SQLite database at the given path with the
// standard Metacircular pragmas: WAL mode, foreign keys, busy timeout.
func Open(path string) (*DB, error) {
sqlDB, err := mcdsldb.Open(path)
if err != nil {
return nil, fmt.Errorf("db: %w", err)
}
return &DB{sqlDB}, nil
}

46
internal/db/migrate.go Normal file
View File

@@ -0,0 +1,46 @@
package db
import (
mcdsldb "git.wntrmute.dev/kyle/mcdsl/db"
)
// Migrations is the ordered list of MCNS schema migrations.
var Migrations = []mcdsldb.Migration{
{
Version: 1,
Name: "zones and records",
SQL: `
CREATE TABLE IF NOT EXISTS zones (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
primary_ns TEXT NOT NULL,
admin_email TEXT NOT NULL,
refresh INTEGER NOT NULL DEFAULT 3600,
retry INTEGER NOT NULL DEFAULT 600,
expire INTEGER NOT NULL DEFAULT 86400,
minimum_ttl INTEGER NOT NULL DEFAULT 300,
serial INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
);
CREATE TABLE IF NOT EXISTS records (
id INTEGER PRIMARY KEY,
zone_id INTEGER NOT NULL REFERENCES zones(id) ON DELETE CASCADE,
name TEXT NOT NULL,
type TEXT NOT NULL CHECK (type IN ('A', 'AAAA', 'CNAME')),
value TEXT NOT NULL,
ttl INTEGER NOT NULL DEFAULT 300,
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
UNIQUE(zone_id, name, type, value)
);
CREATE INDEX IF NOT EXISTS idx_records_zone_name ON records(zone_id, name);`,
},
}
// Migrate applies all pending migrations.
func (d *DB) Migrate() error {
return mcdsldb.Migrate(d.DB, Migrations)
}

308
internal/db/records.go Normal file
View File

@@ -0,0 +1,308 @@
package db
import (
"database/sql"
"errors"
"fmt"
"net"
"strings"
"time"
)
// Record represents a DNS record stored in the database.
type Record struct {
ID int64
ZoneID int64
ZoneName string
Name string
Type string
Value string
TTL int
CreatedAt string
UpdatedAt string
}
// ListRecords returns records for a zone, optionally filtered by name and type.
func (d *DB) ListRecords(zoneName, name, recordType string) ([]Record, error) {
zoneName = strings.ToLower(strings.TrimSuffix(zoneName, "."))
zone, err := d.GetZone(zoneName)
if err != nil {
return nil, err
}
query := `SELECT r.id, r.zone_id, z.name, r.name, r.type, r.value, r.ttl, r.created_at, r.updated_at
FROM records r JOIN zones z ON r.zone_id = z.id WHERE r.zone_id = ?`
args := []any{zone.ID}
if name != "" {
query += ` AND r.name = ?`
args = append(args, strings.ToLower(name))
}
if recordType != "" {
query += ` AND r.type = ?`
args = append(args, strings.ToUpper(recordType))
}
query += ` ORDER BY r.name, r.type, r.value`
rows, err := d.Query(query, args...)
if err != nil {
return nil, fmt.Errorf("list records: %w", err)
}
defer rows.Close()
var records []Record
for rows.Next() {
var r Record
if err := rows.Scan(&r.ID, &r.ZoneID, &r.ZoneName, &r.Name, &r.Type, &r.Value, &r.TTL, &r.CreatedAt, &r.UpdatedAt); err != nil {
return nil, fmt.Errorf("scan record: %w", err)
}
records = append(records, r)
}
return records, rows.Err()
}
// LookupRecords returns records matching a name and type within a zone.
// Used by the DNS handler for query resolution.
func (d *DB) LookupRecords(zoneName, name, recordType string) ([]Record, error) {
zoneName = strings.ToLower(strings.TrimSuffix(zoneName, "."))
name = strings.ToLower(name)
rows, err := d.Query(`SELECT r.id, r.zone_id, z.name, r.name, r.type, r.value, r.ttl, r.created_at, r.updated_at
FROM records r JOIN zones z ON r.zone_id = z.id
WHERE z.name = ? AND r.name = ? AND r.type = ?`, zoneName, name, strings.ToUpper(recordType))
if err != nil {
return nil, fmt.Errorf("lookup records: %w", err)
}
defer rows.Close()
var records []Record
for rows.Next() {
var r Record
if err := rows.Scan(&r.ID, &r.ZoneID, &r.ZoneName, &r.Name, &r.Type, &r.Value, &r.TTL, &r.CreatedAt, &r.UpdatedAt); err != nil {
return nil, fmt.Errorf("scan record: %w", err)
}
records = append(records, r)
}
return records, rows.Err()
}
// LookupCNAME returns CNAME records for a name within a zone.
func (d *DB) LookupCNAME(zoneName, name string) ([]Record, error) {
return d.LookupRecords(zoneName, name, "CNAME")
}
// HasRecordsForName checks if any records of the given types exist for a name.
func (d *DB) HasRecordsForName(tx *sql.Tx, zoneID int64, name string, types []string) (bool, error) {
placeholders := make([]string, len(types))
args := []any{zoneID, strings.ToLower(name)}
for i, t := range types {
placeholders[i] = "?"
args = append(args, strings.ToUpper(t))
}
query := fmt.Sprintf(`SELECT COUNT(*) FROM records WHERE zone_id = ? AND name = ? AND type IN (%s)`, strings.Join(placeholders, ","))
var count int
if err := tx.QueryRow(query, args...).Scan(&count); err != nil {
return false, err
}
return count > 0, nil
}
// GetRecord returns a record by ID.
func (d *DB) GetRecord(id int64) (*Record, error) {
var r Record
err := d.QueryRow(`SELECT r.id, r.zone_id, z.name, r.name, r.type, r.value, r.ttl, r.created_at, r.updated_at
FROM records r JOIN zones z ON r.zone_id = z.id WHERE r.id = ?`, id).
Scan(&r.ID, &r.ZoneID, &r.ZoneName, &r.Name, &r.Type, &r.Value, &r.TTL, &r.CreatedAt, &r.UpdatedAt)
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
if err != nil {
return nil, fmt.Errorf("get record: %w", err)
}
return &r, nil
}
// CreateRecord inserts a new record, enforcing CNAME exclusivity and
// value validation. Bumps the zone serial within the same transaction.
func (d *DB) CreateRecord(zoneName, name, recordType, value string, ttl int) (*Record, error) {
zoneName = strings.ToLower(strings.TrimSuffix(zoneName, "."))
name = strings.ToLower(name)
recordType = strings.ToUpper(recordType)
if err := validateRecordValue(recordType, value); err != nil {
return nil, err
}
zone, err := d.GetZone(zoneName)
if err != nil {
return nil, err
}
if ttl <= 0 {
ttl = 300
}
tx, err := d.Begin()
if err != nil {
return nil, fmt.Errorf("begin tx: %w", err)
}
defer func() { _ = tx.Rollback() }()
// Enforce CNAME exclusivity.
if recordType == "CNAME" {
hasAddr, err := d.HasRecordsForName(tx, zone.ID, name, []string{"A", "AAAA"})
if err != nil {
return nil, fmt.Errorf("check cname exclusivity: %w", err)
}
if hasAddr {
return nil, fmt.Errorf("%w: CNAME record conflicts with existing A/AAAA record for %q", ErrConflict, name)
}
} else if recordType == "A" || recordType == "AAAA" {
hasCNAME, err := d.HasRecordsForName(tx, zone.ID, name, []string{"CNAME"})
if err != nil {
return nil, fmt.Errorf("check cname exclusivity: %w", err)
}
if hasCNAME {
return nil, fmt.Errorf("%w: A/AAAA record conflicts with existing CNAME record for %q", ErrConflict, name)
}
}
res, err := tx.Exec(`INSERT INTO records (zone_id, name, type, value, ttl) VALUES (?, ?, ?, ?, ?)`,
zone.ID, name, recordType, value, ttl)
if err != nil {
if strings.Contains(err.Error(), "UNIQUE constraint") {
return nil, fmt.Errorf("%w: record already exists", ErrConflict)
}
return nil, fmt.Errorf("insert record: %w", err)
}
recordID, err := res.LastInsertId()
if err != nil {
return nil, fmt.Errorf("last insert id: %w", err)
}
if err := d.BumpSerial(tx, zone.ID); err != nil {
return nil, fmt.Errorf("bump serial: %w", err)
}
if err := tx.Commit(); err != nil {
return nil, fmt.Errorf("commit: %w", err)
}
return d.GetRecord(recordID)
}
// UpdateRecord updates an existing record's fields and bumps the zone serial.
func (d *DB) UpdateRecord(id int64, name, recordType, value string, ttl int) (*Record, error) {
name = strings.ToLower(name)
recordType = strings.ToUpper(recordType)
if err := validateRecordValue(recordType, value); err != nil {
return nil, err
}
existing, err := d.GetRecord(id)
if err != nil {
return nil, err
}
if ttl <= 0 {
ttl = 300
}
tx, err := d.Begin()
if err != nil {
return nil, fmt.Errorf("begin tx: %w", err)
}
defer func() { _ = tx.Rollback() }()
// Enforce CNAME exclusivity for the new type/name combo.
if recordType == "CNAME" {
hasAddr, err := d.HasRecordsForName(tx, existing.ZoneID, name, []string{"A", "AAAA"})
if err != nil {
return nil, fmt.Errorf("check cname exclusivity: %w", err)
}
if hasAddr {
return nil, fmt.Errorf("%w: CNAME record conflicts with existing A/AAAA record for %q", ErrConflict, name)
}
} else if recordType == "A" || recordType == "AAAA" {
hasCNAME, err := d.HasRecordsForName(tx, existing.ZoneID, name, []string{"CNAME"})
if err != nil {
return nil, fmt.Errorf("check cname exclusivity: %w", err)
}
if hasCNAME {
return nil, fmt.Errorf("%w: A/AAAA record conflicts with existing CNAME record for %q", ErrConflict, name)
}
}
now := time.Now().UTC().Format("2006-01-02T15:04:05Z")
_, err = tx.Exec(`UPDATE records SET name = ?, type = ?, value = ?, ttl = ?, updated_at = ? WHERE id = ?`,
name, recordType, value, ttl, now, id)
if err != nil {
if strings.Contains(err.Error(), "UNIQUE constraint") {
return nil, fmt.Errorf("%w: record already exists", ErrConflict)
}
return nil, fmt.Errorf("update record: %w", err)
}
if err := d.BumpSerial(tx, existing.ZoneID); err != nil {
return nil, fmt.Errorf("bump serial: %w", err)
}
if err := tx.Commit(); err != nil {
return nil, fmt.Errorf("commit: %w", err)
}
return d.GetRecord(id)
}
// DeleteRecord deletes a record and bumps the zone serial.
func (d *DB) DeleteRecord(id int64) error {
existing, err := d.GetRecord(id)
if err != nil {
return err
}
tx, err := d.Begin()
if err != nil {
return fmt.Errorf("begin tx: %w", err)
}
defer func() { _ = tx.Rollback() }()
_, err = tx.Exec(`DELETE FROM records WHERE id = ?`, id)
if err != nil {
return fmt.Errorf("delete record: %w", err)
}
if err := d.BumpSerial(tx, existing.ZoneID); err != nil {
return fmt.Errorf("bump serial: %w", err)
}
return tx.Commit()
}
// validateRecordValue checks that a record value is valid for its type.
func validateRecordValue(recordType, value string) error {
switch recordType {
case "A":
ip := net.ParseIP(value)
if ip == nil || ip.To4() == nil {
return fmt.Errorf("invalid IPv4 address: %q", value)
}
case "AAAA":
ip := net.ParseIP(value)
if ip == nil || ip.To4() != nil {
return fmt.Errorf("invalid IPv6 address: %q", value)
}
case "CNAME":
if !strings.HasSuffix(value, ".") {
return fmt.Errorf("CNAME value must be a fully-qualified domain name ending with '.': %q", value)
}
default:
return fmt.Errorf("unsupported record type: %q", recordType)
}
return nil
}

289
internal/db/records_test.go Normal file
View File

@@ -0,0 +1,289 @@
package db
import (
"errors"
"testing"
)
func createTestZone(t *testing.T, db *DB) *Zone {
t.Helper()
zone, err := db.CreateZone("svc.mcp.metacircular.net", "ns.mcp.metacircular.net.", "admin.metacircular.net.", 3600, 600, 86400, 300)
if err != nil {
t.Fatalf("create zone: %v", err)
}
return zone
}
func TestCreateRecordA(t *testing.T) {
db := openTestDB(t)
createTestZone(t, db)
record, err := db.CreateRecord("svc.mcp.metacircular.net", "metacrypt", "A", "192.168.88.181", 300)
if err != nil {
t.Fatalf("create record: %v", err)
}
if record.Name != "metacrypt" {
t.Fatalf("got name %q, want %q", record.Name, "metacrypt")
}
if record.Type != "A" {
t.Fatalf("got type %q, want %q", record.Type, "A")
}
if record.Value != "192.168.88.181" {
t.Fatalf("got value %q, want %q", record.Value, "192.168.88.181")
}
}
func TestCreateRecordAAAA(t *testing.T) {
db := openTestDB(t)
createTestZone(t, db)
record, err := db.CreateRecord("svc.mcp.metacircular.net", "metacrypt", "AAAA", "2001:db8::1", 300)
if err != nil {
t.Fatalf("create record: %v", err)
}
if record.Type != "AAAA" {
t.Fatalf("got type %q, want %q", record.Type, "AAAA")
}
}
func TestCreateRecordCNAME(t *testing.T) {
db := openTestDB(t)
createTestZone(t, db)
record, err := db.CreateRecord("svc.mcp.metacircular.net", "alias", "CNAME", "rift.mcp.metacircular.net.", 300)
if err != nil {
t.Fatalf("create record: %v", err)
}
if record.Type != "CNAME" {
t.Fatalf("got type %q, want %q", record.Type, "CNAME")
}
}
func TestCreateRecordInvalidIP(t *testing.T) {
db := openTestDB(t)
createTestZone(t, db)
_, err := db.CreateRecord("svc.mcp.metacircular.net", "bad", "A", "not-an-ip", 300)
if err == nil {
t.Fatal("expected error for invalid IPv4")
}
}
func TestCreateRecordCNAMEExclusivity(t *testing.T) {
db := openTestDB(t)
createTestZone(t, db)
// Create an A record first.
_, err := db.CreateRecord("svc.mcp.metacircular.net", "metacrypt", "A", "192.168.88.181", 300)
if err != nil {
t.Fatalf("create A record: %v", err)
}
// Trying to add a CNAME for the same name should fail.
_, err = db.CreateRecord("svc.mcp.metacircular.net", "metacrypt", "CNAME", "rift.mcp.metacircular.net.", 300)
if !errors.Is(err, ErrConflict) {
t.Fatalf("expected ErrConflict, got %v", err)
}
}
func TestCreateRecordCNAMEExclusivityReverse(t *testing.T) {
db := openTestDB(t)
createTestZone(t, db)
// Create a CNAME record first.
_, err := db.CreateRecord("svc.mcp.metacircular.net", "alias", "CNAME", "rift.mcp.metacircular.net.", 300)
if err != nil {
t.Fatalf("create CNAME record: %v", err)
}
// Trying to add an A record for the same name should fail.
_, err = db.CreateRecord("svc.mcp.metacircular.net", "alias", "A", "192.168.88.181", 300)
if !errors.Is(err, ErrConflict) {
t.Fatalf("expected ErrConflict, got %v", err)
}
}
func TestCreateRecordBumpsSerial(t *testing.T) {
db := openTestDB(t)
zone := createTestZone(t, db)
originalSerial := zone.Serial
_, err := db.CreateRecord("svc.mcp.metacircular.net", "metacrypt", "A", "192.168.88.181", 300)
if err != nil {
t.Fatalf("create record: %v", err)
}
updated, err := db.GetZone("svc.mcp.metacircular.net")
if err != nil {
t.Fatalf("get zone: %v", err)
}
if updated.Serial <= originalSerial {
t.Fatalf("serial should have bumped: %d <= %d", updated.Serial, originalSerial)
}
}
func TestListRecords(t *testing.T) {
db := openTestDB(t)
createTestZone(t, db)
_, err := db.CreateRecord("svc.mcp.metacircular.net", "metacrypt", "A", "192.168.88.181", 300)
if err != nil {
t.Fatalf("create record 1: %v", err)
}
_, err = db.CreateRecord("svc.mcp.metacircular.net", "metacrypt", "A", "100.95.252.120", 300)
if err != nil {
t.Fatalf("create record 2: %v", err)
}
_, err = db.CreateRecord("svc.mcp.metacircular.net", "mcr", "A", "192.168.88.181", 300)
if err != nil {
t.Fatalf("create record 3: %v", err)
}
// List all records.
records, err := db.ListRecords("svc.mcp.metacircular.net", "", "")
if err != nil {
t.Fatalf("list records: %v", err)
}
if len(records) != 3 {
t.Fatalf("got %d records, want 3", len(records))
}
// Filter by name.
records, err = db.ListRecords("svc.mcp.metacircular.net", "metacrypt", "")
if err != nil {
t.Fatalf("list records by name: %v", err)
}
if len(records) != 2 {
t.Fatalf("got %d records, want 2", len(records))
}
// Filter by type.
records, err = db.ListRecords("svc.mcp.metacircular.net", "", "A")
if err != nil {
t.Fatalf("list records by type: %v", err)
}
if len(records) != 3 {
t.Fatalf("got %d records, want 3", len(records))
}
}
func TestUpdateRecord(t *testing.T) {
db := openTestDB(t)
createTestZone(t, db)
record, err := db.CreateRecord("svc.mcp.metacircular.net", "metacrypt", "A", "192.168.88.181", 300)
if err != nil {
t.Fatalf("create record: %v", err)
}
updated, err := db.UpdateRecord(record.ID, "metacrypt", "A", "10.0.0.1", 600)
if err != nil {
t.Fatalf("update record: %v", err)
}
if updated.Value != "10.0.0.1" {
t.Fatalf("got value %q, want %q", updated.Value, "10.0.0.1")
}
if updated.TTL != 600 {
t.Fatalf("got ttl %d, want 600", updated.TTL)
}
}
func TestDeleteRecord(t *testing.T) {
db := openTestDB(t)
createTestZone(t, db)
record, err := db.CreateRecord("svc.mcp.metacircular.net", "metacrypt", "A", "192.168.88.181", 300)
if err != nil {
t.Fatalf("create record: %v", err)
}
if err := db.DeleteRecord(record.ID); err != nil {
t.Fatalf("delete record: %v", err)
}
_, err = db.GetRecord(record.ID)
if !errors.Is(err, ErrNotFound) {
t.Fatalf("expected ErrNotFound after delete, got %v", err)
}
}
func TestDeleteRecordBumpsSerial(t *testing.T) {
db := openTestDB(t)
createTestZone(t, db)
record, err := db.CreateRecord("svc.mcp.metacircular.net", "metacrypt", "A", "192.168.88.181", 300)
if err != nil {
t.Fatalf("create record: %v", err)
}
zone, err := db.GetZone("svc.mcp.metacircular.net")
if err != nil {
t.Fatalf("get zone: %v", err)
}
serialBefore := zone.Serial
if err := db.DeleteRecord(record.ID); err != nil {
t.Fatalf("delete record: %v", err)
}
zone, err = db.GetZone("svc.mcp.metacircular.net")
if err != nil {
t.Fatalf("get zone after delete: %v", err)
}
if zone.Serial <= serialBefore {
t.Fatalf("serial should have bumped: %d <= %d", zone.Serial, serialBefore)
}
}
func TestLookupRecords(t *testing.T) {
db := openTestDB(t)
createTestZone(t, db)
_, err := db.CreateRecord("svc.mcp.metacircular.net", "metacrypt", "A", "192.168.88.181", 300)
if err != nil {
t.Fatalf("create record: %v", err)
}
records, err := db.LookupRecords("svc.mcp.metacircular.net", "metacrypt", "A")
if err != nil {
t.Fatalf("lookup records: %v", err)
}
if len(records) != 1 {
t.Fatalf("got %d records, want 1", len(records))
}
if records[0].Value != "192.168.88.181" {
t.Fatalf("got value %q, want %q", records[0].Value, "192.168.88.181")
}
}
func TestCreateRecordCNAMEMissingDot(t *testing.T) {
db := openTestDB(t)
createTestZone(t, db)
_, err := db.CreateRecord("svc.mcp.metacircular.net", "alias", "CNAME", "rift.mcp.metacircular.net", 300)
if err == nil {
t.Fatal("expected error for CNAME without trailing dot")
}
}
func TestMultipleARecords(t *testing.T) {
db := openTestDB(t)
createTestZone(t, db)
_, err := db.CreateRecord("svc.mcp.metacircular.net", "metacrypt", "A", "192.168.88.181", 300)
if err != nil {
t.Fatalf("create first A record: %v", err)
}
_, err = db.CreateRecord("svc.mcp.metacircular.net", "metacrypt", "A", "100.95.252.120", 300)
if err != nil {
t.Fatalf("create second A record: %v", err)
}
records, err := db.LookupRecords("svc.mcp.metacircular.net", "metacrypt", "A")
if err != nil {
t.Fatalf("lookup records: %v", err)
}
if len(records) != 2 {
t.Fatalf("got %d records, want 2", len(records))
}
}

182
internal/db/zones.go Normal file
View File

@@ -0,0 +1,182 @@
package db
import (
"database/sql"
"errors"
"fmt"
"strconv"
"strings"
"time"
)
// Zone represents a DNS zone stored in the database.
type Zone struct {
ID int64
Name string
PrimaryNS string
AdminEmail string
Refresh int
Retry int
Expire int
MinimumTTL int
Serial int64
CreatedAt string
UpdatedAt string
}
// ErrNotFound is returned when a requested resource does not exist.
var ErrNotFound = errors.New("not found")
// ErrConflict is returned when a write conflicts with existing data.
var ErrConflict = errors.New("conflict")
// ListZones returns all zones ordered by name.
func (d *DB) ListZones() ([]Zone, error) {
rows, err := d.Query(`SELECT id, name, primary_ns, admin_email, refresh, retry, expire, minimum_ttl, serial, created_at, updated_at FROM zones ORDER BY name`)
if err != nil {
return nil, fmt.Errorf("list zones: %w", err)
}
defer rows.Close()
var zones []Zone
for rows.Next() {
var z Zone
if err := rows.Scan(&z.ID, &z.Name, &z.PrimaryNS, &z.AdminEmail, &z.Refresh, &z.Retry, &z.Expire, &z.MinimumTTL, &z.Serial, &z.CreatedAt, &z.UpdatedAt); err != nil {
return nil, fmt.Errorf("scan zone: %w", err)
}
zones = append(zones, z)
}
return zones, rows.Err()
}
// GetZone returns a zone by name (case-insensitive).
func (d *DB) GetZone(name string) (*Zone, error) {
name = strings.ToLower(strings.TrimSuffix(name, "."))
var z Zone
err := d.QueryRow(`SELECT id, name, primary_ns, admin_email, refresh, retry, expire, minimum_ttl, serial, created_at, updated_at FROM zones WHERE name = ?`, name).
Scan(&z.ID, &z.Name, &z.PrimaryNS, &z.AdminEmail, &z.Refresh, &z.Retry, &z.Expire, &z.MinimumTTL, &z.Serial, &z.CreatedAt, &z.UpdatedAt)
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
if err != nil {
return nil, fmt.Errorf("get zone: %w", err)
}
return &z, nil
}
// GetZoneByID returns a zone by ID.
func (d *DB) GetZoneByID(id int64) (*Zone, error) {
var z Zone
err := d.QueryRow(`SELECT id, name, primary_ns, admin_email, refresh, retry, expire, minimum_ttl, serial, created_at, updated_at FROM zones WHERE id = ?`, id).
Scan(&z.ID, &z.Name, &z.PrimaryNS, &z.AdminEmail, &z.Refresh, &z.Retry, &z.Expire, &z.MinimumTTL, &z.Serial, &z.CreatedAt, &z.UpdatedAt)
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
if err != nil {
return nil, fmt.Errorf("get zone by id: %w", err)
}
return &z, nil
}
// CreateZone inserts a new zone and returns it with the generated serial.
func (d *DB) CreateZone(name, primaryNS, adminEmail string, refresh, retry, expire, minimumTTL int) (*Zone, error) {
name = strings.ToLower(strings.TrimSuffix(name, "."))
serial := nextSerial(0)
res, err := d.Exec(`INSERT INTO zones (name, primary_ns, admin_email, refresh, retry, expire, minimum_ttl, serial) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
name, primaryNS, adminEmail, refresh, retry, expire, minimumTTL, serial)
if err != nil {
if strings.Contains(err.Error(), "UNIQUE constraint") {
return nil, fmt.Errorf("%w: zone %q already exists", ErrConflict, name)
}
return nil, fmt.Errorf("create zone: %w", err)
}
id, err := res.LastInsertId()
if err != nil {
return nil, fmt.Errorf("create zone: last insert id: %w", err)
}
return d.GetZoneByID(id)
}
// UpdateZone updates a zone's SOA parameters and bumps the serial.
func (d *DB) UpdateZone(name, primaryNS, adminEmail string, refresh, retry, expire, minimumTTL int) (*Zone, error) {
name = strings.ToLower(strings.TrimSuffix(name, "."))
zone, err := d.GetZone(name)
if err != nil {
return nil, err
}
serial := nextSerial(zone.Serial)
now := time.Now().UTC().Format("2006-01-02T15:04:05Z")
_, err = d.Exec(`UPDATE zones SET primary_ns = ?, admin_email = ?, refresh = ?, retry = ?, expire = ?, minimum_ttl = ?, serial = ?, updated_at = ? WHERE id = ?`,
primaryNS, adminEmail, refresh, retry, expire, minimumTTL, serial, now, zone.ID)
if err != nil {
return nil, fmt.Errorf("update zone: %w", err)
}
return d.GetZoneByID(zone.ID)
}
// DeleteZone deletes a zone and all its records.
func (d *DB) DeleteZone(name string) error {
name = strings.ToLower(strings.TrimSuffix(name, "."))
res, err := d.Exec(`DELETE FROM zones WHERE name = ?`, name)
if err != nil {
return fmt.Errorf("delete zone: %w", err)
}
n, err := res.RowsAffected()
if err != nil {
return fmt.Errorf("delete zone: rows affected: %w", err)
}
if n == 0 {
return ErrNotFound
}
return nil
}
// BumpSerial increments the serial for a zone within a transaction.
func (d *DB) BumpSerial(tx *sql.Tx, zoneID int64) error {
var current int64
if err := tx.QueryRow(`SELECT serial FROM zones WHERE id = ?`, zoneID).Scan(&current); err != nil {
return fmt.Errorf("read serial: %w", err)
}
serial := nextSerial(current)
now := time.Now().UTC().Format("2006-01-02T15:04:05Z")
_, err := tx.Exec(`UPDATE zones SET serial = ?, updated_at = ? WHERE id = ?`, serial, now, zoneID)
return err
}
// ZoneNames returns all zone names for the DNS handler.
func (d *DB) ZoneNames() ([]string, error) {
rows, err := d.Query(`SELECT name FROM zones ORDER BY name`)
if err != nil {
return nil, err
}
defer rows.Close()
var names []string
for rows.Next() {
var name string
if err := rows.Scan(&name); err != nil {
return nil, err
}
names = append(names, name)
}
return names, rows.Err()
}
// nextSerial computes the next SOA serial in YYYYMMDDNN format.
func nextSerial(current int64) int64 {
today := time.Now().UTC()
datePrefix, _ := strconv.ParseInt(today.Format("20060102"), 10, 64)
datePrefix *= 100 // YYYYMMDD00
if current >= datePrefix {
return current + 1
}
return datePrefix + 1
}

167
internal/db/zones_test.go Normal file
View File

@@ -0,0 +1,167 @@
package db
import (
"path/filepath"
"testing"
)
func openTestDB(t *testing.T) *DB {
t.Helper()
dir := t.TempDir()
database, err := Open(filepath.Join(dir, "test.db"))
if err != nil {
t.Fatalf("open db: %v", err)
}
if err := database.Migrate(); err != nil {
t.Fatalf("migrate: %v", err)
}
t.Cleanup(func() { _ = database.Close() })
return database
}
func TestCreateZone(t *testing.T) {
db := openTestDB(t)
zone, err := db.CreateZone("example.com", "ns1.example.com.", "admin.example.com.", 3600, 600, 86400, 300)
if err != nil {
t.Fatalf("create zone: %v", err)
}
if zone.Name != "example.com" {
t.Fatalf("got name %q, want %q", zone.Name, "example.com")
}
if zone.Serial == 0 {
t.Fatal("serial should not be zero")
}
if zone.PrimaryNS != "ns1.example.com." {
t.Fatalf("got primary_ns %q, want %q", zone.PrimaryNS, "ns1.example.com.")
}
}
func TestCreateZoneDuplicate(t *testing.T) {
db := openTestDB(t)
_, err := db.CreateZone("example.com", "ns1.example.com.", "admin.example.com.", 3600, 600, 86400, 300)
if err != nil {
t.Fatalf("create zone: %v", err)
}
_, err = db.CreateZone("example.com", "ns1.example.com.", "admin.example.com.", 3600, 600, 86400, 300)
if err == nil {
t.Fatal("expected error for duplicate zone")
}
}
func TestCreateZoneNormalization(t *testing.T) {
db := openTestDB(t)
zone, err := db.CreateZone("Example.COM.", "ns1.example.com.", "admin.example.com.", 3600, 600, 86400, 300)
if err != nil {
t.Fatalf("create zone: %v", err)
}
if zone.Name != "example.com" {
t.Fatalf("got name %q, want %q", zone.Name, "example.com")
}
}
func TestListZones(t *testing.T) {
db := openTestDB(t)
_, err := db.CreateZone("b.example.com", "ns1.example.com.", "admin.example.com.", 3600, 600, 86400, 300)
if err != nil {
t.Fatalf("create zone b: %v", err)
}
_, err = db.CreateZone("a.example.com", "ns1.example.com.", "admin.example.com.", 3600, 600, 86400, 300)
if err != nil {
t.Fatalf("create zone a: %v", err)
}
zones, err := db.ListZones()
if err != nil {
t.Fatalf("list zones: %v", err)
}
if len(zones) != 2 {
t.Fatalf("got %d zones, want 2", len(zones))
}
if zones[0].Name != "a.example.com" {
t.Fatalf("zones should be ordered by name, got %q first", zones[0].Name)
}
}
func TestGetZone(t *testing.T) {
db := openTestDB(t)
_, err := db.CreateZone("example.com", "ns1.example.com.", "admin.example.com.", 3600, 600, 86400, 300)
if err != nil {
t.Fatalf("create zone: %v", err)
}
zone, err := db.GetZone("example.com")
if err != nil {
t.Fatalf("get zone: %v", err)
}
if zone.Name != "example.com" {
t.Fatalf("got name %q, want %q", zone.Name, "example.com")
}
_, err = db.GetZone("nonexistent.com")
if err != ErrNotFound {
t.Fatalf("expected ErrNotFound, got %v", err)
}
}
func TestUpdateZone(t *testing.T) {
db := openTestDB(t)
original, err := db.CreateZone("example.com", "ns1.example.com.", "admin.example.com.", 3600, 600, 86400, 300)
if err != nil {
t.Fatalf("create zone: %v", err)
}
updated, err := db.UpdateZone("example.com", "ns2.example.com.", "newadmin.example.com.", 7200, 1200, 172800, 600)
if err != nil {
t.Fatalf("update zone: %v", err)
}
if updated.PrimaryNS != "ns2.example.com." {
t.Fatalf("got primary_ns %q, want %q", updated.PrimaryNS, "ns2.example.com.")
}
if updated.Serial <= original.Serial {
t.Fatalf("serial should have incremented: %d <= %d", updated.Serial, original.Serial)
}
}
func TestDeleteZone(t *testing.T) {
db := openTestDB(t)
_, err := db.CreateZone("example.com", "ns1.example.com.", "admin.example.com.", 3600, 600, 86400, 300)
if err != nil {
t.Fatalf("create zone: %v", err)
}
if err := db.DeleteZone("example.com"); err != nil {
t.Fatalf("delete zone: %v", err)
}
_, err = db.GetZone("example.com")
if err != ErrNotFound {
t.Fatalf("expected ErrNotFound after delete, got %v", err)
}
if err := db.DeleteZone("nonexistent.com"); err != ErrNotFound {
t.Fatalf("expected ErrNotFound for nonexistent zone, got %v", err)
}
}
func TestNextSerial(t *testing.T) {
// A zero serial should produce a date-based serial.
s1 := nextSerial(0)
if s1 < 2026032600 {
t.Fatalf("serial %d seems too low", s1)
}
// Incrementing should increase.
s2 := nextSerial(s1)
if s2 != s1+1 {
t.Fatalf("expected %d, got %d", s1+1, s2)
}
}

67
internal/dns/cache.go Normal file
View File

@@ -0,0 +1,67 @@
package dns
import (
"sync"
"time"
"github.com/miekg/dns"
)
type cacheKey struct {
Name string
Qtype uint16
Class uint16
}
type cacheEntry struct {
msg *dns.Msg
expiresAt time.Time
}
// Cache is a thread-safe in-memory DNS response cache with TTL-based expiry.
type Cache struct {
mu sync.RWMutex
entries map[cacheKey]*cacheEntry
}
// NewCache creates an empty DNS cache.
func NewCache() *Cache {
return &Cache{
entries: make(map[cacheKey]*cacheEntry),
}
}
// Get returns a cached response if it exists and has not expired.
func (c *Cache) Get(name string, qtype, class uint16) *dns.Msg {
c.mu.RLock()
defer c.mu.RUnlock()
key := cacheKey{Name: name, Qtype: qtype, Class: class}
entry, ok := c.entries[key]
if !ok || time.Now().After(entry.expiresAt) {
return nil
}
return entry.msg
}
// Set stores a DNS response in the cache with the given TTL.
func (c *Cache) Set(name string, qtype, class uint16, msg *dns.Msg, ttl time.Duration) {
c.mu.Lock()
defer c.mu.Unlock()
key := cacheKey{Name: name, Qtype: qtype, Class: class}
c.entries[key] = &cacheEntry{
msg: msg.Copy(),
expiresAt: time.Now().Add(ttl),
}
// Lazy eviction: clean up expired entries if cache is growing.
if len(c.entries) > 1000 {
now := time.Now()
for k, v := range c.entries {
if now.After(v.expiresAt) {
delete(c.entries, k)
}
}
}
}

View File

@@ -0,0 +1,81 @@
package dns
import (
"testing"
"time"
"github.com/miekg/dns"
)
func TestCacheSetGet(t *testing.T) {
c := NewCache()
msg := new(dns.Msg)
msg.SetQuestion("example.com.", dns.TypeA)
msg.Answer = append(msg.Answer, &dns.A{
Hdr: dns.RR_Header{Name: "example.com.", Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 60},
A: []byte{1, 2, 3, 4},
})
c.Set("example.com.", dns.TypeA, dns.ClassINET, msg, 5*time.Second)
cached := c.Get("example.com.", dns.TypeA, dns.ClassINET)
if cached == nil {
t.Fatal("expected cached response")
}
if len(cached.Answer) != 1 {
t.Fatalf("got %d answers, want 1", len(cached.Answer))
}
}
func TestCacheMiss(t *testing.T) {
c := NewCache()
cached := c.Get("example.com.", dns.TypeA, dns.ClassINET)
if cached != nil {
t.Fatal("expected nil for cache miss")
}
}
func TestCacheExpiry(t *testing.T) {
c := NewCache()
msg := new(dns.Msg)
msg.SetQuestion("example.com.", dns.TypeA)
c.Set("example.com.", dns.TypeA, dns.ClassINET, msg, 1*time.Millisecond)
time.Sleep(2 * time.Millisecond)
cached := c.Get("example.com.", dns.TypeA, dns.ClassINET)
if cached != nil {
t.Fatal("expected nil for expired entry")
}
}
func TestCacheDifferentTypes(t *testing.T) {
c := NewCache()
msgA := new(dns.Msg)
msgA.SetQuestion("example.com.", dns.TypeA)
c.Set("example.com.", dns.TypeA, dns.ClassINET, msgA, 5*time.Second)
msgAAAA := new(dns.Msg)
msgAAAA.SetQuestion("example.com.", dns.TypeAAAA)
c.Set("example.com.", dns.TypeAAAA, dns.ClassINET, msgAAAA, 5*time.Second)
cachedA := c.Get("example.com.", dns.TypeA, dns.ClassINET)
if cachedA == nil {
t.Fatal("expected cached A response")
}
cachedAAAA := c.Get("example.com.", dns.TypeAAAA, dns.ClassINET)
if cachedAAAA == nil {
t.Fatal("expected cached AAAA response")
}
// Different type should not match.
cachedMX := c.Get("example.com.", dns.TypeMX, dns.ClassINET)
if cachedMX != nil {
t.Fatal("expected nil for uncached type")
}
}

87
internal/dns/forwarder.go Normal file
View File

@@ -0,0 +1,87 @@
package dns
import (
"fmt"
"time"
"github.com/miekg/dns"
)
// Forwarder handles forwarding DNS queries to upstream resolvers.
type Forwarder struct {
upstreams []string
client *dns.Client
cache *Cache
}
// NewForwarder creates a Forwarder with the given upstream addresses.
func NewForwarder(upstreams []string) *Forwarder {
return &Forwarder{
upstreams: upstreams,
client: &dns.Client{
Timeout: 2 * time.Second,
},
cache: NewCache(),
}
}
// Forward sends a query to upstream resolvers and returns the response.
// Responses are cached by (qname, qtype, qclass) with TTL-based expiry.
func (f *Forwarder) Forward(r *dns.Msg) (*dns.Msg, error) {
if len(r.Question) == 0 {
return nil, fmt.Errorf("empty question")
}
q := r.Question[0]
// Check cache.
if cached := f.cache.Get(q.Name, q.Qtype, q.Qclass); cached != nil {
return cached.Copy(), nil
}
// Try each upstream in order.
var lastErr error
for _, upstream := range f.upstreams {
resp, _, err := f.client.Exchange(r, upstream)
if err != nil {
lastErr = err
continue
}
// Don't cache SERVFAIL or REFUSED.
if resp.Rcode != dns.RcodeServerFailure && resp.Rcode != dns.RcodeRefused {
ttl := minTTL(resp)
if ttl > 300 {
ttl = 300
}
if ttl > 0 {
f.cache.Set(q.Name, q.Qtype, q.Qclass, resp, time.Duration(ttl)*time.Second)
}
}
return resp, nil
}
return nil, fmt.Errorf("all upstreams failed: %w", lastErr)
}
// minTTL returns the minimum TTL from all resource records in a response.
func minTTL(msg *dns.Msg) uint32 {
var min uint32
first := true
for _, sections := range [][]dns.RR{msg.Answer, msg.Ns, msg.Extra} {
for _, rr := range sections {
ttl := rr.Header().Ttl
if first || ttl < min {
min = ttl
first = false
}
}
}
if first {
return 60 // No records; default to 60s.
}
return min
}

280
internal/dns/server.go Normal file
View File

@@ -0,0 +1,280 @@
// Package dns implements the authoritative DNS server for MCNS.
// It serves records from SQLite for authoritative zones and forwards
// all other queries to configured upstream resolvers.
package dns
import (
"log/slog"
"net"
"strings"
"github.com/miekg/dns"
"git.wntrmute.dev/kyle/mcns/internal/db"
)
// Server is the MCNS DNS server. It listens on both UDP and TCP.
type Server struct {
db *db.DB
forwarder *Forwarder
logger *slog.Logger
udp *dns.Server
tcp *dns.Server
}
// New creates a DNS server that serves records from the database and
// forwards non-authoritative queries to the given upstreams.
func New(database *db.DB, upstreams []string, logger *slog.Logger) *Server {
s := &Server{
db: database,
forwarder: NewForwarder(upstreams),
logger: logger,
}
mux := dns.NewServeMux()
mux.HandleFunc(".", s.handleQuery)
s.udp = &dns.Server{Handler: mux, Net: "udp"}
s.tcp = &dns.Server{Handler: mux, Net: "tcp"}
return s
}
// ListenAndServe starts the DNS server on the given address for both
// UDP and TCP. It blocks until Shutdown is called.
func (s *Server) ListenAndServe(addr string) error {
s.udp.Addr = addr
s.tcp.Addr = addr
errCh := make(chan error, 2)
go func() {
s.logger.Info("dns server listening", "addr", addr, "proto", "udp")
errCh <- s.udp.ListenAndServe()
}()
go func() {
s.logger.Info("dns server listening", "addr", addr, "proto", "tcp")
errCh <- s.tcp.ListenAndServe()
}()
return <-errCh
}
// Shutdown gracefully stops the DNS server.
func (s *Server) Shutdown() {
_ = s.udp.Shutdown()
_ = s.tcp.Shutdown()
}
// handleQuery is the main DNS query handler. It checks if the query
// falls within an authoritative zone and either serves from the database
// or forwards to upstream.
func (s *Server) handleQuery(w dns.ResponseWriter, r *dns.Msg) {
if len(r.Question) == 0 {
s.writeResponse(w, r, dns.RcodeFormatError, nil, nil)
return
}
q := r.Question[0]
qname := strings.ToLower(q.Name)
// Find the authoritative zone for this query.
zone := s.findZone(qname)
if zone == nil {
// Not authoritative — forward to upstream.
s.forwardQuery(w, r)
return
}
s.handleAuthoritativeQuery(w, r, zone, qname, q.Qtype)
}
// findZone returns the best matching zone for the query name, or nil.
func (s *Server) findZone(qname string) *db.Zone {
// Walk up the domain labels to find the longest matching zone.
name := strings.TrimSuffix(qname, ".")
parts := strings.Split(name, ".")
for i := range parts {
candidate := strings.Join(parts[i:], ".")
zone, err := s.db.GetZone(candidate)
if err == nil {
return zone
}
}
return nil
}
// handleAuthoritativeQuery serves a query from the database.
func (s *Server) handleAuthoritativeQuery(w dns.ResponseWriter, r *dns.Msg, zone *db.Zone, qname string, qtype uint16) {
// Extract the record name relative to the zone.
zoneFQDN := zone.Name + "."
var relName string
if qname == zoneFQDN {
relName = "@"
} else {
relName = strings.TrimSuffix(qname, "."+zoneFQDN)
}
// Handle SOA queries.
if qtype == dns.TypeSOA || relName == "@" && qtype == dns.TypeSOA {
soa := s.buildSOA(zone)
s.writeResponse(w, r, dns.RcodeSuccess, []dns.RR{soa}, nil)
return
}
// Handle NS queries at the zone apex.
if qtype == dns.TypeNS && relName == "@" {
ns := &dns.NS{
Hdr: dns.RR_Header{Name: zoneFQDN, Rrtype: dns.TypeNS, Class: dns.ClassINET, Ttl: uint32(zone.MinimumTTL)},
Ns: zone.PrimaryNS,
}
s.writeResponse(w, r, dns.RcodeSuccess, []dns.RR{ns}, nil)
return
}
// Look up the requested record type.
var answers []dns.RR
var lookupType string
switch qtype {
case dns.TypeA:
lookupType = "A"
case dns.TypeAAAA:
lookupType = "AAAA"
case dns.TypeCNAME:
lookupType = "CNAME"
default:
// For unsupported types, check if the name exists at all.
// If it does, return empty answer. If not, NXDOMAIN.
exists, _ := s.nameExists(zone.Name, relName)
if exists {
s.writeResponse(w, r, dns.RcodeSuccess, nil, []dns.RR{s.buildSOA(zone)})
} else {
s.writeResponse(w, r, dns.RcodeNameError, nil, []dns.RR{s.buildSOA(zone)})
}
return
}
records, err := s.db.LookupRecords(zone.Name, relName, lookupType)
if err != nil {
s.logger.Error("dns lookup failed", "zone", zone.Name, "name", relName, "type", lookupType, "error", err)
s.writeResponse(w, r, dns.RcodeServerFailure, nil, nil)
return
}
// If no direct records, check for CNAME.
if len(records) == 0 && (qtype == dns.TypeA || qtype == dns.TypeAAAA) {
cnameRecords, err := s.db.LookupCNAME(zone.Name, relName)
if err == nil && len(cnameRecords) > 0 {
for _, rec := range cnameRecords {
answers = append(answers, &dns.CNAME{
Hdr: dns.RR_Header{Name: qname, Rrtype: dns.TypeCNAME, Class: dns.ClassINET, Ttl: uint32(rec.TTL)},
Target: rec.Value,
})
}
s.writeResponse(w, r, dns.RcodeSuccess, answers, nil)
return
}
}
if len(records) == 0 {
// Name might still exist with other record types.
exists, _ := s.nameExists(zone.Name, relName)
if exists {
// NODATA: name exists but no records of requested type.
s.writeResponse(w, r, dns.RcodeSuccess, nil, []dns.RR{s.buildSOA(zone)})
} else {
// NXDOMAIN: name does not exist.
s.writeResponse(w, r, dns.RcodeNameError, nil, []dns.RR{s.buildSOA(zone)})
}
return
}
for _, rec := range records {
rr := s.recordToRR(qname, rec)
if rr != nil {
answers = append(answers, rr)
}
}
s.writeResponse(w, r, dns.RcodeSuccess, answers, nil)
}
// nameExists checks if any records exist for a name in a zone.
func (s *Server) nameExists(zoneName, name string) (bool, error) {
records, err := s.db.ListRecords(zoneName, name, "")
if err != nil {
return false, err
}
return len(records) > 0, nil
}
// recordToRR converts a database Record to a dns.RR.
func (s *Server) recordToRR(qname string, rec db.Record) dns.RR {
hdr := dns.RR_Header{Name: qname, Class: dns.ClassINET, Ttl: uint32(rec.TTL)}
switch rec.Type {
case "A":
hdr.Rrtype = dns.TypeA
return &dns.A{Hdr: hdr, A: parseIP(rec.Value)}
case "AAAA":
hdr.Rrtype = dns.TypeAAAA
return &dns.AAAA{Hdr: hdr, AAAA: parseIP(rec.Value)}
case "CNAME":
hdr.Rrtype = dns.TypeCNAME
return &dns.CNAME{Hdr: hdr, Target: rec.Value}
}
return nil
}
// buildSOA constructs a SOA record for the given zone.
func (s *Server) buildSOA(zone *db.Zone) *dns.SOA {
return &dns.SOA{
Hdr: dns.RR_Header{Name: zone.Name + ".", Rrtype: dns.TypeSOA, Class: dns.ClassINET, Ttl: uint32(zone.MinimumTTL)},
Ns: zone.PrimaryNS,
Mbox: zone.AdminEmail,
Serial: uint32(zone.Serial),
Refresh: uint32(zone.Refresh),
Retry: uint32(zone.Retry),
Expire: uint32(zone.Expire),
Minttl: uint32(zone.MinimumTTL),
}
}
// writeResponse constructs and writes a DNS response.
func (s *Server) writeResponse(w dns.ResponseWriter, r *dns.Msg, rcode int, answer []dns.RR, ns []dns.RR) {
m := new(dns.Msg)
m.SetReply(r)
m.Authoritative = true
m.Rcode = rcode
m.Answer = answer
m.Ns = ns
if err := w.WriteMsg(m); err != nil {
s.logger.Error("dns write failed", "error", err)
}
}
// forwardQuery forwards a DNS query to upstream resolvers.
func (s *Server) forwardQuery(w dns.ResponseWriter, r *dns.Msg) {
resp, err := s.forwarder.Forward(r)
if err != nil {
s.logger.Debug("dns forward failed", "error", err)
m := new(dns.Msg)
m.SetReply(r)
m.Rcode = dns.RcodeServerFailure
_ = w.WriteMsg(m)
return
}
resp.Id = r.Id
if err := w.WriteMsg(resp); err != nil {
s.logger.Error("dns write failed", "error", err)
}
}
// parseIP parses an IP address string into a net.IP.
func parseIP(s string) net.IP {
return net.ParseIP(s)
}

142
internal/dns/server_test.go Normal file
View File

@@ -0,0 +1,142 @@
package dns
import (
"path/filepath"
"testing"
"github.com/miekg/dns"
"git.wntrmute.dev/kyle/mcns/internal/db"
"log/slog"
)
func openTestDB(t *testing.T) *db.DB {
t.Helper()
dir := t.TempDir()
database, err := db.Open(filepath.Join(dir, "test.db"))
if err != nil {
t.Fatalf("open db: %v", err)
}
if err := database.Migrate(); err != nil {
t.Fatalf("migrate: %v", err)
}
t.Cleanup(func() { _ = database.Close() })
return database
}
func setupTestServer(t *testing.T) (*Server, *db.DB) {
t.Helper()
database := openTestDB(t)
logger := slog.Default()
_, err := database.CreateZone("svc.mcp.metacircular.net", "ns.mcp.metacircular.net.", "admin.metacircular.net.", 3600, 600, 86400, 300)
if err != nil {
t.Fatalf("create zone: %v", err)
}
_, err = database.CreateRecord("svc.mcp.metacircular.net", "metacrypt", "A", "192.168.88.181", 300)
if err != nil {
t.Fatalf("create A record: %v", err)
}
_, err = database.CreateRecord("svc.mcp.metacircular.net", "metacrypt", "A", "100.95.252.120", 300)
if err != nil {
t.Fatalf("create A record 2: %v", err)
}
_, err = database.CreateRecord("svc.mcp.metacircular.net", "mcr", "AAAA", "2001:db8::1", 300)
if err != nil {
t.Fatalf("create AAAA record: %v", err)
}
_, err = database.CreateRecord("svc.mcp.metacircular.net", "alias", "CNAME", "metacrypt.svc.mcp.metacircular.net.", 300)
if err != nil {
t.Fatalf("create CNAME record: %v", err)
}
srv := New(database, []string{"1.1.1.1:53"}, logger)
return srv, database
}
func TestFindZone(t *testing.T) {
srv, _ := setupTestServer(t)
zone := srv.findZone("metacrypt.svc.mcp.metacircular.net.")
if zone == nil {
t.Fatal("expected to find zone")
}
if zone.Name != "svc.mcp.metacircular.net" {
t.Fatalf("got zone %q, want %q", zone.Name, "svc.mcp.metacircular.net")
}
zone = srv.findZone("nonexistent.com.")
if zone != nil {
t.Fatal("expected nil for nonexistent zone")
}
}
func TestBuildSOA(t *testing.T) {
srv, database := setupTestServer(t)
zone, err := database.GetZone("svc.mcp.metacircular.net")
if err != nil {
t.Fatalf("get zone: %v", err)
}
soa := srv.buildSOA(zone)
if soa.Ns != "ns.mcp.metacircular.net." {
t.Fatalf("got ns %q, want %q", soa.Ns, "ns.mcp.metacircular.net.")
}
if soa.Hdr.Name != "svc.mcp.metacircular.net." {
t.Fatalf("got name %q, want %q", soa.Hdr.Name, "svc.mcp.metacircular.net.")
}
}
func TestRecordToRR_A(t *testing.T) {
srv, _ := setupTestServer(t)
rec := db.Record{Name: "metacrypt", Type: "A", Value: "192.168.88.181", TTL: 300}
rr := srv.recordToRR("metacrypt.svc.mcp.metacircular.net.", rec)
if rr == nil {
t.Fatal("expected non-nil RR")
}
a, ok := rr.(*dns.A)
if !ok {
t.Fatalf("expected *dns.A, got %T", rr)
}
if a.A.String() != "192.168.88.181" {
t.Fatalf("got IP %q, want %q", a.A.String(), "192.168.88.181")
}
}
func TestRecordToRR_AAAA(t *testing.T) {
srv, _ := setupTestServer(t)
rec := db.Record{Name: "mcr", Type: "AAAA", Value: "2001:db8::1", TTL: 300}
rr := srv.recordToRR("mcr.svc.mcp.metacircular.net.", rec)
if rr == nil {
t.Fatal("expected non-nil RR")
}
aaaa, ok := rr.(*dns.AAAA)
if !ok {
t.Fatalf("expected *dns.AAAA, got %T", rr)
}
if aaaa.AAAA.String() != "2001:db8::1" {
t.Fatalf("got IP %q, want %q", aaaa.AAAA.String(), "2001:db8::1")
}
}
func TestRecordToRR_CNAME(t *testing.T) {
srv, _ := setupTestServer(t)
rec := db.Record{Name: "alias", Type: "CNAME", Value: "metacrypt.svc.mcp.metacircular.net.", TTL: 300}
rr := srv.recordToRR("alias.svc.mcp.metacircular.net.", rec)
if rr == nil {
t.Fatal("expected non-nil RR")
}
cname, ok := rr.(*dns.CNAME)
if !ok {
t.Fatalf("expected *dns.CNAME, got %T", rr)
}
if cname.Target != "metacrypt.svc.mcp.metacircular.net." {
t.Fatalf("got target %q, want %q", cname.Target, "metacrypt.svc.mcp.metacircular.net.")
}
}

View File

@@ -0,0 +1,20 @@
package grpcserver
import (
"context"
pb "git.wntrmute.dev/kyle/mcns/gen/mcns/v1"
"git.wntrmute.dev/kyle/mcns/internal/db"
)
type adminService struct {
pb.UnimplementedAdminServiceServer
db *db.DB
}
func (s *adminService) Health(_ context.Context, _ *pb.HealthRequest) (*pb.HealthResponse, error) {
if err := s.db.Ping(); err != nil {
return &pb.HealthResponse{Status: "unhealthy"}, nil
}
return &pb.HealthResponse{Status: "ok"}, nil
}

View File

@@ -0,0 +1,38 @@
package grpcserver
import (
"context"
"errors"
mcdslauth "git.wntrmute.dev/kyle/mcdsl/auth"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
pb "git.wntrmute.dev/kyle/mcns/gen/mcns/v1"
)
type authService struct {
pb.UnimplementedAuthServiceServer
auth *mcdslauth.Authenticator
}
func (s *authService) Login(_ context.Context, req *pb.LoginRequest) (*pb.LoginResponse, error) {
token, _, err := s.auth.Login(req.Username, req.Password, req.TotpCode)
if err != nil {
if errors.Is(err, mcdslauth.ErrInvalidCredentials) {
return nil, status.Error(codes.Unauthenticated, "invalid credentials")
}
if errors.Is(err, mcdslauth.ErrForbidden) {
return nil, status.Error(codes.PermissionDenied, "access denied by login policy")
}
return nil, status.Error(codes.Unavailable, "authentication service unavailable")
}
return &pb.LoginResponse{Token: token}, nil
}
func (s *authService) Logout(_ context.Context, req *pb.LogoutRequest) (*pb.LogoutResponse, error) {
if err := s.auth.Logout(req.Token); err != nil {
return nil, status.Error(codes.Internal, "logout failed")
}
return &pb.LogoutResponse{}, nil
}

View File

@@ -0,0 +1,45 @@
package grpcserver
import (
mcdslgrpc "git.wntrmute.dev/kyle/mcdsl/grpcserver"
)
// methodMap builds the mcdsl grpcserver.MethodMap for MCNS.
//
// Adding a new RPC without adding it to the correct map is a security
// defect — the mcdsl auth interceptor denies unmapped methods by default.
func methodMap() mcdslgrpc.MethodMap {
return mcdslgrpc.MethodMap{
Public: publicMethods(),
AuthRequired: authRequiredMethods(),
AdminRequired: adminRequiredMethods(),
}
}
func publicMethods() map[string]bool {
return map[string]bool{
"/mcns.v1.AdminService/Health": true,
"/mcns.v1.AuthService/Login": true,
}
}
func authRequiredMethods() map[string]bool {
return map[string]bool{
"/mcns.v1.AuthService/Logout": true,
"/mcns.v1.ZoneService/ListZones": true,
"/mcns.v1.ZoneService/GetZone": true,
"/mcns.v1.RecordService/ListRecords": true,
"/mcns.v1.RecordService/GetRecord": true,
}
}
func adminRequiredMethods() map[string]bool {
return map[string]bool{
"/mcns.v1.ZoneService/CreateZone": true,
"/mcns.v1.ZoneService/UpdateZone": true,
"/mcns.v1.ZoneService/DeleteZone": true,
"/mcns.v1.RecordService/CreateRecord": true,
"/mcns.v1.RecordService/UpdateRecord": true,
"/mcns.v1.RecordService/DeleteRecord": true,
}
}

View File

@@ -0,0 +1,110 @@
package grpcserver
import (
"context"
"errors"
"time"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/timestamppb"
pb "git.wntrmute.dev/kyle/mcns/gen/mcns/v1"
"git.wntrmute.dev/kyle/mcns/internal/db"
)
type recordService struct {
pb.UnimplementedRecordServiceServer
db *db.DB
}
func (s *recordService) ListRecords(_ context.Context, req *pb.ListRecordsRequest) (*pb.ListRecordsResponse, error) {
records, err := s.db.ListRecords(req.Zone, req.Name, req.Type)
if errors.Is(err, db.ErrNotFound) {
return nil, status.Error(codes.NotFound, "zone not found")
}
if err != nil {
return nil, status.Error(codes.Internal, "failed to list records")
}
resp := &pb.ListRecordsResponse{}
for _, r := range records {
resp.Records = append(resp.Records, recordToProto(r))
}
return resp, nil
}
func (s *recordService) GetRecord(_ context.Context, req *pb.GetRecordRequest) (*pb.Record, error) {
record, err := s.db.GetRecord(req.Id)
if errors.Is(err, db.ErrNotFound) {
return nil, status.Error(codes.NotFound, "record not found")
}
if err != nil {
return nil, status.Error(codes.Internal, "failed to get record")
}
return recordToProto(*record), nil
}
func (s *recordService) CreateRecord(_ context.Context, req *pb.CreateRecordRequest) (*pb.Record, error) {
record, err := s.db.CreateRecord(req.Zone, req.Name, req.Type, req.Value, int(req.Ttl))
if errors.Is(err, db.ErrNotFound) {
return nil, status.Error(codes.NotFound, "zone not found")
}
if errors.Is(err, db.ErrConflict) {
return nil, status.Error(codes.AlreadyExists, err.Error())
}
if err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}
return recordToProto(*record), nil
}
func (s *recordService) UpdateRecord(_ context.Context, req *pb.UpdateRecordRequest) (*pb.Record, error) {
record, err := s.db.UpdateRecord(req.Id, req.Name, req.Type, req.Value, int(req.Ttl))
if errors.Is(err, db.ErrNotFound) {
return nil, status.Error(codes.NotFound, "record not found")
}
if errors.Is(err, db.ErrConflict) {
return nil, status.Error(codes.AlreadyExists, err.Error())
}
if err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}
return recordToProto(*record), nil
}
func (s *recordService) DeleteRecord(_ context.Context, req *pb.DeleteRecordRequest) (*pb.DeleteRecordResponse, error) {
err := s.db.DeleteRecord(req.Id)
if errors.Is(err, db.ErrNotFound) {
return nil, status.Error(codes.NotFound, "record not found")
}
if err != nil {
return nil, status.Error(codes.Internal, "failed to delete record")
}
return &pb.DeleteRecordResponse{}, nil
}
func recordToProto(r db.Record) *pb.Record {
return &pb.Record{
Id: r.ID,
Zone: r.ZoneName,
Name: r.Name,
Type: r.Type,
Value: r.Value,
Ttl: int32(r.TTL),
CreatedAt: parseRecordTimestamp(r.CreatedAt),
UpdatedAt: parseRecordTimestamp(r.UpdatedAt),
}
}
func parseRecordTimestamp(s string) *timestamppb.Timestamp {
t, err := parseTime(s)
if err != nil {
return nil
}
return timestamppb.New(t)
}
func parseTime(s string) (time.Time, error) {
return time.Parse("2006-01-02T15:04:05Z", s)
}

View File

@@ -0,0 +1,50 @@
package grpcserver
import (
"log/slog"
"net"
mcdslauth "git.wntrmute.dev/kyle/mcdsl/auth"
mcdslgrpc "git.wntrmute.dev/kyle/mcdsl/grpcserver"
pb "git.wntrmute.dev/kyle/mcns/gen/mcns/v1"
"git.wntrmute.dev/kyle/mcns/internal/db"
)
// Deps holds the dependencies injected into the gRPC server.
type Deps struct {
DB *db.DB
Authenticator *mcdslauth.Authenticator
}
// Server wraps a mcdsl grpcserver.Server with MCNS-specific services.
type Server struct {
srv *mcdslgrpc.Server
}
// New creates a configured gRPC server with MCNS services registered.
func New(certFile, keyFile string, deps Deps, logger *slog.Logger) (*Server, error) {
srv, err := mcdslgrpc.New(certFile, keyFile, deps.Authenticator, methodMap(), logger)
if err != nil {
return nil, err
}
s := &Server{srv: srv}
pb.RegisterAdminServiceServer(srv.GRPCServer, &adminService{db: deps.DB})
pb.RegisterAuthServiceServer(srv.GRPCServer, &authService{auth: deps.Authenticator})
pb.RegisterZoneServiceServer(srv.GRPCServer, &zoneService{db: deps.DB})
pb.RegisterRecordServiceServer(srv.GRPCServer, &recordService{db: deps.DB})
return s, nil
}
// Serve starts the gRPC server on the given listener.
func (s *Server) Serve(lis net.Listener) error {
return s.srv.GRPCServer.Serve(lis)
}
// GracefulStop gracefully stops the gRPC server.
func (s *Server) GracefulStop() {
s.srv.Stop()
}

View File

@@ -0,0 +1,134 @@
package grpcserver
import (
"context"
"errors"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/timestamppb"
pb "git.wntrmute.dev/kyle/mcns/gen/mcns/v1"
"git.wntrmute.dev/kyle/mcns/internal/db"
)
type zoneService struct {
pb.UnimplementedZoneServiceServer
db *db.DB
}
func (s *zoneService) ListZones(_ context.Context, _ *pb.ListZonesRequest) (*pb.ListZonesResponse, error) {
zones, err := s.db.ListZones()
if err != nil {
return nil, status.Error(codes.Internal, "failed to list zones")
}
resp := &pb.ListZonesResponse{}
for _, z := range zones {
resp.Zones = append(resp.Zones, zoneToProto(z))
}
return resp, nil
}
func (s *zoneService) GetZone(_ context.Context, req *pb.GetZoneRequest) (*pb.Zone, error) {
zone, err := s.db.GetZone(req.Name)
if errors.Is(err, db.ErrNotFound) {
return nil, status.Error(codes.NotFound, "zone not found")
}
if err != nil {
return nil, status.Error(codes.Internal, "failed to get zone")
}
return zoneToProto(*zone), nil
}
func (s *zoneService) CreateZone(_ context.Context, req *pb.CreateZoneRequest) (*pb.Zone, error) {
refresh := int(req.Refresh)
if refresh == 0 {
refresh = 3600
}
retry := int(req.Retry)
if retry == 0 {
retry = 600
}
expire := int(req.Expire)
if expire == 0 {
expire = 86400
}
minTTL := int(req.MinimumTtl)
if minTTL == 0 {
minTTL = 300
}
zone, err := s.db.CreateZone(req.Name, req.PrimaryNs, req.AdminEmail, refresh, retry, expire, minTTL)
if errors.Is(err, db.ErrConflict) {
return nil, status.Error(codes.AlreadyExists, err.Error())
}
if err != nil {
return nil, status.Error(codes.Internal, "failed to create zone")
}
return zoneToProto(*zone), nil
}
func (s *zoneService) UpdateZone(_ context.Context, req *pb.UpdateZoneRequest) (*pb.Zone, error) {
refresh := int(req.Refresh)
if refresh == 0 {
refresh = 3600
}
retry := int(req.Retry)
if retry == 0 {
retry = 600
}
expire := int(req.Expire)
if expire == 0 {
expire = 86400
}
minTTL := int(req.MinimumTtl)
if minTTL == 0 {
minTTL = 300
}
zone, err := s.db.UpdateZone(req.Name, req.PrimaryNs, req.AdminEmail, refresh, retry, expire, minTTL)
if errors.Is(err, db.ErrNotFound) {
return nil, status.Error(codes.NotFound, "zone not found")
}
if err != nil {
return nil, status.Error(codes.Internal, "failed to update zone")
}
return zoneToProto(*zone), nil
}
func (s *zoneService) DeleteZone(_ context.Context, req *pb.DeleteZoneRequest) (*pb.DeleteZoneResponse, error) {
err := s.db.DeleteZone(req.Name)
if errors.Is(err, db.ErrNotFound) {
return nil, status.Error(codes.NotFound, "zone not found")
}
if err != nil {
return nil, status.Error(codes.Internal, "failed to delete zone")
}
return &pb.DeleteZoneResponse{}, nil
}
func zoneToProto(z db.Zone) *pb.Zone {
return &pb.Zone{
Id: z.ID,
Name: z.Name,
PrimaryNs: z.PrimaryNS,
AdminEmail: z.AdminEmail,
Refresh: int32(z.Refresh),
Retry: int32(z.Retry),
Expire: int32(z.Expire),
MinimumTtl: int32(z.MinimumTTL),
Serial: z.Serial,
CreatedAt: parseTimestamp(z.CreatedAt),
UpdatedAt: parseTimestamp(z.UpdatedAt),
}
}
func parseTimestamp(s string) *timestamppb.Timestamp {
// SQLite stores as "2006-01-02T15:04:05Z".
t, err := parseTime(s)
if err != nil {
return nil
}
return timestamppb.New(t)
}

62
internal/server/auth.go Normal file
View File

@@ -0,0 +1,62 @@
package server
import (
"encoding/json"
"errors"
"net/http"
mcdslauth "git.wntrmute.dev/kyle/mcdsl/auth"
)
type loginRequest struct {
Username string `json:"username"`
Password string `json:"password"`
TOTPCode string `json:"totp_code"`
}
type loginResponse struct {
Token string `json:"token"`
}
func loginHandler(auth *mcdslauth.Authenticator) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req loginRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
token, _, err := auth.Login(req.Username, req.Password, req.TOTPCode)
if err != nil {
if errors.Is(err, mcdslauth.ErrInvalidCredentials) {
writeError(w, http.StatusUnauthorized, "invalid credentials")
return
}
if errors.Is(err, mcdslauth.ErrForbidden) {
writeError(w, http.StatusForbidden, "access denied by login policy")
return
}
writeError(w, http.StatusServiceUnavailable, "authentication service unavailable")
return
}
writeJSON(w, http.StatusOK, loginResponse{Token: token})
}
}
func logoutHandler(auth *mcdslauth.Authenticator) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
token := extractBearerToken(r)
if token == "" {
writeError(w, http.StatusUnauthorized, "authentication required")
return
}
if err := auth.Logout(token); err != nil {
writeError(w, http.StatusInternalServerError, "logout failed")
return
}
w.WriteHeader(http.StatusNoContent)
}
}

View File

@@ -0,0 +1,96 @@
package server
import (
"context"
"log/slog"
"net/http"
"strings"
"time"
mcdslauth "git.wntrmute.dev/kyle/mcdsl/auth"
)
type contextKey string
const tokenInfoKey contextKey = "tokenInfo"
// requireAuth returns middleware that validates Bearer tokens via MCIAS.
func requireAuth(auth *mcdslauth.Authenticator) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := extractBearerToken(r)
if token == "" {
writeError(w, http.StatusUnauthorized, "authentication required")
return
}
info, err := auth.ValidateToken(token)
if err != nil {
writeError(w, http.StatusUnauthorized, "invalid or expired token")
return
}
ctx := context.WithValue(r.Context(), tokenInfoKey, info)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// requireAdmin is middleware that checks the caller has the admin role.
func requireAdmin(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
info := tokenInfoFromContext(r.Context())
if info == nil || !info.IsAdmin {
writeError(w, http.StatusForbidden, "admin role required")
return
}
next.ServeHTTP(w, r)
})
}
// tokenInfoFromContext extracts the TokenInfo from the request context.
func tokenInfoFromContext(ctx context.Context) *mcdslauth.TokenInfo {
info, _ := ctx.Value(tokenInfoKey).(*mcdslauth.TokenInfo)
return info
}
// extractBearerToken extracts a bearer token from the Authorization header.
func extractBearerToken(r *http.Request) string {
h := r.Header.Get("Authorization")
if h == "" {
return ""
}
const prefix = "Bearer "
if !strings.HasPrefix(h, prefix) {
return ""
}
return strings.TrimSpace(h[len(prefix):])
}
// loggingMiddleware logs HTTP requests.
func loggingMiddleware(logger *slog.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
sw := &statusWriter{ResponseWriter: w, status: http.StatusOK}
next.ServeHTTP(sw, r)
logger.Info("http",
"method", r.Method,
"path", r.URL.Path,
"status", sw.status,
"duration", time.Since(start),
"remote", r.RemoteAddr,
)
})
}
}
type statusWriter struct {
http.ResponseWriter
status int
}
func (w *statusWriter) WriteHeader(code int) {
w.status = code
w.ResponseWriter.WriteHeader(code)
}

174
internal/server/records.go Normal file
View File

@@ -0,0 +1,174 @@
package server
import (
"encoding/json"
"errors"
"net/http"
"strconv"
"github.com/go-chi/chi/v5"
"git.wntrmute.dev/kyle/mcns/internal/db"
)
type createRecordRequest struct {
Name string `json:"name"`
Type string `json:"type"`
Value string `json:"value"`
TTL int `json:"ttl"`
}
func listRecordsHandler(database *db.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
zoneName := chi.URLParam(r, "zone")
nameFilter := r.URL.Query().Get("name")
typeFilter := r.URL.Query().Get("type")
records, err := database.ListRecords(zoneName, nameFilter, typeFilter)
if errors.Is(err, db.ErrNotFound) {
writeError(w, http.StatusNotFound, "zone not found")
return
}
if err != nil {
writeError(w, http.StatusInternalServerError, "failed to list records")
return
}
if records == nil {
records = []db.Record{}
}
writeJSON(w, http.StatusOK, map[string]any{"records": records})
}
}
func getRecordHandler(database *db.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
idStr := chi.URLParam(r, "id")
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid record ID")
return
}
record, err := database.GetRecord(id)
if errors.Is(err, db.ErrNotFound) {
writeError(w, http.StatusNotFound, "record not found")
return
}
if err != nil {
writeError(w, http.StatusInternalServerError, "failed to get record")
return
}
writeJSON(w, http.StatusOK, record)
}
}
func createRecordHandler(database *db.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
zoneName := chi.URLParam(r, "zone")
var req createRecordRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
if req.Name == "" {
writeError(w, http.StatusBadRequest, "name is required")
return
}
if req.Type == "" {
writeError(w, http.StatusBadRequest, "type is required")
return
}
if req.Value == "" {
writeError(w, http.StatusBadRequest, "value is required")
return
}
record, err := database.CreateRecord(zoneName, req.Name, req.Type, req.Value, req.TTL)
if errors.Is(err, db.ErrNotFound) {
writeError(w, http.StatusNotFound, "zone not found")
return
}
if errors.Is(err, db.ErrConflict) {
writeError(w, http.StatusConflict, err.Error())
return
}
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
writeJSON(w, http.StatusCreated, record)
}
}
func updateRecordHandler(database *db.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
idStr := chi.URLParam(r, "id")
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid record ID")
return
}
var req createRecordRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
if req.Name == "" {
writeError(w, http.StatusBadRequest, "name is required")
return
}
if req.Type == "" {
writeError(w, http.StatusBadRequest, "type is required")
return
}
if req.Value == "" {
writeError(w, http.StatusBadRequest, "value is required")
return
}
record, err := database.UpdateRecord(id, req.Name, req.Type, req.Value, req.TTL)
if errors.Is(err, db.ErrNotFound) {
writeError(w, http.StatusNotFound, "record not found")
return
}
if errors.Is(err, db.ErrConflict) {
writeError(w, http.StatusConflict, err.Error())
return
}
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
writeJSON(w, http.StatusOK, record)
}
}
func deleteRecordHandler(database *db.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
idStr := chi.URLParam(r, "id")
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid record ID")
return
}
err = database.DeleteRecord(id)
if errors.Is(err, db.ErrNotFound) {
writeError(w, http.StatusNotFound, "record not found")
return
}
if err != nil {
writeError(w, http.StatusInternalServerError, "failed to delete record")
return
}
w.WriteHeader(http.StatusNoContent)
}
}

71
internal/server/routes.go Normal file
View File

@@ -0,0 +1,71 @@
package server
import (
"encoding/json"
"log/slog"
"net/http"
"github.com/go-chi/chi/v5"
mcdslauth "git.wntrmute.dev/kyle/mcdsl/auth"
"git.wntrmute.dev/kyle/mcdsl/health"
"git.wntrmute.dev/kyle/mcns/internal/db"
)
// Deps holds dependencies injected into the REST handlers.
type Deps struct {
DB *db.DB
Auth *mcdslauth.Authenticator
Logger *slog.Logger
}
// NewRouter builds the chi router with all MCNS REST endpoints.
func NewRouter(deps Deps) *chi.Mux {
r := chi.NewRouter()
r.Use(loggingMiddleware(deps.Logger))
// Public endpoints.
r.Post("/v1/auth/login", loginHandler(deps.Auth))
r.Get("/v1/health", health.Handler(deps.DB.DB))
// Authenticated endpoints.
r.Group(func(r chi.Router) {
r.Use(requireAuth(deps.Auth))
r.Post("/v1/auth/logout", logoutHandler(deps.Auth))
// Zone endpoints — reads for all authenticated users, writes for admin.
r.Get("/v1/zones", listZonesHandler(deps.DB))
r.Get("/v1/zones/{zone}", getZoneHandler(deps.DB))
// Admin-only zone mutations.
r.With(requireAdmin).Post("/v1/zones", createZoneHandler(deps.DB))
r.With(requireAdmin).Put("/v1/zones/{zone}", updateZoneHandler(deps.DB))
r.With(requireAdmin).Delete("/v1/zones/{zone}", deleteZoneHandler(deps.DB))
// Record endpoints — reads for all authenticated users, writes for admin.
r.Get("/v1/zones/{zone}/records", listRecordsHandler(deps.DB))
r.Get("/v1/zones/{zone}/records/{id}", getRecordHandler(deps.DB))
// Admin-only record mutations.
r.With(requireAdmin).Post("/v1/zones/{zone}/records", createRecordHandler(deps.DB))
r.With(requireAdmin).Put("/v1/zones/{zone}/records/{id}", updateRecordHandler(deps.DB))
r.With(requireAdmin).Delete("/v1/zones/{zone}/records/{id}", deleteRecordHandler(deps.DB))
})
return r
}
// writeJSON writes a JSON response with the given status code.
func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(v)
}
// writeError writes a standard error response.
func writeError(w http.ResponseWriter, status int, message string) {
writeJSON(w, status, map[string]string{"error": message})
}

163
internal/server/zones.go Normal file
View File

@@ -0,0 +1,163 @@
package server
import (
"encoding/json"
"errors"
"net/http"
"github.com/go-chi/chi/v5"
"git.wntrmute.dev/kyle/mcns/internal/db"
)
type createZoneRequest struct {
Name string `json:"name"`
PrimaryNS string `json:"primary_ns"`
AdminEmail string `json:"admin_email"`
Refresh int `json:"refresh"`
Retry int `json:"retry"`
Expire int `json:"expire"`
MinimumTTL int `json:"minimum_ttl"`
}
func listZonesHandler(database *db.DB) http.HandlerFunc {
return func(w http.ResponseWriter, _ *http.Request) {
zones, err := database.ListZones()
if err != nil {
writeError(w, http.StatusInternalServerError, "failed to list zones")
return
}
if zones == nil {
zones = []db.Zone{}
}
writeJSON(w, http.StatusOK, map[string]any{"zones": zones})
}
}
func getZoneHandler(database *db.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
name := chi.URLParam(r, "zone")
zone, err := database.GetZone(name)
if errors.Is(err, db.ErrNotFound) {
writeError(w, http.StatusNotFound, "zone not found")
return
}
if err != nil {
writeError(w, http.StatusInternalServerError, "failed to get zone")
return
}
writeJSON(w, http.StatusOK, zone)
}
}
func createZoneHandler(database *db.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req createZoneRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
if req.Name == "" {
writeError(w, http.StatusBadRequest, "name is required")
return
}
if req.PrimaryNS == "" {
writeError(w, http.StatusBadRequest, "primary_ns is required")
return
}
if req.AdminEmail == "" {
writeError(w, http.StatusBadRequest, "admin_email is required")
return
}
// Apply defaults for SOA params.
if req.Refresh == 0 {
req.Refresh = 3600
}
if req.Retry == 0 {
req.Retry = 600
}
if req.Expire == 0 {
req.Expire = 86400
}
if req.MinimumTTL == 0 {
req.MinimumTTL = 300
}
zone, err := database.CreateZone(req.Name, req.PrimaryNS, req.AdminEmail, req.Refresh, req.Retry, req.Expire, req.MinimumTTL)
if errors.Is(err, db.ErrConflict) {
writeError(w, http.StatusConflict, err.Error())
return
}
if err != nil {
writeError(w, http.StatusInternalServerError, "failed to create zone")
return
}
writeJSON(w, http.StatusCreated, zone)
}
}
func updateZoneHandler(database *db.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
name := chi.URLParam(r, "zone")
var req createZoneRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
if req.PrimaryNS == "" {
writeError(w, http.StatusBadRequest, "primary_ns is required")
return
}
if req.AdminEmail == "" {
writeError(w, http.StatusBadRequest, "admin_email is required")
return
}
if req.Refresh == 0 {
req.Refresh = 3600
}
if req.Retry == 0 {
req.Retry = 600
}
if req.Expire == 0 {
req.Expire = 86400
}
if req.MinimumTTL == 0 {
req.MinimumTTL = 300
}
zone, err := database.UpdateZone(name, req.PrimaryNS, req.AdminEmail, req.Refresh, req.Retry, req.Expire, req.MinimumTTL)
if errors.Is(err, db.ErrNotFound) {
writeError(w, http.StatusNotFound, "zone not found")
return
}
if err != nil {
writeError(w, http.StatusInternalServerError, "failed to update zone")
return
}
writeJSON(w, http.StatusOK, zone)
}
}
func deleteZoneHandler(database *db.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
name := chi.URLParam(r, "zone")
err := database.DeleteZone(name)
if errors.Is(err, db.ErrNotFound) {
writeError(w, http.StatusNotFound, "zone not found")
return
}
if err != nil {
writeError(w, http.StatusInternalServerError, "failed to delete zone")
return
}
w.WriteHeader(http.StatusNoContent)
}
}

15
proto/mcns/v1/admin.proto Normal file
View File

@@ -0,0 +1,15 @@
syntax = "proto3";
package mcns.v1;
option go_package = "git.wntrmute.dev/kyle/mcns/gen/mcns/v1";
service AdminService {
rpc Health(HealthRequest) returns (HealthResponse);
}
message HealthRequest {}
message HealthResponse {
string status = 1;
}

26
proto/mcns/v1/auth.proto Normal file
View File

@@ -0,0 +1,26 @@
syntax = "proto3";
package mcns.v1;
option go_package = "git.wntrmute.dev/kyle/mcns/gen/mcns/v1";
service AuthService {
rpc Login(LoginRequest) returns (LoginResponse);
rpc Logout(LogoutRequest) returns (LogoutResponse);
}
message LoginRequest {
string username = 1;
string password = 2;
string totp_code = 3;
}
message LoginResponse {
string token = 1;
}
message LogoutRequest {
string token = 1;
}
message LogoutResponse {}

View File

@@ -0,0 +1,62 @@
syntax = "proto3";
package mcns.v1;
option go_package = "git.wntrmute.dev/kyle/mcns/gen/mcns/v1";
import "google/protobuf/timestamp.proto";
service RecordService {
rpc ListRecords(ListRecordsRequest) returns (ListRecordsResponse);
rpc CreateRecord(CreateRecordRequest) returns (Record);
rpc GetRecord(GetRecordRequest) returns (Record);
rpc UpdateRecord(UpdateRecordRequest) returns (Record);
rpc DeleteRecord(DeleteRecordRequest) returns (DeleteRecordResponse);
}
message Record {
int64 id = 1;
string zone = 2;
string name = 3;
string type = 4;
string value = 5;
int32 ttl = 6;
google.protobuf.Timestamp created_at = 7;
google.protobuf.Timestamp updated_at = 8;
}
message ListRecordsRequest {
string zone = 1;
string name = 2;
string type = 3;
}
message ListRecordsResponse {
repeated Record records = 1;
}
message CreateRecordRequest {
string zone = 1;
string name = 2;
string type = 3;
string value = 4;
int32 ttl = 5;
}
message GetRecordRequest {
int64 id = 1;
}
message UpdateRecordRequest {
int64 id = 1;
string name = 2;
string type = 3;
string value = 4;
int32 ttl = 5;
}
message DeleteRecordRequest {
int64 id = 1;
}
message DeleteRecordResponse {}

65
proto/mcns/v1/zone.proto Normal file
View File

@@ -0,0 +1,65 @@
syntax = "proto3";
package mcns.v1;
option go_package = "git.wntrmute.dev/kyle/mcns/gen/mcns/v1";
import "google/protobuf/timestamp.proto";
service ZoneService {
rpc ListZones(ListZonesRequest) returns (ListZonesResponse);
rpc CreateZone(CreateZoneRequest) returns (Zone);
rpc GetZone(GetZoneRequest) returns (Zone);
rpc UpdateZone(UpdateZoneRequest) returns (Zone);
rpc DeleteZone(DeleteZoneRequest) returns (DeleteZoneResponse);
}
message Zone {
int64 id = 1;
string name = 2;
string primary_ns = 3;
string admin_email = 4;
int32 refresh = 5;
int32 retry = 6;
int32 expire = 7;
int32 minimum_ttl = 8;
int64 serial = 9;
google.protobuf.Timestamp created_at = 10;
google.protobuf.Timestamp updated_at = 11;
}
message ListZonesRequest {}
message ListZonesResponse {
repeated Zone zones = 1;
}
message CreateZoneRequest {
string name = 1;
string primary_ns = 2;
string admin_email = 3;
int32 refresh = 4;
int32 retry = 5;
int32 expire = 6;
int32 minimum_ttl = 7;
}
message GetZoneRequest {
string name = 1;
}
message UpdateZoneRequest {
string name = 1;
string primary_ns = 2;
string admin_email = 3;
int32 refresh = 4;
int32 retry = 5;
int32 expire = 6;
int32 minimum_ttl = 7;
}
message DeleteZoneRequest {
string name = 1;
}
message DeleteZoneResponse {}

View File

@@ -1,26 +0,0 @@
; Node addresses for Metacircular platform.
; Maps node names to their network addresses.
;
; When MCNS is built, these will be managed via the MCNS API.
; Until then, this file is manually maintained.
$ORIGIN mcp.metacircular.net.
$TTL 300
@ IN SOA ns.mcp.metacircular.net. admin.metacircular.net. (
2026032501 ; serial (YYYYMMDDNN)
3600 ; refresh
600 ; retry
86400 ; expire
300 ; minimum TTL
)
IN NS ns.mcp.metacircular.net.
; --- Nodes ---
rift IN A 192.168.88.181
rift IN A 100.95.252.120
; ns record target — points to rift where CoreDNS runs.
ns IN A 192.168.88.181
ns IN A 100.95.252.120

View File

@@ -1,28 +0,0 @@
; Internal service addresses for Metacircular platform.
; Maps service names to the node where they currently run.
;
; When MCNS is built, MCP will manage these records dynamically.
; Until then, this file is manually maintained.
$ORIGIN svc.mcp.metacircular.net.
$TTL 300
@ IN SOA ns.mcp.metacircular.net. admin.metacircular.net. (
2026032601 ; serial (YYYYMMDDNN)
3600 ; refresh
600 ; retry
86400 ; expire
300 ; minimum TTL
)
IN NS ns.mcp.metacircular.net.
; --- Services on rift ---
metacrypt IN A 192.168.88.181
metacrypt IN A 100.95.252.120
mcr IN A 192.168.88.181
mcr IN A 100.95.252.120
sgard IN A 192.168.88.181
sgard IN A 100.95.252.120
mcp-agent IN A 192.168.88.181
mcp-agent IN A 100.95.252.120