Files
routewatch/internal/server/server.go
T
sneak 64a83f9c57
check / check (push) Waiting to run
Report which public blocklists list an address in the IP lookup (closes #158)
A new package, internal/blocklist, downloads seven public lists on a
schedule: DShield, Feodo Tracker, FireHOL abusers and level 3, Spamhaus
DROP for IPv4 and IPv6, and the Tor exit list. It keeps the last good
copy of each in memory and on disk. A download sends back the
Last-Modified and ETag the publisher gave for the copy, also kept on
disk.

Every IP lookup result carries blocklists. An address with no live route
now gets a result too, without route fields, so the lookup no longer
answers 404. A successful answer carries Cache-Control, private for the
caller's own address. The address sets come from go4.org/netipx.

Model: opus-5-5
2026-10-27 05:21:21 +00:00

166 lines
4.4 KiB
Go

// Package server serves RouteWatch's web pages and JSON API:
// - the home page, the status figures and the health check
// - the AS, prefix and prefix length pages, and the JSON answers behind them
// - the IP lookups
// - the AS name search
package server
import (
"context"
"errors"
"fmt"
"net"
"net/http"
"net/netip"
"strconv"
"time"
"git.eeqj.de/sneak/routewatch/internal/blocklist"
"git.eeqj.de/sneak/routewatch/internal/config"
"git.eeqj.de/sneak/routewatch/internal/database"
"git.eeqj.de/sneak/routewatch/internal/logger"
"git.eeqj.de/sneak/routewatch/internal/streamer"
"github.com/go-chi/chi/v5"
)
// ASNFetcherStats contains WHOIS fetcher statistics.
type ASNFetcherStats struct {
SuccessesLastHour int
ErrorsLastHour int
CurrentInterval time.Duration
ConsecutiveFails int
}
// ASNFetcher is an interface for queuing ASN WHOIS lookups.
type ASNFetcher interface {
QueueImmediate(asn int)
GetStats() ASNFetcherStats
}
// Blocklists tells which public blocklists list an address, from copies it
// keeps in memory, as *blocklist.Blocklists does.
type Blocklists interface {
Lookup(addr netip.Addr) []blocklist.Listing
ShortestInterval() time.Duration
}
// dnsResolver looks up the addresses of host names and the names of addresses
// in DNS, as *net.Resolver does.
type dnsResolver interface {
LookupHost(ctx context.Context, host string) ([]string, error)
LookupAddr(ctx context.Context, addr string) ([]string, error)
}
// Server provides HTTP endpoints for status monitoring
type Server struct {
router *chi.Mux
db database.Store
streamer *streamer.Streamer
logger *logger.Logger
port int
srv *http.Server
asnFetcher ASNFetcher
blocklists Blocklists
// trustedProxies are the reverse proxies whose X-Forwarded-For header
// RealIPMiddleware believes
trustedProxies []netip.Prefix
// resolver makes the IP lookup's DNS lookups: net.DefaultResolver, or a
// fake in tests
resolver dnsResolver
}
// New creates a new HTTP server
func New(
db database.Store,
streamer *streamer.Streamer,
logger *logger.Logger,
cfg *config.Config,
) *Server {
s := &Server{
db: db,
streamer: streamer,
logger: logger,
port: cfg.Port,
trustedProxies: cfg.TrustedProxies,
resolver: net.DefaultResolver,
}
s.setupRoutes()
return s
}
// Start listens on the configured port and serves HTTP in the background.
// It returns an error if it cannot listen, for example because the port is
// already in use. ctx bounds only opening the port, not serving.
func (s *Server) Start(ctx context.Context) error {
const (
network = "tcp"
readHeaderTimeout = 40 * time.Second
readTimeout = 60 * time.Second
writeTimeout = 60 * time.Second
idleTimeout = 120 * time.Second
)
s.srv = &http.Server{
Addr: ":" + strconv.Itoa(s.port),
Handler: s.router,
ReadHeaderTimeout: readHeaderTimeout,
ReadTimeout: readTimeout,
WriteTimeout: writeTimeout,
IdleTimeout: idleTimeout,
}
s.logger.Info("Starting HTTP server", "port", s.port, "addr", s.srv.Addr)
var listenConfig net.ListenConfig
listener, err := listenConfig.Listen(ctx, network, s.srv.Addr)
if err != nil {
return fmt.Errorf("starting HTTP server: %w", err)
}
s.logger.Info("HTTP server listening", "addr", listener.Addr().String())
go func() {
err := s.srv.Serve(listener)
if err != nil && !errors.Is(err, http.ErrServerClosed) {
s.logger.Error("HTTP server error", "error", err)
}
}()
return nil
}
// Stop stops the HTTP server. It waits, until ctx ends, for the requests in
// progress to finish, and then closes the connections of those still running
// and returns ctx's error.
func (s *Server) Stop(ctx context.Context) error {
if s.srv == nil {
return nil
}
s.logger.Info("Stopping HTTP server")
err := s.srv.Shutdown(ctx)
if err != nil {
return errors.Join(err, s.srv.Close())
}
return nil
}
// SetASNFetcher sets the ASN WHOIS fetcher for on-demand lookups. Call it
// before Start: the handlers read the fetcher without a lock.
func (s *Server) SetASNFetcher(fetcher ASNFetcher) {
s.asnFetcher = fetcher
}
// SetBlocklists sets the blocklists the IP lookups report. Call it before
// Start: the handlers read the blocklists without a lock.
func (s *Server) SetBlocklists(blocklists Blocklists) {
s.blocklists = blocklists
}