-
Notifications
You must be signed in to change notification settings - Fork 2
Dummy DNS Server #164
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Dummy DNS Server #164
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| .DS_Store | ||
| .idea | ||
|
|
||
| ./boundary # Compiled binary from makefile | ||
| # Compiled binary from makefile | ||
| /boundary |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| package dnsdummy | ||
|
|
||
| import ( | ||
| "log/slog" | ||
|
|
||
| "github.com/miekg/dns" | ||
| ) | ||
|
|
||
| // DummyA is the IPv4 address returned for every A record query (arbitrary non-loopback). | ||
| const DummyA = "6.6.6.6" | ||
|
|
||
| // DummyAAAA is the IPv6 address returned for every AAAA record query (documentation prefix). | ||
| const DummyAAAA = "2001:db8::1" | ||
|
|
||
| // Server is a minimal DNS server that responds to every query with a dummy A record. | ||
| // Used inside the network namespace to prevent DNS exfiltration. | ||
| type Server struct { | ||
| udp *dns.Server | ||
| tcp *dns.Server | ||
| logger *slog.Logger | ||
| } | ||
|
|
||
| // NewServer creates a dummy DNS server that listens on addr (e.g. "127.0.0.1:53"). | ||
| func NewServer(addr string, logger *slog.Logger) *Server { | ||
| handler := dns.HandlerFunc(func(w dns.ResponseWriter, r *dns.Msg) { | ||
| m := new(dns.Msg) | ||
| m.SetReply(r) | ||
| m.Authoritative = true | ||
|
|
||
| for _, q := range r.Question { | ||
| switch q.Qtype { | ||
| case dns.TypeA: | ||
| rr, err := dns.NewRR(q.Name + " 1 IN A " + DummyA) | ||
| if err != nil { | ||
| continue | ||
| } | ||
| m.Answer = append(m.Answer, rr) | ||
| case dns.TypeAAAA: | ||
| rr, err := dns.NewRR(q.Name + " 1 IN AAAA " + DummyAAAA) | ||
| if err != nil { | ||
| continue | ||
| } | ||
| m.Answer = append(m.Answer, rr) | ||
| default: | ||
| m.Rcode = dns.RcodeSuccess | ||
| } | ||
| } | ||
|
|
||
| if err := w.WriteMsg(m); err != nil { | ||
| logger.Debug("dummy DNS: failed to write response", "error", err) | ||
| } | ||
| }) | ||
|
|
||
| udp := &dns.Server{ | ||
| Addr: addr, | ||
| Net: "udp", | ||
| Handler: handler, | ||
| } | ||
| tcp := &dns.Server{ | ||
| Addr: addr, | ||
| Net: "tcp", | ||
| Handler: handler, | ||
| } | ||
|
|
||
| return &Server{udp: udp, tcp: tcp, logger: logger} | ||
| } | ||
|
|
||
| // ListenAndServe starts the UDP and TCP servers in goroutines and returns immediately. | ||
| // Logger must be non-nil; server errors are logged via s.logger. | ||
| func (s *Server) ListenAndServe() { | ||
| go func() { | ||
| if err := s.tcp.ListenAndServe(); err != nil { | ||
| s.logger.Error("dummy DNS TCP server failed", "error", err) | ||
| } | ||
| }() | ||
| go func() { | ||
| if err := s.udp.ListenAndServe(); err != nil { | ||
| s.logger.Error("dummy DNS UDP server failed", "error", err) | ||
| } | ||
| }() | ||
| } | ||
|
|
||
| // Shutdown stops the servers. | ||
| func (s *Server) Shutdown() { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did you mean to call this somewhere?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it would be nice to call it in boundary-child process, after target process is finished, but it requires some refactoring. |
||
| if err := s.udp.Shutdown(); err != nil { | ||
| s.logger.Error("dummy DNS UDP server shutdown failed", "error", err) | ||
| } | ||
| if err := s.tcp.Shutdown(); err != nil { | ||
| s.logger.Error("dummy DNS TCP server shutdown failed", "error", err) | ||
| } | ||
| } | ||
|
|
||
| // DefaultDummyDNSPort is the port the dummy DNS server listens on (high port to avoid CAP_NET_BIND_SERVICE). | ||
| // Traffic to port 53 is DNAT'd to this port in the namespace. | ||
| const DefaultDummyDNSPort = "5353" | ||
evgeniy-scherbina marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| package nsjail | ||
|
|
||
| import ( | ||
| "log/slog" | ||
| "os/exec" | ||
|
|
||
| "github.com/coder/boundary/dnsdummy" | ||
| "golang.org/x/sys/unix" | ||
| ) | ||
|
|
||
| // StartDummyDNSAndRedirect starts a dummy DNS server in-process (goroutine) listening on | ||
| // 127.0.0.1:5353 and redirects all DNS traffic (UDP/TCP port 53) in the namespace to it | ||
| // via iptables. This prevents DNS exfiltration: all DNS queries get a dummy response (6.6.6.6). | ||
| // Must be called from inside the network namespace. | ||
| func StartDummyDNSAndRedirect(logger *slog.Logger) error { | ||
| addr := "127.0.0.1:" + dnsdummy.DefaultDummyDNSPort | ||
| server := dnsdummy.NewServer(addr, logger) | ||
| server.ListenAndServe() | ||
| logger.Debug("dummy DNS server started in-process", "addr", addr) | ||
|
|
||
| // Redirect all DNS (UDP and TCP port 53) to 127.0.0.1:5353 | ||
| runner := newCommandRunner([]*command{ | ||
| // Allow loopback-destined traffic to pass through NAT so DNAT to 127.0.0.1 works. | ||
| // Best-effort: in some environments (e.g. Sysbox/Docker) this command may not work, | ||
| // but DNS setup should work anyway. | ||
| newCommandWithIgnoreErr( | ||
| "Allow loopback-destined traffic for dummy DNS (route_localnet)", | ||
| exec.Command("sysctl", "-w", "net.ipv4.conf.all.route_localnet=1"), | ||
| []uintptr{uintptr(unix.CAP_NET_ADMIN)}, | ||
| "*", | ||
| ), | ||
| newCommand( | ||
| "Redirect UDP DNS to dummy server", | ||
| exec.Command("iptables", "-t", "nat", "-A", "OUTPUT", "-p", "udp", "--dport", "53", "-j", "DNAT", "--to-destination", addr), | ||
| []uintptr{uintptr(unix.CAP_NET_ADMIN)}, | ||
| ), | ||
| newCommand( | ||
| "Redirect TCP DNS to dummy server", | ||
| exec.Command("iptables", "-t", "nat", "-A", "OUTPUT", "-p", "tcp", "--dport", "53", "-j", "DNAT", "--to-destination", addr), | ||
| []uintptr{uintptr(unix.CAP_NET_ADMIN)}, | ||
| ), | ||
| }) | ||
| if err := runner.run(); err != nil { | ||
evgeniy-scherbina marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| return err | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this
defaultcase is redundant becausem.SetReply()does exactly this internally