feat(traffic): добавить приём Traffic Flow с jump-host
Docker images / prepare-release (push) Successful in 8s
Docker images / backend-image (push) Successful in 2m1s
Docker images / frontend-image (push) Successful in 3m56s
Docker images / notify-webhook (push) Skipped
Docker images / updater-image (push) Successful in 46s
Docker images / publish-release (push) Successful in 12s

Чтобы видеть «кто с кем», а не только объём порта: IPFIX внутри WG на хосте Docker MM, REST-счётчики не трогаем.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-09-06 21:54:30 +07:00
co-authored by Cursor
parent 5884bd8873
commit 1e9312acbd
24 changed files with 2007 additions and 71 deletions
+50
View File
@@ -121,6 +121,47 @@ CREATE INDEX IF NOT EXISTS idx_traffic_samples_server_time
CREATE INDEX IF NOT EXISTS idx_traffic_samples_server_iface_time
ON traffic_samples(server_id, interface_name, sampled_at);
CREATE TABLE IF NOT EXISTS traffic_flow_settings (
id INTEGER PRIMARY KEY,
enabled INTEGER NOT NULL DEFAULT 0,
collector_ip TEXT NOT NULL DEFAULT '10.255.254.1',
flow_listen_port INTEGER NOT NULL DEFAULT 4739,
wg_listen_port INTEGER NOT NULL DEFAULT 51821,
prefix TEXT NOT NULL DEFAULT '10.255.254.0/24',
public_endpoint TEXT NOT NULL DEFAULT '',
host_public_key TEXT NOT NULL DEFAULT '',
host_private_key TEXT NOT NULL DEFAULT '',
hub_server_id INTEGER,
retention_hours INTEGER NOT NULL DEFAULT 24,
top_n INTEGER NOT NULL DEFAULT 200,
last_datagram_at TEXT,
last_exporter_ip TEXT,
last_error TEXT,
packets_received INTEGER NOT NULL DEFAULT 0,
peers_json TEXT NOT NULL DEFAULT '[]',
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS flow_buckets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
server_id INTEGER NOT NULL,
bucket_at TEXT NOT NULL,
src TEXT NOT NULL,
dst TEXT NOT NULL,
proto INTEGER NOT NULL DEFAULT 0,
src_port INTEGER NOT NULL DEFAULT 0,
dst_port INTEGER NOT NULL DEFAULT 0,
bytes INTEGER NOT NULL DEFAULT 0,
packets INTEGER NOT NULL DEFAULT 0,
in_iface TEXT NOT NULL DEFAULT '',
FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_flow_buckets_unique
ON flow_buckets(server_id, bucket_at, src, dst, proto, src_port, dst_port);
CREATE INDEX IF NOT EXISTS idx_flow_buckets_server_time
ON flow_buckets(server_id, bucket_at);
CREATE TABLE IF NOT EXISTS uptime_settings (
id INTEGER PRIMARY KEY,
enabled INTEGER NOT NULL DEFAULT 1,
@@ -674,6 +715,9 @@ if (!serverCols.some((c) => c.name === "lan_subnet")) {
if (!serverCols.some((c) => c.name === "wan_uplinks")) {
sqlite.exec(`ALTER TABLE servers ADD COLUMN wan_uplinks TEXT NOT NULL DEFAULT '[]'`)
}
if (!serverCols.some((c) => c.name === "mgmt_tunnel_ip")) {
sqlite.exec(`ALTER TABLE servers ADD COLUMN mgmt_tunnel_ip TEXT NOT NULL DEFAULT ''`)
}
const alertTgCols = sqlite.prepare(`PRAGMA table_info('alert_telegram_settings')`).all() as Array<{ name?: string }>
if (!alertTgCols.some((c) => c.name === "message_thread_id")) {
@@ -719,6 +763,12 @@ SELECT 1, 1, 30, 14
WHERE NOT EXISTS (SELECT 1 FROM traffic_settings WHERE id = 1);
`)
sqlite.exec(`
INSERT INTO traffic_flow_settings (id, enabled, collector_ip, flow_listen_port, wg_listen_port, prefix)
SELECT 1, 0, '10.255.254.1', 4739, 51821, '10.255.254.0/24'
WHERE NOT EXISTS (SELECT 1 FROM traffic_flow_settings WHERE id = 1);
`)
sqlite.exec(`
INSERT INTO uptime_settings (id, enabled, interval_sec, retention_days)
SELECT 1, 1, 15, 14
+46
View File
@@ -32,6 +32,8 @@ export const servers = sqliteTable("servers", {
lanSubnet: text("lan_subnet").notNull().default(""),
/** JSON-массив WAN-аплинков [{ id, name, isp, iface, ip, maxDl, maxUl }, …] */
wanUplinks: text("wan_uplinks").notNull().default("[]"),
/** Адрес в оверлее wg-flow (экспортёр IPFIX), например 10.255.254.5 */
mgmtTunnelIp: text("mgmt_tunnel_ip").notNull().default(""),
createdAt: text("created_at").notNull().default(sql`(datetime('now'))`),
updatedAt: text("updated_at").notNull().default(sql`(datetime('now'))`),
@@ -158,6 +160,48 @@ export const alertBgpPeerSamples = sqliteTable("alert_bgp_peer_samples", {
// ── raw traffic samples (per server/interface/timepoint) ──────────────────────
export const trafficFlowSettings = sqliteTable("traffic_flow_settings", {
id: integer("id").primaryKey(),
enabled: integer("enabled", { mode: "boolean" }).notNull().default(false),
collectorIp: text("collector_ip").notNull().default("10.255.254.1"),
flowListenPort: integer("flow_listen_port").notNull().default(4739),
wgListenPort: integer("wg_listen_port").notNull().default(51821),
prefix: text("prefix").notNull().default("10.255.254.0/24"),
publicEndpoint: text("public_endpoint").notNull().default(""),
hostPublicKey: text("host_public_key").notNull().default(""),
hostPrivateKey: text("host_private_key").notNull().default(""),
hubServerId: integer("hub_server_id"),
retentionHours: integer("retention_hours").notNull().default(24),
topN: integer("top_n").notNull().default(200),
lastDatagramAt: text("last_datagram_at"),
lastExporterIp: text("last_exporter_ip"),
lastError: text("last_error"),
packetsReceived: integer("packets_received").notNull().default(0),
peersJson: text("peers_json").notNull().default("[]"),
createdAt: text("created_at").notNull().default(sql`(datetime('now'))`),
updatedAt: text("updated_at").notNull().default(sql`(datetime('now'))`),
})
export const flowBuckets = sqliteTable("flow_buckets", {
id: integer("id").primaryKey({ autoIncrement: true }),
serverId: integer("server_id")
.notNull()
.references(() => servers.id, { onDelete: "cascade" }),
bucketAt: text("bucket_at").notNull(),
src: text("src").notNull(),
dst: text("dst").notNull(),
proto: integer("proto").notNull().default(0),
srcPort: integer("src_port").notNull().default(0),
dstPort: integer("dst_port").notNull().default(0),
bytes: integer("bytes").notNull().default(0),
packets: integer("packets").notNull().default(0),
inIface: text("in_iface").notNull().default(""),
}, (t) => [
uniqueIndex("idx_flow_buckets_unique").on(
t.serverId, t.bucketAt, t.src, t.dst, t.proto, t.srcPort, t.dstPort,
),
])
export const trafficSamples = sqliteTable("traffic_samples", {
id: integer("id").primaryKey({ autoIncrement: true }),
serverId: integer("server_id")
@@ -595,6 +639,8 @@ export type SnapshotInsert = typeof serverSnapshots.$inferInsert
export type FilterRuleRow = typeof filterRules.$inferSelect
export type RecursiveRouteRow = typeof recursiveRoutes.$inferSelect
export type TrafficSettingsRow = typeof trafficSettings.$inferSelect
export type TrafficFlowSettingsRow = typeof trafficFlowSettings.$inferSelect
export type FlowBucketRow = typeof flowBuckets.$inferSelect
export type ServersApiPingSettingsRow = typeof serversApiPingSettings.$inferSelect
export type TrafficSampleRow = typeof trafficSamples.$inferSelect
export type UptimeSettingsRow = typeof uptimeSettings.$inferSelect