diff --git a/src/routing_layer/topo_node_sqlite.c b/src/routing_layer/topo_node_sqlite.c index 256851a6..a6328810 100644 --- a/src/routing_layer/topo_node_sqlite.c +++ b/src/routing_layer/topo_node_sqlite.c @@ -250,12 +250,38 @@ int topo_node_sqlite_init(sqlite3* db) { /* Проекция адресов производная. Старый формат пересоздаём, не интерпретируя NAT_TYPE_* как новый профиль. */ sqlite3_stmt *schema = NULL; int legacy_profile = 0; - if (sqlite3_prepare_v2(db, "SELECT 1 FROM sqlite_master WHERE type='table' AND name='node_addresses'", + if (sqlite3_prepare_v2(db, "SELECT sql FROM sqlite_master WHERE type='table' AND name='node_addresses'", -1, &schema, NULL) != SQLITE_OK) { DEBUG_ERROR(DEBUG_CATEGORY_BGP, "address schema inspection failed: %s", sqlite3_errmsg(db)); return -1; } int exists = sqlite3_step(schema) == SQLITE_ROW; + char *zero_port_schema = NULL; + if (exists) { + const char *ddl = (const char *)sqlite3_column_text(schema, 0); + const char *strict_port = ddl ? strstr(ddl, "port > 0") : NULL; + if (strict_port) { + zero_port_schema = sqlite3_mprintf("%.*sport >= 0%s", (int)(strict_port - ddl), ddl, strict_port + strlen("port > 0")); + if (!zero_port_schema) { + DEBUG_ERROR(DEBUG_CATEGORY_BGP, "address zero-port schema allocation failed"); sqlite3_finalize(schema); return -1; + } + } + } sqlite3_finalize(schema); schema = NULL; + if (zero_port_schema) { + int failed = sqlite3_exec(db, "SAVEPOINT address_zero_port; ALTER TABLE node_addresses RENAME TO address_previous_ports", + NULL, NULL, NULL) != SQLITE_OK || + sqlite3_exec(db, zero_port_schema, NULL, NULL, NULL) != SQLITE_OK || + sqlite3_exec(db, "INSERT INTO node_addresses SELECT * FROM address_previous_ports;" + "DROP TABLE address_previous_ports; RELEASE address_zero_port", NULL, NULL, NULL) != SQLITE_OK; + sqlite3_free(zero_port_schema); + if (failed) { + DEBUG_ERROR(DEBUG_CATEGORY_BGP, "address zero-port schema rebuild failed: %s", sqlite3_errmsg(db)); + if (sqlite3_exec(db, "ROLLBACK TO address_zero_port; RELEASE address_zero_port", NULL, NULL, NULL) != SQLITE_OK) + DEBUG_ERROR(DEBUG_CATEGORY_BGP, "address zero-port schema rollback failed: %s", sqlite3_errmsg(db)); + return -1; + } + DEBUG_INFO(DEBUG_CATEGORY_BGP, "address schema now accepts TCP IP-only port=0; stored addresses preserved"); + } if (exists) { int compatible = sqlite3_prepare_v2(db, "SELECT nat_mapping,nat_inbound,nat_source,address_kind FROM node_addresses LIMIT 0", @@ -303,7 +329,7 @@ int topo_node_sqlite_init(sqlite3* db) { " family INTEGER NOT NULL CHECK(family IN (4, 6))," " protocol INTEGER NOT NULL DEFAULT 1," " address BLOB NOT NULL," - " port INTEGER NOT NULL CHECK(port > 0 AND port <= 65535)," + " port INTEGER NOT NULL CHECK(port >= 0 AND port <= 65535)," " rtt INTEGER," " addr_type INTEGER DEFAULT 0," " config_type INTEGER DEFAULT 0," diff --git a/tests/test_node_snapshot.c b/tests/test_node_snapshot.c index 4d865952..54af4728 100644 --- a/tests/test_node_snapshot.c +++ b/tests/test_node_snapshot.c @@ -36,8 +36,36 @@ static struct TOPO_NODE* record(struct UTUN_INSTANCE* inst, uint64_t timestamp, return ni; } +/* Старая БД запрещала port=0; перестройка CHECK сохраняет адреса и восстанавливает индексы. */ +static void test_zero_port_schema(void) { + sqlite3 *db = NULL; assert(sqlite3_open(":memory:", &db) == SQLITE_OK); + assert(sqlite3_exec(db, "CREATE TABLE node_addresses(" + "id INTEGER PRIMARY KEY,node_id INTEGER,family INTEGER,protocol INTEGER,address BLOB," + "port INTEGER CHECK(port > 0 AND port <= 65535),rtt INTEGER,addr_type INTEGER,config_type INTEGER," + "nat_mapping INTEGER,nat_inbound INTEGER,nat_source INTEGER,address_kind INTEGER,socket_id INTEGER," + "options BLOB,created_at INTEGER);" + "CREATE UNIQUE INDEX idx_na_unique ON node_addresses(node_id,family,socket_id,protocol,address,port,address_kind);" + "INSERT INTO node_addresses VALUES(7,1,4,2,X'CB00712C',443,15,4,2,0,0,1,1,3,X'0102',123)", + NULL, NULL, NULL) == SQLITE_OK); + assert(!topo_node_sqlite_init(db)); + sqlite3_stmt *statement = NULL; + assert(sqlite3_prepare_v2(db, "SELECT port,rtt,nat_source,length(options),created_at FROM node_addresses WHERE id=7", + -1, &statement, NULL) == SQLITE_OK); + assert(sqlite3_step(statement) == SQLITE_ROW && sqlite3_column_int(statement, 0) == 443); + assert(sqlite3_column_int(statement, 1) == 15 && sqlite3_column_int(statement, 2) == ADDRESS_SOURCE_ETCP); + assert(sqlite3_column_int(statement, 3) == 2 && sqlite3_column_int(statement, 4) == 123); + sqlite3_finalize(statement); + assert(sqlite3_exec(db, "UPDATE node_addresses SET port=0 WHERE id=7", NULL, NULL, NULL) == SQLITE_OK); + assert(sqlite3_exec(db, "INSERT INTO node_addresses SELECT 8,node_id,family,protocol,address,port,rtt," + "addr_type,config_type,nat_mapping,nat_inbound,nat_source,address_kind,socket_id,options,created_at" + " FROM node_addresses WHERE id=7", NULL, NULL, NULL) == SQLITE_CONSTRAINT); + assert(!topo_node_sqlite_init(db)); + sqlite3_close(db); +} + /* Новый wire и SQL сохраняют адреса, протоколы, классификацию и REALITY; смена формата оставляет bootstrap. */ static void test_compact_addresses(struct UTUN_INSTANCE *inst) { + test_zero_port_schema(); sqlite3 *saved_db = inst->topo_sqlite_db; assert(sqlite3_open(":memory:", &inst->topo_sqlite_db) == SQLITE_OK); sqlite3 *db = inst->topo_sqlite_db;