Commit 9c7d940
Changed files (5)
ai/client.go
@@ -0,0 +1,54 @@
+package ai
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "strings"
+ "time"
+)
+
+var client = &http.Client{
+ Timeout: 10 * time.Second,
+}
+
+func Ask(prompt string) (string, error) {
+ payload := map[string]any{
+ "model": "llama3-chatqa:8b",
+ "prompt": prompt,
+ "stream": false,
+ "think": false,
+ }
+
+ body, err := json.Marshal(payload)
+ if err != nil {
+ return "", fmt.Errorf("marshal: %w", err)
+ }
+
+ resp, err := client.Post(
+ "http://localhost:11434/api/generate",
+ "application/json",
+ bytes.NewReader(body),
+ )
+ if err != nil {
+ return "", fmt.Errorf("http post: %w", err)
+ }
+ defer resp.Body.Close()
+
+ respBody, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return "", fmt.Errorf("read body: %w", err)
+ }
+
+ var result struct {
+ Response string `json:"response"`
+ }
+
+ if err := json.Unmarshal(respBody, &result); err != nil {
+ return "", fmt.Errorf("unmarshal: %w", err)
+ }
+
+ return strings.TrimSpace(result.Response), nil
+}
handlers/commands.go
@@ -0,0 +1,47 @@
+package handlers
+
+import (
+ "dnsense/ai"
+ "dnsense/dns"
+ "fmt"
+)
+
+type command struct {
+ prompt string
+}
+
+var commands = map[string]command{
+ "explain": {
+ prompt: "Explain '%s' in 2-3 sentences. Be concise, technical, and clear. No markdown.",
+ },
+ "tldr": {
+ prompt: "Give a TL;DR of '%s' in one sentence, max 200 characters. No markdown.",
+ },
+ "define": {
+ prompt: "Define '%s' in one sentence like a dictionary. Max 200 characters. No markdown.",
+ },
+ "ask": {
+ prompt: "Answer this question factually and concisely in 2-3 sentences. No markdown. No preamble. Question: '%s'",
+ },
+}
+
+func handleCMD(req *dns.Message, cmdName string, input string) *dns.Message {
+ cmd, ok := commands[cmdName]
+ if !ok {
+ return errorResponse(req, "unknown command: "+cmdName)
+ }
+
+ text, err := ai.Ask(fmt.Sprintf(cmd.prompt, input))
+ if err != nil {
+ return errorResponse(req, "ai error: "+err.Error())
+ }
+
+ rr, err := dns.NewTXTRecord(req.Questions[0].Name, 300, text)
+ if err != nil {
+ return errorResponse(req, "encoding error")
+ }
+
+ resp := baseResponse(req)
+ resp.Answers = append(resp.Answers, *rr)
+ return resp
+}
handlers/routers.go
@@ -0,0 +1,53 @@
+package handlers
+
+import (
+ "dnsense/dns"
+ "strings"
+)
+
+func baseResponse(req *dns.Message) *dns.Message {
+ resp := &dns.Message{}
+ resp.Header = req.Header
+ resp.Header.SetResponse()
+ resp.Header.SetAA()
+ resp.Header.Flags &^= (1 << 5) // clear AD bit
+ resp.Header.ARCount = 0
+ resp.Questions = req.Questions
+ return resp
+}
+
+func errorResponse(req *dns.Message, msg string) *dns.Message {
+ resp := baseResponse(req)
+
+ rr, err := dns.NewTXTRecord(req.Questions[0].Name, 30, "error: "+msg)
+ if err != nil {
+ return resp
+ }
+
+ resp.Answers = append(resp.Answers, *rr)
+ return resp
+}
+
+func Route(req *dns.Message) *dns.Message {
+ if len(req.Questions) == 0 {
+ return errorResponse(req, "no question")
+ }
+
+ q := req.Questions[0]
+
+ if q.Type != dns.TypeTXT {
+ return errorResponse(req, "only TXT queries supported")
+ }
+
+ name := strings.TrimSuffix(q.Name, ".")
+ labels := strings.SplitN(name, ".", 3)
+
+ if len(labels) < 2 {
+ return errorResponse(req, "format: <command>.<input>")
+ }
+
+ command := strings.ToLower(labels[0])
+ input := strings.ReplaceAll(labels[1], "-", " ")
+
+ return handleCMD(req, command, input)
+}
go.mod
@@ -0,0 +1,3 @@
+module dnsense
+
+go 1.25.5
main.go
@@ -0,0 +1,16 @@
+package main
+
+import (
+ "dnsense/dns"
+ "dnsense/handlers"
+ "log"
+)
+
+func main() {
+ server := dns.NewServer("0.0.0.0:8053", func(req *dns.Message) *dns.Message {
+ return handlers.Route(req)
+ })
+
+ log.Println("dns running on :8053")
+ log.Fatal(server.Start())
+}