package main import ( "context" "log/slog" "time" "os" "os/signal" "path/filepath" "syscall" "fmt" "linum/internal/blocklist" "linum/internal/cache" "linum/internal/config" "linum/internal/resolver" "linum/internal/server" ) 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("github.com/radhityax/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 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, logger, r, c, bl) if err != nil { logger.Error("create server failed", "err", err) os.Exit(1) } defer srv.Close() logger.Info("linum starting", "udp", cfg.Server.ListenUDP, "tcp", cfg.Server.ListenTCP, "doh", cfg.Server.ListenDOH, ) 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") }