decoygrid dev log #002 — SQLite state and the polling loop
Following on from dev log #001 — persistent state is in. This is the first piece of the rebuild that makes decoygrid actually useful instead of just a polling demo.
The schema
Kept it deliberately small. Two tables:
CREATE TABLE honeypots (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
address TEXT NOT NULL,
port INTEGER NOT NULL,
last_up DATETIME,
last_down DATETIME,
status TEXT NOT NULL DEFAULT 'unknown'
);
CREATE TABLE connection_attempts (
id INTEGER PRIMARY KEY,
honeypot_id INTEGER NOT NULL REFERENCES honeypots(id),
source_ip TEXT NOT NULL,
occurred_at DATETIME NOT NULL,
raw_payload BLOB
);
raw_payload stays as a blob rather than a parsed structure — different honeypot types capture wildly
different data (a fake SSH banner exchange looks nothing like an HTTP request to a decoy admin panel), and
trying to normalise that into columns now would mean redesigning the schema every time a new decoy type gets
added. Parsing happens at read time, per protocol, not at write time.
The polling loop
func (g *Grid) pollLoop(ctx context.Context) {
ticker := time.NewTicker(g.interval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
var wg sync.WaitGroup
for _, hp := range g.honeypots {
wg.Add(1)
go func(h *Honeypot) {
defer wg.Done()
g.checkAndRecord(h)
}(hp)
}
wg.Wait()
}
}
}
Each honeypot gets checked concurrently within a tick rather than sequentially — with twenty honeypots and a one-second dial timeout each, sequential checking would mean a single tick could take up to twenty seconds in the worst case. Concurrent checking bounds it to roughly the slowest single check.
checkAndRecord does the actual dial attempt, updates last_up/last_down/status on success or failure,
and that’s the entire write path for now. No batching, no write queue — SQLite handles this volume of writes
without issue at the scale this runs at.
What’s next
The HTTP status endpoint is the next piece — a single /api/status route that reads the honeypots table
and returns JSON. Once that exists, the web interface is just a template rendering that same data, and dev
log #003 will cover it.