package main import ( "context" "crypto/tls" "fmt" "linum/internal/blocklist" "linum/internal/cache" "linum/internal/config" "linum/internal/resolver" "linum/internal/server" "log/slog" "os" "os/signal" "path/filepath" "syscall" "time" ) func main() { flags := config.ParseFlags() cfg := config.Default() if _, err := os.Stat(flags.Config); err == nil { fileCfg, err := config.LoadFile(flags.Config) if err != nil { slog.Error("invalid config file", "err", err) os.Exit(1) } cfg = config.Merge(cfg, fileCfg) } cfg = flags.Apply(cfg) if err := cfg.Validate(); err != nil { slog.Error("config validation failed", "err", err) os.Exit(1) } var lvl slog.Level _ = lvl.UnmarshalText([]byte(cfg.Log.Level)) logger := slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: lvl})) slog.SetDefault(logger) fmt.Println("linum - simple dns recursive") fmt.Printf("codeberg.org/radhitya/linum\n\n") logger.Info("config loaded", "file", flags.Config) var ropts []resolver.Option if cfg.Resolver.Mode == "forward" && len(cfg.Resolver.Forwarders) > 0 { ropts = append(ropts, resolver.WithForwarders(cfg.Resolver.Forwarders)) logger.Info("resolver mode: forward", "upstreams", cfg.Resolver.Forwarders) } else { logger.Info("resolver mode: recursive (root hints)") } if dur, err := time.ParseDuration(cfg.Resolver.Timeout); err == nil { ropts = append(ropts, resolver.WithTimeout(dur)) } else { ropts = append(ropts, resolver.WithTimeout(2*time.Second)) } r := resolver.New(ropts...) c, err := cache.NewCache(cfg.Cache.MaxEntries, cfg.Cache.DBPath) if err != nil { logger.Error("create cache failed", "err", err) os.Exit(1) } 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 if cfg.Blocklist.Response == "nxdomain" { resp = blocklist.ResponseNXDOMAIN } bl = blocklist.New(resp) for _, pattern := range cfg.Blocklist.Files { matches, err := filepath.Glob(pattern) if err != nil { logger.Warn("invalid blocklist glob", "pattern", pattern, "err", err) continue } for _, f := range matches { if err := bl.LoadFile(f); err != nil { logger.Error("load blocklist failed", "file", f, "err", err) os.Exit(1) } logger.Info("blocklist loaded", "file", f, "rules", bl.TotalRules) } } for _, u := range cfg.Blocklist.URLs { if err := bl.LoadURL(u); err != nil { logger.Warn("load blocklist url failed", "url", u, "err", err) continue } logger.Info("blocklist url loaded", "url", u) } } else { logger.Info("no blocklist configured, ad-blocking disabled") } 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, cfg.Server.ListenDoT, tlsCfg, logger, r, c, bl, cfg) if err != nil { logger.Error("create server failed", "err", err) os.Exit(1) } defer srv.Close() if cfg.Admin.Listen != "" { logger.Info("admin server configured", "addr", cfg.Admin.Listen) } logger.Info("linum starting", "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 { logger.Error("server stopped with error", "err", err) os.Exit(1) } logger.Info("linum stopped cleanly") }