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