summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore5
-rw-r--r--Makefile5
-rw-r--r--README.md109
-rw-r--r--internal/config/config.go18
-rw-r--r--internal/server/server.go40
-rw-r--r--linum.toml11
-rw-r--r--main.go17
7 files changed, 165 insertions, 40 deletions
diff --git a/.gitignore b/.gitignore
index 2eae826..f1469da 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,7 @@
-*.md
+*.sh
build/
+*.pem
+*.key
etc/blocklist/*.txt
+*.md
todo.md
diff --git a/Makefile b/Makefile
index a27b1d7..bd66e11 100644
--- a/Makefile
+++ b/Makefile
@@ -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
diff --git a/README.md b/README.md
index 5ceeb66..3cf0ed3 100644
--- a/README.md
+++ b/README.md
@@ -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()
}
diff --git a/linum.toml b/linum.toml
index 3e4637f..4fbde64 100644
--- a/linum.toml
+++ b/linum.toml
@@ -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"
diff --git a/main.go b/main.go
index 9a341fb..aace165 100644
--- a/main.go
+++ b/main.go
@@ -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 {