diff options
| author | radhitya <alif@radhitya.org> | 2026-06-21 13:11:55 +0700 |
|---|---|---|
| committer | radhitya <alif@radhitya.org> | 2026-06-21 13:11:55 +0700 |
| commit | 81661cc8deaacbff3497f0c9ef2625e98257ef76 (patch) | |
| tree | 704e61822482cf5a9f18ef3b34cea2d4f78be4f3 | |
| parent | b7359e1d45f505171356bcae3c7d5e2341ecc859 (diff) | |
dot, readme
| -rw-r--r-- | .gitignore | 5 | ||||
| -rw-r--r-- | Makefile | 5 | ||||
| -rw-r--r-- | README.md | 109 | ||||
| -rw-r--r-- | internal/config/config.go | 18 | ||||
| -rw-r--r-- | internal/server/server.go | 40 | ||||
| -rw-r--r-- | linum.toml | 11 | ||||
| -rw-r--r-- | main.go | 17 |
7 files changed, 165 insertions, 40 deletions
@@ -1,4 +1,7 @@ -*.md +*.sh build/ +*.pem +*.key etc/blocklist/*.txt +*.md todo.md @@ -31,11 +31,12 @@ clean: run: build ./$(OUTPUT) -install: build +install: install -d $(DESTDIR)$(BINDIR) install -m 0755 $(OUTPUT) $(DESTDIR)$(BINDIR)/$(BINARY) + install -d $(DESTDIR)/etc/linum/blocklist -install-service: install +install-service: install -d $(DESTDIR)$(SYSTEMD_DIR) install -m 0644 etc/linum.service $(DESTDIR)$(SYSTEMD_DIR)/linum.service @@ -1,56 +1,113 @@ -## Linum +# Linum -A simple DNS recursive resolver written in Golang +A simple DNS recursive resolver written in Golang. Linum supports both +recursive resolution from root hints and forwarding to upstream resolvers with +built-in hosts blocking. -### Features +## Features -- Forward resolver -- Adblock +- UDP, TCP, DoT listeners +- Recursive & foreward modes +- Host blocking -### Config Reference +## Requirements -Save it to `/etc/linum.toml` +- Go 1.26+ +- Linux +- Root to bind port 53 / 853 -```toml +## Quickstart + +```bash +git clone https://codeberg.org/radhitya/linum.git +cd linum +make +./build/linum -config linum.toml +``` + +Test with dig: + +```bash +dig @127.0.0.1 -p 5353 example.com +``` + +### Build & Install + +From source + +```bash +make +sudo make install +sudo make install-config +``` + +and install systemd service + +```bash +sudo make install-service +sudo systemctl enable --now linum +``` + +## Config Reference + +Save as /etc/linum/linum.toml + +``` [server] listen_udp = ":5353" listen_tcp = ":5353" listen_doh = ":8443" +listen_dot = ":853" [cache] max_entries = 100000 -db_path = "/tmp/cache.db" +db_path = "/var/cache/linum/cache.db" [resolver] -mode = "forward" +mode = "recursive" # "recursive" or "forward" timeout = "2s" max_delegations = 30 -forwarders = ["1.1.1.1"] +# forwarders = ["1.1.1.1", "8.8.8.8"] # only used when mode = "forward" [blocklist] -response = "zero_ip" +response = "zero_ip" # "zero_ip" or "nxdomain" files = ["etc/blocklist/*.txt"] -#urls = [ - # "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts", -#] +# urls = [ +# "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts", +# ] + +[acl] +allow = ["127.0.0.1/8", "::1/128"] +rate_limit_qps = 50 +rate_limit_burst = 10 + +[admin] +listen = "127.0.0.1:8080" + +[tls] +cert = "/etc/linum/linum-cert.pem" +key = "/etc/linum/linum-key.pem" [log] level = "info" ``` -### Build and Run +## Blocklist Format -First, build the program: +Linum supports several rule formats: -```bash -$ go get codeberg.org/miekg/dns -$ go get modernc.org/sqlite -$ go get github.com/BurntSushi/toml -$ make +```txt +# hosts format +0.0.0.0 example.com +# AdGuard format +||example.com^ + +# Exception / whitelist +@@||example.com^ ``` -And run the program +Set response = "zero_ip" to return 0.0.0.0 / ::, or response = "nxdomain" to return NXDOMAIN. -```bash -$ ./build/linum -``` +- [codeberg.org/miekg/dns](https://codeberg.org/miekg/dns) +- [github.com/BurntSushi/toml](https://github.com/BurntSushi/toml) +- [modernc.org/sqlite](https://modernc.org/sqlite) diff --git a/internal/config/config.go b/internal/config/config.go index b2c88ee..42af236 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -15,14 +15,20 @@ type Config struct { Admin AdminConfig `toml:"admin"` ACL ACLConfig `toml:"acl"` Log LogConfig `toml:"log"` + TLS TLSConfig `toml:"tls"` } type ServerConfig struct { ListenUDP string `toml:"listen_udp"` ListenTCP string `toml:"listen_tcp"` ListenDOH string `toml:"listen_doh"` + ListenDoT string `toml:"listen_dot"` } +type TLSConfig struct { + Cert string `toml:"cert"` + Key string `toml:"key"` +} type CacheConfig struct { MaxEntries int `toml:"max_entries"` DBPath string `toml:"db_path"` @@ -164,6 +170,15 @@ func Merge(dst, src Config) Config { if src.Admin.Listen != "" { dst.Admin.Listen = src.Admin.Listen } + if src.Server.ListenDoT != "" { + dst.Server.ListenDoT = src.Server.ListenDoT + } + if src.TLS.Cert != "" { + dst.TLS.Cert = src.TLS.Cert + } + if src.TLS.Key != "" { + dst.TLS.Key = src.TLS.Key + } return dst } @@ -202,5 +217,8 @@ func (c Config) Validate() error { if c.Resolver.Mode == "forward" && len(c.Resolver.Forwarders) == 0 { return fmt.Errorf("resolver mode=forward requires at least one forwarder") } + if c.Server.ListenDoT != "" && (c.TLS.Cert == "" || c.TLS.Key == "") { + return fmt.Errorf("listen_dot requires tls.cert and tls.key") + } return nil } diff --git a/internal/server/server.go b/internal/server/server.go index a90e5ac..74add95 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -2,6 +2,7 @@ package server import ( "context" + "crypto/tls" "fmt" "log/slog" "net" @@ -30,12 +31,13 @@ type Server struct { admin *Admin aclNets []*net.IPNet rateLimiter *rateLimiter + dot *dns.Server - mu sync.RWMutex - upUDP bool - upTCP bool - upDoH bool - + mu sync.RWMutex + upUDP bool + upTCP bool + upDoH bool + upDoT bool cancel context.CancelFunc } @@ -45,10 +47,12 @@ func (s *Server) Ready() bool { wantUDP := s.cfg.ListenUDP != "" wantTCP := s.cfg.ListenTCP != "" wantDoH := s.cfg.ListenDOH != "" - return (!wantUDP || s.upUDP) && (!wantTCP || s.upTCP) && (!wantDoH || s.upDoH) + wantDoT := s.cfg.ListenDoT != "" + return (!wantUDP || s.upUDP) && (!wantTCP || s.upTCP) && (!wantDoH || s.upDoH) && + (!wantDoT || s.upDoT) } -func New(udpAddr, tcpAddr, dohAddr string, logger *slog.Logger, r *resolver.Resolver, c *cache.Cache, bl *blocklist.Blocklist, cfg config.Config) (*Server, error) { +func New(udpAddr, tcpAddr, dohAddr, dotAddr string, tlsCfg *tls.Config, logger *slog.Logger, r *resolver.Resolver, c *cache.Cache, bl *blocklist.Blocklist, cfg config.Config) (*Server, error) { baseCtx, cancel := context.WithCancel(context.Background()) s := &Server{ logger: logger, @@ -94,6 +98,16 @@ func New(udpAddr, tcpAddr, dohAddr string, logger *slog.Logger, r *resolver.Reso } } + if dotAddr != "" && tlsCfg != nil { + + s.dot = &dns.Server{ + Addr: dotAddr, + Net: "tcp", + Handler: mux, + TLSConfig: tlsCfg, + ReadTimeout: 5 * time.Second, + } + } if dohAddr != "" { dohMux := http.NewServeMux() dohMux.HandleFunc("/dns-query", s.dohHandler) @@ -122,6 +136,9 @@ func (s *Server) Run(ctx context.Context) error { if s.doh != nil { s.upDoH = true } + if s.dot != nil { + s.upDoT = true + } s.mu.Unlock() if s.admin != nil { @@ -149,6 +166,12 @@ func (s *Server) Run(ctx context.Context) error { errCh <- s.doh.ListenAndServe() }() } + if s.dot != nil { + go func() { + s.logger.Info("dot listener active", "addr", s.dot.Addr) + errCh <- s.dot.ListenAndServe() + }() + } select { case <-ctx.Done(): @@ -180,6 +203,9 @@ func (s *Server) Close() error { if s.tcp != nil { s.tcp.Shutdown(context.Background()) } + if s.dot != nil { + s.dot.Shutdown(context.Background()) + } if s.doh != nil { s.doh.Close() } @@ -1,7 +1,8 @@ [server] -listen_udp = ":5354" -listen_tcp = ":5354" +listen_udp = ":53" +listen_tcp = ":53" listen_doh = ":8443" +listen_dot = ":853" [cache] max_entries = 100000 @@ -15,7 +16,7 @@ forwarders = ["1.1.1.1"] [blocklist] response = "zero_ip" -files = ["etc/blocklist/*.txt"] +files = ["/etc/linum/blocklist/*.txt"] #urls = [ # "https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts", #] @@ -28,5 +29,9 @@ allow = ["127.0.0.1/8", "::1/128"] rate_limit_qps = 50 rate_limit_burst = 10 +[tls] +cert = "/tmp/linum-cert.pem" +key = "/tmp/linum-key.pem" + [log] level = "info" @@ -2,6 +2,7 @@ package main import ( "context" + "crypto/tls" "fmt" "linum/internal/blocklist" "linum/internal/cache" @@ -65,6 +66,19 @@ func main() { } defer c.Stop() + var tlsCfg *tls.Config + if cfg.Server.ListenDoT != "" { + cert, err := tls.LoadX509KeyPair(cfg.TLS.Cert, cfg.TLS.Key) + if err != nil { + logger.Error("load tls cert failed", "err", err) + os.Exit(1) + } + tlsCfg = &tls.Config{ + Certificates: []tls.Certificate{cert}, + NextProtos: []string{"dot"}, + } + logger.Info("dot listener configured", "addr", cfg.Server.ListenDoT) + } var bl *blocklist.Blocklist if len(cfg.Blocklist.Files) > 0 || len(cfg.Blocklist.URLs) > 0 { resp := blocklist.ResponseZeroIP @@ -101,7 +115,7 @@ func main() { ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() - srv, err := server.New(cfg.Server.ListenUDP, cfg.Server.ListenTCP, cfg.Server.ListenDOH, logger, r, c, bl, cfg) + srv, err := server.New(cfg.Server.ListenUDP, cfg.Server.ListenTCP, cfg.Server.ListenDOH, cfg.Server.ListenDoT, tlsCfg, logger, r, c, bl, cfg) if err != nil { logger.Error("create server failed", "err", err) os.Exit(1) @@ -116,6 +130,7 @@ func main() { "udp", cfg.Server.ListenUDP, "tcp", cfg.Server.ListenTCP, "doh", cfg.Server.ListenDOH, + "dot", cfg.Server.ListenDoT, ) if err := srv.Run(ctx); err != nil && err != context.Canceled { |
