Fixed bug on rqlite array overflow buffer

This commit is contained in:
anonpenguin23 2026-02-25 08:37:55 +02:00
parent 45a8285ae8
commit ed4e490463
6 changed files with 72 additions and 22 deletions

View File

@ -63,7 +63,7 @@ test-e2e-quick:
.PHONY: build clean test deps tidy fmt vet lint install-hooks upload-devnet upload-testnet redeploy-devnet redeploy-testnet release health .PHONY: build clean test deps tidy fmt vet lint install-hooks upload-devnet upload-testnet redeploy-devnet redeploy-testnet release health
VERSION := 0.112.7 VERSION := 0.112.8
COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown) COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
DATE ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ) DATE ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
LDFLAGS := -X 'main.version=$(VERSION)' -X 'main.commit=$(COMMIT)' -X 'main.date=$(DATE)' LDFLAGS := -X 'main.version=$(VERSION)' -X 'main.commit=$(COMMIT)' -X 'main.date=$(DATE)'

View File

@ -9,6 +9,28 @@ import (
"github.com/rqlite/gorqlite" "github.com/rqlite/gorqlite"
) )
// safeWriteOneParameterized wraps conn.WriteOneParameterized with panic recovery.
// gorqlite panics with "index out of range" when RQLite returns empty results
// during temporary unavailability. This converts the panic to a normal error.
func safeWriteOneParameterized(conn *gorqlite.Connection, stmt gorqlite.ParameterizedStatement) (result gorqlite.WriteResult, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("gorqlite panic (WriteOneParameterized): %v", r)
}
}()
return conn.WriteOneParameterized(stmt)
}
// safeWriteOne wraps conn.WriteOne with panic recovery.
func safeWriteOne(conn *gorqlite.Connection, query string) (result gorqlite.WriteResult, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("gorqlite panic (WriteOne): %v", r)
}
}()
return conn.WriteOne(query)
}
// DatabaseClientImpl implements DatabaseClient // DatabaseClientImpl implements DatabaseClient
type DatabaseClientImpl struct { type DatabaseClientImpl struct {
client *Client client *Client
@ -79,7 +101,7 @@ func (d *DatabaseClientImpl) Query(ctx context.Context, sql string, args ...inte
if isWriteOperation { if isWriteOperation {
// Execute write operation with parameters // Execute write operation with parameters
_, err := conn.WriteOneParameterized(gorqlite.ParameterizedStatement{ _, err := safeWriteOneParameterized(conn, gorqlite.ParameterizedStatement{
Query: sql, Query: sql,
Arguments: args, Arguments: args,
}) })
@ -293,7 +315,7 @@ func (d *DatabaseClientImpl) Transaction(ctx context.Context, queries []string)
// Execute all queries in the transaction // Execute all queries in the transaction
success := true success := true
for _, query := range queries { for _, query := range queries {
_, err := conn.WriteOne(query) _, err := safeWriteOne(conn, query)
if err != nil { if err != nil {
lastErr = err lastErr = err
success = false success = false
@ -321,7 +343,7 @@ func (d *DatabaseClientImpl) CreateTable(ctx context.Context, schema string) err
} }
return d.withRetry(func(conn *gorqlite.Connection) error { return d.withRetry(func(conn *gorqlite.Connection) error {
_, err := conn.WriteOne(schema) _, err := safeWriteOne(conn, schema)
return err return err
}) })
} }
@ -334,7 +356,7 @@ func (d *DatabaseClientImpl) DropTable(ctx context.Context, tableName string) er
return d.withRetry(func(conn *gorqlite.Connection) error { return d.withRetry(func(conn *gorqlite.Connection) error {
dropSQL := fmt.Sprintf("DROP TABLE IF EXISTS %s", tableName) dropSQL := fmt.Sprintf("DROP TABLE IF EXISTS %s", tableName)
_, err := conn.WriteOne(dropSQL) _, err := safeWriteOne(conn, dropSQL)
return err return err
}) })
} }

View File

@ -11,6 +11,7 @@ import (
"time" "time"
"github.com/DeBrosOfficial/network/pkg/logging" "github.com/DeBrosOfficial/network/pkg/logging"
"github.com/DeBrosOfficial/network/pkg/rqlite"
"github.com/DeBrosOfficial/network/pkg/wireguard" "github.com/DeBrosOfficial/network/pkg/wireguard"
"go.uber.org/zap" "go.uber.org/zap"
) )
@ -57,7 +58,7 @@ func (n *Node) registerDNSNode(ctx context.Context) error {
` `
db := n.rqliteAdapter.GetSQLDB() db := n.rqliteAdapter.GetSQLDB()
_, err = db.ExecContext(ctx, query, nodeID, ipAddress, internalIP, region) _, err = rqlite.SafeExecContext(db, ctx, query, nodeID, ipAddress, internalIP, region)
if err != nil { if err != nil {
return fmt.Errorf("failed to register DNS node: %w", err) return fmt.Errorf("failed to register DNS node: %w", err)
} }
@ -112,7 +113,7 @@ func (n *Node) updateDNSHeartbeat(ctx context.Context) error {
query := `UPDATE dns_nodes SET last_seen = datetime('now'), updated_at = datetime('now') WHERE id = ?` query := `UPDATE dns_nodes SET last_seen = datetime('now'), updated_at = datetime('now') WHERE id = ?`
db := n.rqliteAdapter.GetSQLDB() db := n.rqliteAdapter.GetSQLDB()
_, err := db.ExecContext(ctx, query, nodeID) _, err := rqlite.SafeExecContext(db, ctx, query, nodeID)
if err != nil { if err != nil {
return fmt.Errorf("failed to update DNS heartbeat: %w", err) return fmt.Errorf("failed to update DNS heartbeat: %w", err)
} }
@ -177,7 +178,7 @@ func (n *Node) ensureBaseDNSRecords(ctx context.Context) error {
query := `INSERT INTO dns_records (fqdn, record_type, value, ttl, namespace, created_by, is_active, created_at, updated_at) query := `INSERT INTO dns_records (fqdn, record_type, value, ttl, namespace, created_by, is_active, created_at, updated_at)
VALUES (?, 'A', ?, 300, 'system', 'system', TRUE, datetime('now'), datetime('now')) VALUES (?, 'A', ?, 300, 'system', 'system', TRUE, datetime('now'), datetime('now'))
ON CONFLICT(fqdn, record_type, value) DO NOTHING` ON CONFLICT(fqdn, record_type, value) DO NOTHING`
if _, err := db.ExecContext(ctx, query, r.fqdn, r.value); err != nil { if _, err := rqlite.SafeExecContext(db, ctx, query, r.fqdn, r.value); err != nil {
n.logger.ComponentWarn(logging.ComponentNode, "Failed to ensure DNS record", n.logger.ComponentWarn(logging.ComponentNode, "Failed to ensure DNS record",
zap.String("fqdn", r.fqdn), zap.Error(err)) zap.String("fqdn", r.fqdn), zap.Error(err))
} }
@ -219,7 +220,7 @@ func (n *Node) ensureSOAAndNSRecords(ctx context.Context, baseDomain string) {
// Create SOA record // Create SOA record
soaValue := fmt.Sprintf("ns1.%s. admin.%s. %d 3600 1800 604800 300", soaValue := fmt.Sprintf("ns1.%s. admin.%s. %d 3600 1800 604800 300",
baseDomain, baseDomain, time.Now().Unix()) baseDomain, baseDomain, time.Now().Unix())
if _, err := db.ExecContext(ctx, if _, err := rqlite.SafeExecContext(db, ctx,
`INSERT INTO dns_records (fqdn, record_type, value, ttl, namespace, created_by, is_active, created_at, updated_at) `INSERT INTO dns_records (fqdn, record_type, value, ttl, namespace, created_by, is_active, created_at, updated_at)
VALUES (?, 'SOA', ?, 300, 'system', 'system', TRUE, datetime('now'), datetime('now')) VALUES (?, 'SOA', ?, 300, 'system', 'system', TRUE, datetime('now'), datetime('now'))
ON CONFLICT(fqdn, record_type, value) DO NOTHING`, ON CONFLICT(fqdn, record_type, value) DO NOTHING`,
@ -231,7 +232,7 @@ func (n *Node) ensureSOAAndNSRecords(ctx context.Context, baseDomain string) {
// Create NS records (ns1, ns2, ns3) // Create NS records (ns1, ns2, ns3)
for i := 1; i <= 3; i++ { for i := 1; i <= 3; i++ {
nsValue := fmt.Sprintf("ns%d.%s.", i, baseDomain) nsValue := fmt.Sprintf("ns%d.%s.", i, baseDomain)
if _, err := db.ExecContext(ctx, if _, err := rqlite.SafeExecContext(db, ctx,
`INSERT INTO dns_records (fqdn, record_type, value, ttl, namespace, created_by, is_active, created_at, updated_at) `INSERT INTO dns_records (fqdn, record_type, value, ttl, namespace, created_by, is_active, created_at, updated_at)
VALUES (?, 'NS', ?, 300, 'system', 'system', TRUE, datetime('now'), datetime('now')) VALUES (?, 'NS', ?, 300, 'system', 'system', TRUE, datetime('now'), datetime('now'))
ON CONFLICT(fqdn, record_type, value) DO NOTHING`, ON CONFLICT(fqdn, record_type, value) DO NOTHING`,
@ -257,7 +258,7 @@ func (n *Node) claimNameserverSlot(ctx context.Context, domain, ipAddress string
if err == nil { if err == nil {
// Already claimed — update IP if changed // Already claimed — update IP if changed
if _, err := db.ExecContext(ctx, if _, err := rqlite.SafeExecContext(db, ctx,
`UPDATE dns_nameservers SET ip_address = ?, updated_at = datetime('now') WHERE hostname = ? AND domain = ?`, `UPDATE dns_nameservers SET ip_address = ?, updated_at = datetime('now') WHERE hostname = ? AND domain = ?`,
ipAddress, existingHostname, domain, ipAddress, existingHostname, domain,
); err != nil { ); err != nil {
@ -265,7 +266,7 @@ func (n *Node) claimNameserverSlot(ctx context.Context, domain, ipAddress string
} }
// Ensure the glue A record matches // Ensure the glue A record matches
nsFQDN := existingHostname + "." + domain + "." nsFQDN := existingHostname + "." + domain + "."
if _, err := db.ExecContext(ctx, if _, err := rqlite.SafeExecContext(db, ctx,
`INSERT INTO dns_records (fqdn, record_type, value, ttl, namespace, created_by, is_active, created_at, updated_at) `INSERT INTO dns_records (fqdn, record_type, value, ttl, namespace, created_by, is_active, created_at, updated_at)
VALUES (?, 'A', ?, 300, 'system', 'system', TRUE, datetime('now'), datetime('now')) VALUES (?, 'A', ?, 300, 'system', 'system', TRUE, datetime('now'), datetime('now'))
ON CONFLICT(fqdn, record_type, value) DO NOTHING`, ON CONFLICT(fqdn, record_type, value) DO NOTHING`,
@ -278,7 +279,7 @@ func (n *Node) claimNameserverSlot(ctx context.Context, domain, ipAddress string
// Try to claim an available slot // Try to claim an available slot
for _, hostname := range []string{"ns1", "ns2", "ns3"} { for _, hostname := range []string{"ns1", "ns2", "ns3"} {
result, err := db.ExecContext(ctx, result, err := rqlite.SafeExecContext(db, ctx,
`INSERT INTO dns_nameservers (hostname, node_id, ip_address, domain) VALUES (?, ?, ?, ?) `INSERT INTO dns_nameservers (hostname, node_id, ip_address, domain) VALUES (?, ?, ?, ?)
ON CONFLICT(hostname) DO NOTHING`, ON CONFLICT(hostname) DO NOTHING`,
hostname, nodeID, ipAddress, domain, hostname, nodeID, ipAddress, domain,
@ -290,7 +291,7 @@ func (n *Node) claimNameserverSlot(ctx context.Context, domain, ipAddress string
if rows > 0 { if rows > 0 {
// Successfully claimed this slot — create glue record // Successfully claimed this slot — create glue record
nsFQDN := hostname + "." + domain + "." nsFQDN := hostname + "." + domain + "."
if _, err := db.ExecContext(ctx, if _, err := rqlite.SafeExecContext(db, ctx,
`INSERT INTO dns_records (fqdn, record_type, value, ttl, namespace, created_by, is_active, created_at, updated_at) `INSERT INTO dns_records (fqdn, record_type, value, ttl, namespace, created_by, is_active, created_at, updated_at)
VALUES (?, 'A', ?, 300, 'system', 'system', TRUE, datetime('now'), datetime('now')) VALUES (?, 'A', ?, 300, 'system', 'system', TRUE, datetime('now'), datetime('now'))
ON CONFLICT(fqdn, record_type, value) DO NOTHING`, ON CONFLICT(fqdn, record_type, value) DO NOTHING`,
@ -347,27 +348,27 @@ func (n *Node) cleanupStaleNodeRecords(ctx context.Context) {
} }
// Mark node as inactive // Mark node as inactive
if _, err := db.ExecContext(ctx, `UPDATE dns_nodes SET status = 'inactive', updated_at = datetime('now') WHERE id = ?`, nodeID); err != nil { if _, err := rqlite.SafeExecContext(db, ctx, `UPDATE dns_nodes SET status = 'inactive', updated_at = datetime('now') WHERE id = ?`, nodeID); err != nil {
n.logger.ComponentWarn(logging.ComponentNode, "Failed to mark node inactive", zap.String("node_id", nodeID), zap.Error(err)) n.logger.ComponentWarn(logging.ComponentNode, "Failed to mark node inactive", zap.String("node_id", nodeID), zap.Error(err))
} }
// Remove the dead node's A records from round-robin // Remove the dead node's A records from round-robin
for _, f := range fqdnsToClean { for _, f := range fqdnsToClean {
if _, err := db.ExecContext(ctx, `DELETE FROM dns_records WHERE fqdn = ? AND record_type = 'A' AND value = ? AND namespace = 'system'`, f, ip); err != nil { if _, err := rqlite.SafeExecContext(db, ctx, `DELETE FROM dns_records WHERE fqdn = ? AND record_type = 'A' AND value = ? AND namespace = 'system'`, f, ip); err != nil {
n.logger.ComponentWarn(logging.ComponentNode, "Failed to remove stale DNS record", n.logger.ComponentWarn(logging.ComponentNode, "Failed to remove stale DNS record",
zap.String("fqdn", f), zap.String("ip", ip), zap.Error(err)) zap.String("fqdn", f), zap.String("ip", ip), zap.Error(err))
} }
} }
// Release any NS slot held by this dead node // Release any NS slot held by this dead node
if _, err := db.ExecContext(ctx, `DELETE FROM dns_nameservers WHERE node_id = ?`, nodeID); err != nil { if _, err := rqlite.SafeExecContext(db, ctx, `DELETE FROM dns_nameservers WHERE node_id = ?`, nodeID); err != nil {
n.logger.ComponentWarn(logging.ComponentNode, "Failed to release NS slot", zap.String("node_id", nodeID), zap.Error(err)) n.logger.ComponentWarn(logging.ComponentNode, "Failed to release NS slot", zap.String("node_id", nodeID), zap.Error(err))
} }
// Remove glue records for this node's IP (ns1.domain., ns2.domain., ns3.domain.) // Remove glue records for this node's IP (ns1.domain., ns2.domain., ns3.domain.)
for _, ns := range []string{"ns1", "ns2", "ns3"} { for _, ns := range []string{"ns1", "ns2", "ns3"} {
nsFQDN := ns + "." + baseDomain + "." nsFQDN := ns + "." + baseDomain + "."
if _, err := db.ExecContext(ctx, if _, err := rqlite.SafeExecContext(db, ctx,
`DELETE FROM dns_records WHERE fqdn = ? AND record_type = 'A' AND value = ? AND namespace = 'system'`, `DELETE FROM dns_records WHERE fqdn = ? AND record_type = 'A' AND value = ? AND namespace = 'system'`,
nsFQDN, ip, nsFQDN, ip,
); err != nil { ); err != nil {
@ -484,7 +485,7 @@ func cleanupPrivateIPRecords(ctx context.Context, db *sql.DB, logger *logging.Co
AND (value LIKE '10.%' OR value LIKE '172.16.%' OR value LIKE '172.17.%' OR value LIKE '172.18.%' AND (value LIKE '10.%' OR value LIKE '172.16.%' OR value LIKE '172.17.%' OR value LIKE '172.18.%'
OR value LIKE '172.19.%' OR value LIKE '172.2_.%' OR value LIKE '172.30.%' OR value LIKE '172.31.%' OR value LIKE '172.19.%' OR value LIKE '172.2_.%' OR value LIKE '172.30.%' OR value LIKE '172.31.%'
OR value LIKE '192.168.%' OR value = '127.0.0.1')` OR value LIKE '192.168.%' OR value = '127.0.0.1')`
result, err := db.ExecContext(ctx, query) result, err := rqlite.SafeExecContext(db, ctx, query)
if err != nil { if err != nil {
logger.ComponentWarn(logging.ComponentNode, "Failed to clean up private IP DNS records", zap.Error(err)) logger.ComponentWarn(logging.ComponentNode, "Failed to clean up private IP DNS records", zap.Error(err))
return return

View File

@ -12,6 +12,7 @@ import (
"github.com/DeBrosOfficial/network/pkg/environments/production" "github.com/DeBrosOfficial/network/pkg/environments/production"
"github.com/DeBrosOfficial/network/pkg/logging" "github.com/DeBrosOfficial/network/pkg/logging"
"github.com/DeBrosOfficial/network/pkg/rqlite"
"go.uber.org/zap" "go.uber.org/zap"
) )
@ -166,13 +167,13 @@ func (n *Node) ensureWireGuardSelfRegistered(ctx context.Context) {
// Clean up stale entries for this public IP with a different node_id. // Clean up stale entries for this public IP with a different node_id.
// This prevents ghost peers from previous installs or from the temporary // This prevents ghost peers from previous installs or from the temporary
// "node-10.0.0.X" ID that the join handler creates. // "node-10.0.0.X" ID that the join handler creates.
if _, err := db.ExecContext(ctx, if _, err := rqlite.SafeExecContext(db, ctx,
"DELETE FROM wireguard_peers WHERE public_ip = ? AND node_id != ?", "DELETE FROM wireguard_peers WHERE public_ip = ? AND node_id != ?",
publicIP, nodeID); err != nil { publicIP, nodeID); err != nil {
n.logger.ComponentWarn(logging.ComponentNode, "Failed to clean stale WG entries", zap.Error(err)) n.logger.ComponentWarn(logging.ComponentNode, "Failed to clean stale WG entries", zap.Error(err))
} }
_, err = db.ExecContext(ctx, _, err = rqlite.SafeExecContext(db, ctx,
"INSERT OR REPLACE INTO wireguard_peers (node_id, wg_ip, public_key, public_ip, wg_port, ipfs_peer_id) VALUES (?, ?, ?, ?, ?, ?)", "INSERT OR REPLACE INTO wireguard_peers (node_id, wg_ip, public_key, public_ip, wg_port, ipfs_peer_id) VALUES (?, ?, ?, ?, ?, ?)",
nodeID, wgIP, localPubKey, publicIP, 51820, ipfsPeerID) nodeID, wgIP, localPubKey, publicIP, 51820, ipfsPeerID)
if err != nil { if err != nil {

View File

@ -35,7 +35,14 @@ func (c *client) Query(ctx context.Context, dest any, query string, args ...any)
} }
// Exec runs a write statement (INSERT/UPDATE/DELETE). // Exec runs a write statement (INSERT/UPDATE/DELETE).
func (c *client) Exec(ctx context.Context, query string, args ...any) (sql.Result, error) { // Includes panic recovery because the gorqlite stdlib driver can panic
// with "index out of range" when RQLite is temporarily unavailable.
func (c *client) Exec(ctx context.Context, query string, args ...any) (result sql.Result, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("gorqlite panic (ExecContext): %v", r)
}
}()
return c.db.ExecContext(ctx, query, args...) return c.db.ExecContext(ctx, query, args...)
} }

19
pkg/rqlite/safe_exec.go Normal file
View File

@ -0,0 +1,19 @@
package rqlite
import (
"context"
"database/sql"
"fmt"
)
// SafeExecContext wraps db.ExecContext with panic recovery.
// The gorqlite stdlib driver can panic with "index out of range" when
// RQLite is temporarily unavailable. This converts the panic to an error.
func SafeExecContext(db *sql.DB, ctx context.Context, query string, args ...interface{}) (result sql.Result, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("gorqlite panic (ExecContext): %v", r)
}
}()
return db.ExecContext(ctx, query, args...)
}