add initial simple and naive solution
This commit is contained in:
commit
7720b19062
4 changed files with 261 additions and 0 deletions
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
.env
|
||||
.env.*
|
||||
|
||||
*.db
|
||||
|
||||
13
go.mod
Normal file
13
go.mod
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
module git.juancwu.dev/juancwu/go-ddns
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/mattn/go-sqlite3 v1.14.32 // indirect
|
||||
github.com/miekg/dns v1.1.69 // indirect
|
||||
golang.org/x/mod v0.30.0 // indirect
|
||||
golang.org/x/net v0.47.0 // indirect
|
||||
golang.org/x/sync v0.18.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/tools v0.39.0 // indirect
|
||||
)
|
||||
14
go.sum
Normal file
14
go.sum
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
||||
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/miekg/dns v1.1.69 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc=
|
||||
github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g=
|
||||
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
||||
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
||||
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
||||
229
main.go
Normal file
229
main.go
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
const (
|
||||
DBPath = "./dns_records.db"
|
||||
DNSPort = "53"
|
||||
APIPort = ":8080"
|
||||
DefaultTTL = 300
|
||||
)
|
||||
|
||||
type RecordRequest struct {
|
||||
Domain string `json:"domain"`
|
||||
IP string `json:"ip"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type UpstreamRequest struct {
|
||||
Address string `json:"address"`
|
||||
}
|
||||
|
||||
func initDB() *sql.DB {
|
||||
db, err := sql.Open("sqlite3", DBPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
queryRecords := `
|
||||
CREATE TABLE IF NOT EXISTS records (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
domain TEXT NOT NULL UNIQUE,
|
||||
ip TEXT NOT NULL,
|
||||
record_type TEXT DEFAULT 'A'
|
||||
);`
|
||||
|
||||
queryUpstreams := `
|
||||
CREATE TABLE IF NOT EXISTS upstreams (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
address TEXT NOT NULL UNIQUE
|
||||
);`
|
||||
|
||||
if _, err := db.Exec(queryRecords); err != nil {
|
||||
log.Fatalf("Error creating records table: %v", err)
|
||||
}
|
||||
if _, err := db.Exec(queryUpstreams); err != nil {
|
||||
log.Fatalf("Error creating upstreams table: %v", err)
|
||||
}
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
type DNSResolver struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func (resolver *DNSResolver) handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) {
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(r)
|
||||
m.Compress = false
|
||||
|
||||
switch r.Opcode {
|
||||
case dns.OpcodeQuery:
|
||||
for _, q := range m.Question {
|
||||
ip, err := resolver.getRecordFromDB(q.Name)
|
||||
if err == nil && ip != "" {
|
||||
log.Printf("[LOCAL] Resolved %s -> %s", q.Name, ip)
|
||||
rr, err := dns.NewRR(fmt.Sprintf("%s %d A %s", q.Name, DefaultTTL, ip))
|
||||
if err == nil {
|
||||
m.Answer = append(m.Answer, rr)
|
||||
}
|
||||
} else {
|
||||
resp, err := resolver.resolveUpstream(r)
|
||||
if err == nil && resp != nil {
|
||||
log.Printf("[UPSTREAM] Forwarded %s", q.Name)
|
||||
m.Answer = resp.Answer
|
||||
m.Ns = resp.Ns
|
||||
m.Extra = resp.Extra
|
||||
m.Rcode = resp.Rcode
|
||||
} else {
|
||||
log.Printf("[ERROR] Could not resolve %s", q.Name)
|
||||
m.Rcode = dns.RcodeServerFailure
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
w.WriteMsg(m)
|
||||
}
|
||||
|
||||
func (resolver *DNSResolver) getRecordFromDB(domain string) (string, error) {
|
||||
// DNS queries usually come with a trailing dot (e.g., "google.com.")
|
||||
// Ensure consistency by checking both with and without it if needed,
|
||||
// or enforcing it in the DB. Here we assume DB stores with trailing dot.
|
||||
var ip string
|
||||
err := resolver.db.QueryRow("SELECT ip FROM records WHERE domain = ?", domain).Scan(&ip)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
func (resolver *DNSResolver) resolveUpstream(r *dns.Msg) (*dns.Msg, error) {
|
||||
rows, err := resolver.db.Query("SELECT address FROM upstreams")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
c := new(dns.Client)
|
||||
c.Net = "udp"
|
||||
|
||||
c.Timeout = 5 * time.Second
|
||||
|
||||
for rows.Next() {
|
||||
var upstreamAddr string
|
||||
if err := rows.Scan(&upstreamAddr); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
resp, _, err := c.Exchange(r, upstreamAddr)
|
||||
if err == nil && resp != nil && resp.Rcode != dns.RcodeServerFailure {
|
||||
return resp, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("all upstreams failed")
|
||||
}
|
||||
|
||||
func startAPIServer(db *sql.DB) {
|
||||
http.HandleFunc("/records", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodPost {
|
||||
var req RecordRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Normalize domain to ensure trailing dot
|
||||
if !strings.HasSuffix(req.Domain, ".") {
|
||||
req.Domain += "."
|
||||
}
|
||||
|
||||
_, err := db.Exec("INSERT OR REPLACE INTO records (domain, ip, record_type) VALUES (?, ?, ?)", req.Domain, req.IP, "A")
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
fmt.Fprintf(w, "Added %s -> %s", req.Domain, req.IP)
|
||||
|
||||
} else if r.Method == http.MethodDelete {
|
||||
var req RecordRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if !strings.HasSuffix(req.Domain, ".") {
|
||||
req.Domain += "."
|
||||
}
|
||||
|
||||
_, err := db.Exec("DELETE FROM records WHERE domain = ?", req.Domain)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(w, "Deleted %s", req.Domain)
|
||||
}
|
||||
})
|
||||
|
||||
http.HandleFunc("/upstreams", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodPost {
|
||||
var req UpstreamRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
_, err := db.Exec("INSERT INTO upstreams (address) VALUES (?)", req.Address)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
fmt.Fprintf(w, "Added upstream %s", req.Address)
|
||||
} else if r.Method == http.MethodDelete {
|
||||
var req UpstreamRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
_, err := db.Exec("DELETE FROM upstreams WHERE address = ?", req.Address)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(w, "Deleted upstream %s", req.Address)
|
||||
}
|
||||
})
|
||||
|
||||
log.Printf("API Listening on %s...", APIPort)
|
||||
log.Fatal(http.ListenAndServe(APIPort, nil))
|
||||
}
|
||||
|
||||
func main() {
|
||||
db := initDB()
|
||||
defer db.Close()
|
||||
|
||||
go startAPIServer(db)
|
||||
|
||||
resolver := &DNSResolver{db: db}
|
||||
|
||||
dns.HandleFunc(".", resolver.handleDNSRequest)
|
||||
|
||||
server := &dns.Server{Addr: ":" + DNSPort, Net: "udp"}
|
||||
log.Printf("DNS Server listening on port %s...", DNSPort)
|
||||
|
||||
if err := server.ListenAndServe(); err != nil {
|
||||
log.Fatalf("Failed to set up DNS server: %v", err)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue