mirror of
https://github.com/DevVoxel/vectordns-server.git
synced 2026-02-27 01:40:12 +00:00
Initial commit: Go DNS microservice boilerplate
This commit is contained in:
29
internal/config/config.go
Normal file
29
internal/config/config.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Port string
|
||||
APIKey string
|
||||
CORSOrigins []string
|
||||
LogFormat string
|
||||
}
|
||||
|
||||
func Load() *Config {
|
||||
return &Config{
|
||||
Port: getEnv("PORT", "8080"),
|
||||
APIKey: getEnv("API_KEY", ""),
|
||||
CORSOrigins: strings.Split(getEnv("CORS_ORIGINS", "http://localhost:3000"), ","),
|
||||
LogFormat: getEnv("LOG_FORMAT", "text"),
|
||||
}
|
||||
}
|
||||
|
||||
func getEnv(key, fallback string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
112
internal/dns/resolver.go
Normal file
112
internal/dns/resolver.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
mdns "github.com/miekg/dns"
|
||||
)
|
||||
|
||||
var defaultTypes = []string{"A", "AAAA", "MX", "TXT", "NS", "CNAME", "SOA", "CAA", "SRV"}
|
||||
|
||||
const defaultResolver = "8.8.8.8:53"
|
||||
|
||||
var typeMap = map[string]uint16{
|
||||
"A": mdns.TypeA,
|
||||
"AAAA": mdns.TypeAAAA,
|
||||
"MX": mdns.TypeMX,
|
||||
"TXT": mdns.TypeTXT,
|
||||
"NS": mdns.TypeNS,
|
||||
"CNAME": mdns.TypeCNAME,
|
||||
"SOA": mdns.TypeSOA,
|
||||
"CAA": mdns.TypeCAA,
|
||||
"SRV": mdns.TypeSRV,
|
||||
}
|
||||
|
||||
func Lookup(req LookupRequest) (*LookupResponse, error) {
|
||||
domain := req.Domain
|
||||
if !strings.HasSuffix(domain, ".") {
|
||||
domain += "."
|
||||
}
|
||||
|
||||
types := req.Types
|
||||
if len(types) == 0 {
|
||||
types = defaultTypes
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
records := make(map[string][]Record)
|
||||
hasDNSSEC := false
|
||||
|
||||
c := new(mdns.Client)
|
||||
c.Net = "udp"
|
||||
|
||||
for _, t := range types {
|
||||
qtype, ok := typeMap[strings.ToUpper(t)]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
m := new(mdns.Msg)
|
||||
m.SetQuestion(domain, qtype)
|
||||
m.SetEdns0(4096, true)
|
||||
|
||||
r, _, err := c.Exchange(m, defaultResolver)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if r.AuthenticatedData {
|
||||
hasDNSSEC = true
|
||||
}
|
||||
|
||||
recs := parseRecords(r.Answer, strings.ToUpper(t))
|
||||
if len(recs) > 0 {
|
||||
records[strings.ToUpper(t)] = recs
|
||||
}
|
||||
}
|
||||
|
||||
return &LookupResponse{
|
||||
Domain: req.Domain,
|
||||
Records: records,
|
||||
Resolver: "8.8.8.8",
|
||||
QueryTimeMs: time.Since(start).Milliseconds(),
|
||||
DNSSEC: hasDNSSEC,
|
||||
Timestamp: time.Now().UTC(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func parseRecords(answers []mdns.RR, qtype string) []Record {
|
||||
var records []Record
|
||||
for _, ans := range answers {
|
||||
rec := Record{TTL: ans.Header().Ttl}
|
||||
switch rr := ans.(type) {
|
||||
case *mdns.A:
|
||||
rec.Value = rr.A.String()
|
||||
case *mdns.AAAA:
|
||||
rec.Value = rr.AAAA.String()
|
||||
case *mdns.MX:
|
||||
rec.Value = strings.TrimSuffix(rr.Mx, ".")
|
||||
rec.Priority = rr.Preference
|
||||
case *mdns.TXT:
|
||||
rec.Value = strings.Join(rr.Txt, " ")
|
||||
case *mdns.NS:
|
||||
rec.Value = strings.TrimSuffix(rr.Ns, ".")
|
||||
case *mdns.CNAME:
|
||||
rec.Value = strings.TrimSuffix(rr.Target, ".")
|
||||
case *mdns.SOA:
|
||||
rec.Value = fmt.Sprintf("%s %s %d %d %d %d %d",
|
||||
rr.Ns, rr.Mbox, rr.Serial, rr.Refresh, rr.Retry, rr.Expire, rr.Minttl)
|
||||
case *mdns.CAA:
|
||||
rec.Value = fmt.Sprintf("%d %s \"%s\"", rr.Flag, rr.Tag, rr.Value)
|
||||
case *mdns.SRV:
|
||||
rec.Value = fmt.Sprintf("%d %d %d %s", rr.Priority, rr.Weight, rr.Port, strings.TrimSuffix(rr.Target, "."))
|
||||
rec.Priority = rr.Priority
|
||||
default:
|
||||
continue
|
||||
}
|
||||
records = append(records, rec)
|
||||
}
|
||||
return records
|
||||
}
|
||||
23
internal/dns/types.go
Normal file
23
internal/dns/types.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package dns
|
||||
|
||||
import "time"
|
||||
|
||||
type LookupRequest struct {
|
||||
Domain string `json:"domain"`
|
||||
Types []string `json:"types,omitempty"`
|
||||
}
|
||||
|
||||
type LookupResponse struct {
|
||||
Domain string `json:"domain"`
|
||||
Records map[string][]Record `json:"records"`
|
||||
Resolver string `json:"resolver"`
|
||||
QueryTimeMs int64 `json:"query_time_ms"`
|
||||
DNSSEC bool `json:"dnssec"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
}
|
||||
|
||||
type Record struct {
|
||||
Value string `json:"value"`
|
||||
TTL uint32 `json:"ttl"`
|
||||
Priority uint16 `json:"priority,omitempty"`
|
||||
}
|
||||
41
internal/handler/dns.go
Normal file
41
internal/handler/dns.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"github.com/vectordns/server/internal/dns"
|
||||
)
|
||||
|
||||
func DNSLookup(w http.ResponseWriter, r *http.Request) {
|
||||
var req dns.LookupRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid request body", "INVALID_BODY")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Domain == "" {
|
||||
writeError(w, http.StatusBadRequest, "domain is required", "MISSING_DOMAIN")
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := dns.Lookup(req)
|
||||
if err != nil {
|
||||
slog.Error("dns lookup failed", "domain", req.Domain, "error", err)
|
||||
writeError(w, http.StatusInternalServerError, "dns lookup failed", "LOOKUP_FAILED")
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}
|
||||
|
||||
func writeError(w http.ResponseWriter, status int, msg, code string) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"error": msg,
|
||||
"code": code,
|
||||
})
|
||||
}
|
||||
14
internal/handler/health.go
Normal file
14
internal/handler/health.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func Health(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"status": "ok",
|
||||
"version": "0.1.0",
|
||||
})
|
||||
}
|
||||
30
internal/middleware/apikey.go
Normal file
30
internal/middleware/apikey.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func APIKey(key string) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if key == "" {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
provided := r.Header.Get("X-API-Key")
|
||||
if provided != key {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"error": "invalid or missing API key",
|
||||
"code": "UNAUTHORIZED",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user