diff --git a/changelog.md b/changelog.md index cac7439..01d3f77 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,12 @@ # Changelog +## 2026-03-30 - 1.1.0 - feat(nft) +add source IP filtering for DNAT rules and expose table existence checks + +- Adds an optional sourceIP field to NAT rule definitions to restrict DNAT rules to matching source addresses or subnets. +- Updates DNAT rule generation to include an ip saddr match when sourceIP is provided. +- Introduces a tableExists() manager method to detect whether the managed nftables table is still present in the kernel. + ## 2026-03-26 - 1.0.1 - fix(repo) no changes to commit diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 0704db4..d6ec9c6 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@push.rocks/smartnftables', - version: '1.0.1', + version: '1.1.0', description: 'A TypeScript module for managing nftables rules including NAT, firewall, and rate limiting with a high-level API.' } diff --git a/ts/nft.manager.ts b/ts/nft.manager.ts index d2d68f3..954cabd 100644 --- a/ts/nft.manager.ts +++ b/ts/nft.manager.ts @@ -133,6 +133,22 @@ export class SmartNftables { this.hasFilterChains = false; } + /** + * Check if the managed nftables table exists in the kernel. + * Returns false if not root, not initialized, or the table was removed externally. + */ + public async tableExists(): Promise { + if (!this.executor.isRoot() || !this.initialized) { + return false; + } + try { + await this.executor.exec(`nft list table ${this.family} ${this.tableName}`); + return true; + } catch { + return false; + } + } + /** * Get status report of the managed nftables state. */ diff --git a/ts/nft.rulebuilder.nat.ts b/ts/nft.rulebuilder.nat.ts index aa49353..976dda9 100644 --- a/ts/nft.rulebuilder.nat.ts +++ b/ts/nft.rulebuilder.nat.ts @@ -26,8 +26,9 @@ export function buildDnatRules( for (const proto of protocols) { // DNAT rule + const saddrFilter = rule.sourceIP ? `ip saddr ${rule.sourceIP} ` : ''; commands.push( - `nft add rule ${family} ${tableName} prerouting ${proto} dport ${rule.sourcePort} dnat to ${rule.targetHost}:${rule.targetPort}` + `nft add rule ${family} ${tableName} prerouting ${saddrFilter}${proto} dport ${rule.sourcePort} dnat to ${rule.targetHost}:${rule.targetPort}` ); // Masquerade (SNAT) unless preserveSourceIP is set diff --git a/ts/nft.types.ts b/ts/nft.types.ts index fb274c8..f0840fb 100644 --- a/ts/nft.types.ts +++ b/ts/nft.types.ts @@ -14,6 +14,8 @@ export interface INftDnatRule { targetPort: number; protocol?: TNftProtocol; preserveSourceIP?: boolean; + /** Filter by source IP/subnet (e.g. '10.8.0.0/24'). Only matching traffic gets DNAT'd. */ + sourceIP?: string; } export interface INftSnatRule {