@push.rocks/smartnftables

A TypeScript module for managing Linux nftables rules with a high-level, type-safe API. Handles NAT (DNAT/SNAT/masquerade), firewall rules, IP sets, and rate limiting — all from clean, declarative TypeScript.

Issue Reporting and Security

For reporting bugs, issues, or security vulnerabilities, please visit community.foss.global/. This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a code.foss.global/ account to submit Pull Requests directly.

Install

pnpm install @push.rocks/smartnftables
# or
npm install @push.rocks/smartnftables

The legacy SmartNftables helper tracks rules without applying them when it lacks root privileges. ManagedNftables always rejects unsupported enforcement; it has no dry-run or memory-only enforcement mode.

Managed workload policy

ManagedNftables provides a separate native owner for complete, interface-bound IPv4 policy. It requires Linux 6.9 or newer, CAP_NET_ADMIN in its network namespace, and working nftables OWNER/PERSIST support. The published native matrix is Linux x86_64 and arm64, statically linked with musl. Actual kernel support is checked when applying policy; starting the compiler does not establish enforcement.

An optional caller-owned networkNamespaceFd selects an already-created Linux network namespace. Keep that descriptor valid through start(). The native process validates and enters its inherited copy before readiness, owner identity capture or netlink activity, then closes the copy. Invalid descriptors and denied entry fail before readiness. The option is process configuration and never enters openOwner, policy commands or durable receipts. Native callers use --management --network-namespace-fd 3 with an inherited descriptor at FD 3.

The caller owns the namespace lifetime, links and routes. Retain a namespace owner across process loss and recovery: PERSIST retains policy only while its namespace exists. An entered process keeps its namespace membership if the source descriptor is closed, but a replacement process requires a valid descriptor. Receipts bind the actual target namespace device and inode; recovery in another namespace rejects. Await complete policy and process cleanup before releasing the caller's final namespace owner. Namespace entry does not create uplink authority, drain conntrack flows or authorize address and port reuse.

The Linux x86_64 musl candidate passes isolated Linux 6.18.35 kernel tests, including inherited namespace entry, retained policy after process loss, recovery in the original namespace, rejection in another namespace, and denied entry before readiness. ARM binaries are built; privileged ARM and complete Pallet workload integration remain separate qualification requirements.

The caller owns workload links and durable state. Keep links disabled until their policy is confirmed, serialize link changes with policy operations, and disable them before cleanup. Persist the full prepared transition before calling reconcile(), then retain the full returned receipt in the caller's SmartData store. This library does not persist runtime state in files.

import { ManagedNftables } from '@push.rocks/smartnftables';

const owner = new ManagedNftables({
  ownerId: 'node-policy',
  instanceId: 'caller_retained_unique_instance',
  tableName: 'snft_workloads',
});
await owner.start(); // Compiler/IPC readiness only; no kernel policy yet.

const target = await owner.prepare({
  schemaVersion: 1,
  revision: 1,
  endpoints: [
    { id: 'workload-a', interfaceIndex: 12, interfaceName: 'worka', interfaceKind: 'veth', sourcePrefixes: ['10.81.1.2/32'] },
    { id: 'workload-b', interfaceIndex: 13, interfaceName: 'workb', interfaceKind: 'veth', sourcePrefixes: ['10.81.2.2/32'] },
  ],
  rules: [
    {
      sourceEndpoint: 'workload-a', destinationEndpoint: 'workload-b',
      sourcePrefix: '10.81.1.2/32', destinationPrefix: '10.81.2.2/32',
      protocol: 'tcp', sourcePort: null, destinationPort: 443,
    },
    {
      sourceEndpoint: 'workload-b', destinationEndpoint: 'workload-a',
      sourcePrefix: '10.81.2.2/32', destinationPrefix: '10.81.1.2/32',
      protocol: 'tcp', sourcePort: 443, destinationPort: null,
    },
  ],
});
const transition = { previous: null, target };
// Persist this complete transition before the next operation.
const applied = await owner.reconcile(transition);
// Persist the complete applied result before enabling the caller-owned links.
const status = await owner.inspect();
if (!status.enforced) throw new Error('Workload policy is not confirmed.');

// After disabling/quiescing these workload links:
await owner.release(applied);
await owner.close();

Interface numbers and names in this example must be replaced with the caller's actual, already-created links. Native RTM_GETLINK checks the name/index pair, the declared veth or L3 tun kind and absence of a bridge/master attachment. The caller must prove and own each veth peer and workload network namespace. Both selectors must match for a grant. A stale name/index pair rejects application; renaming either selector retains denial. Do not reuse interface identities or workload addresses while their previous grants remain outstanding.

Method Behavior
start() Starts one private native process and returns idle status.
prepare(policy) Validates, canonicalizes and hashes an inert complete policy without kernel writes.
reconcile({ previous, target }) Creates or replaces the entire policy in one kernel transaction. previous is null initially, otherwise the full prior applied result.
inspect() Reads the current owned graph and reports enforced, pending intent and a bounded error code.
release(applied) Verifies the exact retained graph and confirms deletion; repeats are idempotent.
detach(applied) Selects terminal retention, verifies the same graph with PERSIST-only flags after dropping socket ownership, then joins the native process. Returns { detached: true }; retain the full applied result.
close() Stops command admission, joins admitted policy operations, confirms owned-table deletion unless detach() selected retention, then joins the native process. Failure retains the owner for explicit recovery.

Schema-v1 policies are directed. Return traffic requires its own grant; there is no broad connection-tracking bypass. A null endpoint denotes the actual local host, with an explicit prefix that cannot overlap any endpoint. Such a grant applies only to input/output. Forwarding requires two explicit endpoint references; a relay TUN is an endpoint with interfaceKind: 'tun' and its authenticated remote source prefixes. The relay owner must authenticate that remote source authority. Managed interfaces reject other IPv4 traffic and IPv6 traffic. Ethernet/ARP is outside these inet hooks; workload links must use the qualified routed layout. Unmatched interfaces retain their existing behavior. The compiler creates only inet input, forward and output filter chains; it does not configure links, routes, NAT, DNS, host defaults, forwarding sysctls, or relay sessions. An ungranted destination is denied independently of DNS answers.

The schema permits at most 32 interfaces, 16 source prefixes per interface and 128 directed rules, additionally bounded to 768 compiled operations and 100,000 encoded bytes. IPv4 prefixes must be canonical, non-overlapping local allocations; loopback, unspecified and multicast allocations reject. Ports require explicit TCP/UDP and all nullable fields must be present. The facade rejects getters, proxies, live objects and concurrent commands rather than queuing unbounded work.

One NETLINK_NETFILTER socket exclusively owns the table with OWNER/PERSIST flags. An acknowledged batch commits the complete graph atomically. Lost acknowledgements retain the exact pending transition; retry only that same body. Reconnect/restart uses the same caller-retained owner, instance, table and boot/namespace receipt. Recovery checks the complete graph before and after orphan adoption. The kernel's wrapping generation counter only fences concurrent transactions; it is never a durable revision or sufficient ownership proof. Foreign owners, unexpected tables, chains, rules, sets or objects inside the owned table reject without deletion. Other tables coexist independently. Linux's family-wide chain dump is explicitly filtered by table identity before graph comparison; foreign chains are never included in replacement or cleanup.

PERSIST deliberately retains accepted static grants if the native process crashes. It supplies no wall-clock lease or timed revocation. The caller must keep revocation pending until policy replacement or workload fencing is confirmed. Host reboot invalidates the old boot receipt; the caller must fence its old link/workload lifecycle before starting a new owner instance. Cleanup failures and uncertain operations must not be reported as successful release or used to transfer authority.

For an orderly restart that must preserve policy, persist the complete applied result and call await owner.detach(applied) before calling close(). The native operation validates the exact receipt, graph, interfaces, boot and namespace. It accepts this socket's OWNER/PERSIST table or an exact orphan; it never adopts a foreign owner. After closing the owner socket it reads the graph again and requires the same handle and compiled body with PERSIST-only flags. Native inspection then reports state: 'retained'. The facade returns success only after child termination.

Calling detach() irreversibly selects retention cleanup, including invalid input, transport failure, timeout, native rejection or malformed acknowledgement. Subsequent close() only joins and terminates; it never sends a table-deletion command. It cannot undo a close() already admitted earlier. Other policy operations reject. Resolve any pending policy transition by exact replay before requesting detachment. If a detach ACK is lost while the process remains available, only the same captured body may be retried. After process loss, start a fresh owner with identical options and retry detach(applied) against the exact orphan. Absent graphs, foreign active owners, changed interfaces, handles, bodies or boot/namespace identities reject. Native release/reconcile/close commands reject after a valid detach intent, even when post-drop verification fails; explicit recovery requires the retained body.

Detachment confirms retention at observation time. A completed facade retry returns its existing acknowledgement; it is not a fresh kernel inspection. Retention does not guarantee future enforcement, namespace survival or reboot ordering, and does not withdraw external authority, drain packets, clear conntrack or release leases. A separately created owner can later recover the full policy and perform an explicit release after the caller has fenced that authority and packet lifecycle.

Apply and release receipts describe this owner's table and policy operations. They do not certify that packets admitted under an earlier graph have drained: foreign NFQUEUE or other deferred packet owners may still hold such packets. The caller must qualify the complete packet path or independently fence the workload before treating revocation as complete or reusing its addresses and interfaces. Table deletion does not release IP allocation authority or clean up conntrack/NAT state. This restriction also applies to private veth/TUN forwarding.

Combined router egress and host transit

ManagedNftables<IManagedNftPolicyV2> accepts the exported schema-v2 policy. The prepared/applied/transition/status interfaces accept the same policy type parameter; existing callers default to the unchanged schema-v1 contract. V1 canonical digests and compiled bytes are preserved. V2 uses its own hash domain. An owner cannot transition between v1 private, v2 router, v2 host transit, and v2 allocation-pool guard policy kinds.

V2 scope Required authority and behavior
routerEgress Private endpoints and rules, one exact links binding per endpoint, a separate veth handoff, protection, and active generations. Private veth/TUN/local DNS and egress share one table so terminal private denial cannot override a separate egress table.
hostTransit Exact handoff link/allocations pairs, complete protection, an explicit veth or Ethernet uplink, and its current snatAddress. It checks each handoff's leased source address and protocol/port range, default conntrack zone, direction, uplink, and protected destinations before outer SNAT.
allocationPoolGuard An authenticated authorityDigest and complete current allocation-pool prefixes. Installs host-wide IPv4 destination denial before any handoff exists, without link, uplink or SNAT dependencies.

allocationPoolGuard accepts 164 canonical, disjoint RFC1918 prefixes. Supply the actual allocation pools, not the broader protected union containing management LANs, resolvers and platform endpoints. It applies to IPv4 INPUT, FORWARD and OUTPUT at filter priority 0, after normal destination NAT. It drops an original conntrack destination in a pool. Reply-direction packets additionally check their current source, then return; all remaining packets check their current destination. This denies DNAT into or away from a pool and untracked current-destination traffic, while permitting reverse-SNAT replies to authorized transit sources. It grants no egress permissions; exact hostTransit and router policies remain separate owners. Unrelated host traffic and IPv6 retain their existing behavior.

For example, a guard policy is { schemaVersion: 2, revision: 1, scope: { kind: 'allocationPoolGuard', authorityDigest, prefixes: ['10.240.0.0/16', '10.241.0.0/16'] } }. Hashing binds the supplied body, but does not authenticate the authority or prove its completeness. An empty hostTransit.handoffs array provides no host-wide denial and is not a substitute for this scope.

The caller must retain the authenticated authority, complete transition and applied receipt, verify inspect().enforced, and qualify the complete packet path. This guard alone proves neither early-boot/late-shutdown ordering nor protection after kernel reboot. It does not fence foreign packet queues, later packet rewrites, offloads or proxies, and supplies no quarantine release, conntrack drainage or allocation-reuse evidence. Historical leases remain the caller's durable ownership responsibility. PERSIST retains this table only while its network namespace exists; release() and ordinary close() delete it, so revoke any external protection claim before release, or use the verified retention operation described above.

Local bindings include name, index, kind, MAC (null for L3 TUN), interface-link index, and required IPv4 addresses. RTM_GETLINK/RTM_GETADDR verify those local facts at apply, recovery and inspection. Ethernet must be unbridged driver-backed Ethernet, including VirtIO; virtual VLAN/bond/bridge/dummy kinds are not inferred uplinks. The caller retains actual peer namespaces and link-generation ownership. These serialized facts are not native lifetime capabilities or a continuous link-change monitor. Address, DHCP and route changes require caller fencing.

protection carries an authority digest, non-overlapping protected IPv4 prefixes and exact platform endpoint IDs/address/protocol/port tuples. The caller must authenticate and supply complete authority. Hashing does not prove completeness. A public grant means its declared IPv4 prefix excluding the protected union; only an explicit platform endpoint grant admits a protected destination. Host checks cover both the current packet destination and original conntrack destination, so foreign DNAT cannot turn a protected destination into a public exception or redirect public traffic into protected space. INPUT diversion, local OUTPUT into handoffs, unmatched handoff traffic and IPv6 forwarding are denied.

Each router generation binds an immutable lease reference, transit source address, leased TCP/UDP source-port ranges, a nonzero conntrack zone and a 16-byte label. Each directed grant selects one exact leased protocol range for SNAT. A grant's source is a workload veth or the actual router-local host with an exact assigned IPv4 source address; TUN endpoints retain private routing only. Router-local DNS and relay transports therefore need explicit local-origin grants. Raw PREROUTING and OUTPUT classify before conntrack; filter rules validate current and original tuples, links, direction, zone, state and generation label on every packet. Opening TCP requires SYN with FIN/RST/ACK clear and permits ECN negotiation. Replies must match the labelled original flow and current directed grant. There is no broad ESTABLISHED/RELATED bypass.

Active overlapping classifiers, duplicate zones/labels and conflicting handoff allocations reject. Empty router generations retain private routing while denying egress. Input bounds include 32 private endpoints, 128 private rules, 32 active generations, 128 total egress grants, 128 protected prefixes, 96 platform endpoints, 16 ranges per allocation, and 32 host handoffs/active allocations. The complete compiled graph still must fit 768 operations and 100,000 bytes; cross-products can reach that limit before individual input limits. Exhausted source-port ranges drop new flows rather than allocate outside the lease.

The caller must coordinate router and host apply order, retain complete intents and receipts, authenticate protected authority, prevent reuse of quarantined leases, and qualify other packet owners. An ACCEPT in this table cannot override Docker's independent FORWARD DROP. ManagedDockerForwarding, described below, owns the separate DOCKER-USER contribution. The caller must also control deferred packets, proxy/BPF/ offload paths and changing network authority. No table receipt proves flow drainage, conntrack cleanup, elapsed-time expiry, or safe address/port/zone reuse.

Docker forwarding contribution

ManagedDockerForwarding admits the leased handoff traffic from an exact applied schema-v2 hostTransit receipt through Docker's existing IPv4 forwarding path. It reads and verifies that dedicated barrier's complete table and local links, without adopting its ownership. The contribution uses Docker's supported DOCKER-USER extension point. Docker's native nftables backend has no equivalent user chain and is unsupported.

The host must provide root-owned /usr/sbin/iptables-nft, /usr/sbin/iptables-nft-save and /usr/sbin/iptables-nft-restore, using the same 1.8.10-or-newer 1.8-series nf_tables frontend. prepare() captures the exact frontend version in its digest. Apply, inspection and recovery require that same version. Subprocesses use fixed argv, a clean environment, bounded input/output, one shared eight-second frontend deadline per request, and joined termination. No shell or raw rule API is exposed.

One node-level contribution owns a contiguous leading block in DOCKER-USER. FORWARD must already have policy DROP and its first two unconditional jumps must be DOCKER-USER and DOCKER-FORWARD. Their exact ordered raw graphs are checked without claiming their ownership. Other managed contribution owners, altered or duplicated owned rules, missing chains and changed placement reject. Unrelated rules following the owned block and Docker's own chains remain separate owners. Saved text verifies placement, count and normalized policy. Native netlink reads verify every contributed rule's complete ordered expression graph, including register flow and all match bytes; only counter values and attribute encoding order/flags are normalized. An exact xtables -C check also verifies each compiled deletion specification. These reads must share an unchanged ruleset generation; concurrent Docker reconciliation can therefore make inspection inconclusive. Conntrack source addresses use the canonical bare IPv4 form because an explicit /32 has different hidden match bytes in these frontends. A semantically equal rewrite with a different exact representation rejects, as does an early ACCEPT that the frontend reconstructs as the same command. The contribution never creates, flushes, adopts or deletes a Docker table/chain, and never changes a host forwarding policy.

Each contributed rule binds the handoff and uplink names, current and original transit source, exact leased TCP/UDP port range, packet direction and connection state. New TCP flows require SYN with FIN/RST/ACK clear; reverse traffic requires ESTABLISHED. The exact v2 host barrier independently checks interface indices, MAC/iflink facts, the default host conntrack zone, protected original/current destinations and explicit outer SNAT. A complete contribution is bounded to 192 rules and 100,000 generated command bytes.

Create the class with ownerId, a caller-retained instanceId of at least 16 identifier characters, and optional binaryPath/networkNamespaceFd. After start(), call prepare({ schemaVersion: 1, revision, barrier }), where barrier is the exact applied host policy. Persist the complete { previous, target } transition before reconcile(). Keep every affected handoff link down through mutations; retain those exact links until cleanup finishes. The native owner verifies this down state before and after its single no-flush restore transaction. Exact target replay can recover a lost response without rewriting live rules.

inspect().present confirms the contribution and its referenced barrier, not the complete host packet path or durable controller authority. Errors retain failed-owned state and pending intent. release(applied) and close() delete only exact contributed rules under the same link fence. A cold boot needs a new barrier receipt and caller-authorized replay; old boot receipts cannot authorize native operations. The caller owns restart ordering, disjoint retained leases, exclusive privileged mutation authority and activation. The frontend transaction does not provide compare-and-swap against other privileged processes.

Native qualification

The separate Docker fixture exercises the native contribution with pinned Docker packages on offline Ubuntu 26.04 and Ubuntu 24.04.4/HWE guests, including retained-state cold boots and published workloads. Its checked-in input manifest and per-run receipts identify the tested artifacts.

cargo test --manifest-path rust/Cargo.toml --locked runs unprivileged compiler, snapshot and bounded-subprocess tests. The ignored native cases require the explicitly marked disposable guest from test/native/qualify.py; requesting them on an ordinary host fails its scope check. Qualification uses an offline Linux 6.18.35 x86_64 guest with no host disks, mounts or external network backend. One VirtIO device connects only to a singleton QEMU-internal hub; packet-path tests use guest-owned namespaces, veth pairs and TUN. It covers UDP/TCP grants, direct-IP denial, source spoofing, IPv6 denial, renamed interfaces, foreign ownership, atomic rollback, generation conflicts and lost-ACK recovery. V2 tests exercise complete graph persistence/recovery, private and local DNS, local TCP, TUN isolation, observed UDP/TCP handoff ranges, range exhaustion, fragment reassembly, ECN SYN and invalid opening flags, protected current/original DNAT barriers, nonzero foreign zones, local diversion, IPv6 denial, revocation and restoration with disjoint generation ranges while old flows remain retained. The arm64 binary is cross-built; native packet qualification is currently x86_64. Kernel 6.8 is unsupported. No production activation is implied by these tests.

Native dependency and standard-library attribution is included in native-notices/. Distribution must retain these notices alongside the binaries.

Quick Start

import { SmartNftables } from '@push.rocks/smartnftables';

const nft = new SmartNftables();
await nft.initialize();

// Port forward 8080 → 192.168.1.100:80
await nft.nat.addPortForwarding('web', {
  sourcePort: 8080,
  targetHost: '192.168.1.100',
  targetPort: 80,
});

// Block a suspicious IP
await nft.firewall.blockIP('10.0.0.99');

// Rate limit HTTP to 100 req/s per IP
await nft.rateLimit.addRateLimit('http-limit', {
  port: 80,
  protocol: 'tcp',
  rate: '100/second',
  perSourceIP: true,
});

// Clean up everything when done
await nft.cleanup();

Architecture 🏗️

The library is organized around a facade pattern with specialized sub-managers:

SmartNftables (main facade)
├── nat          → NatManager       (DNAT, SNAT, masquerade)
├── firewall     → FirewallManager  (filter rules, IP sets, stateful tracking)
└── rateLimit    → RateLimitManager (packet/connection rate limiting)

All rules are tracked in rule groups identified by string IDs, so you can add, inspect, and remove them programmatically.

API Reference

SmartNftables — Main Facade

const nft = new SmartNftables({
  tableName: 'smartnftables', // nftables table name (default: 'smartnftables')
  family: 'ip',               // 'ip' | 'ip6' | 'inet' (default: 'ip')
  dryRun: false,               // generate commands without executing (default: false)
});
Method Description
initialize() Create the nftables table and NAT chains. Idempotent.
cleanup() Delete the entire table and clear all tracking.
status() Get an INftStatus report of the current managed state.
applyRuleGroup(id, commands) Apply and track a group of raw nft commands.
removeRuleGroup(id) Remove a tracked rule group.
getRuleGroup(id) Retrieve a tracked rule group by ID.

🌐 NAT — nft.nat

Port Forwarding (DNAT)

await nft.nat.addPortForwarding('my-service', {
  sourcePort: 443,
  targetHost: '10.0.0.5',
  targetPort: 8443,
  protocol: 'tcp',           // 'tcp' | 'udp' | 'both' (default: 'tcp')
  preserveSourceIP: false,    // skip masquerade if true (default: false)
});

await nft.nat.removePortForwarding('my-service');

Port Range Forwarding

Map a range of ports to another host:

// Forward ports 3000-3010 → 10.0.0.5:3000-3010
await nft.nat.addPortRange('dev-ports', 3000, 3010, '10.0.0.5', 3000, 'tcp');
await nft.nat.removePortRange('dev-ports');

SNAT (Source NAT)

await nft.nat.addSnat('egress', {
  sourceAddress: '203.0.113.1',
  targetPort: 80,
  protocol: 'tcp',
});

Masquerade

await nft.nat.addMasquerade('outbound', {
  targetPort: 443,
  protocol: 'tcp',
});

🛡️ Firewall — nft.firewall

Basic Rules

await nft.firewall.addRule('allow-ssh', {
  direction: 'input',         // 'input' | 'output' | 'forward'
  action: 'accept',           // 'accept' | 'drop' | 'reject'
  sourceIP: '10.0.0.0/24',
  destPort: 22,
  protocol: 'tcp',
  comment: 'Allow SSH from trusted network',
});

await nft.firewall.removeRule('allow-ssh');

When sourcePort or destPort is provided without protocol, TCP is used by default. Protocol-only rules match the specified layer-4 protocol.

Block an IP

await nft.firewall.blockIP('10.0.0.99');
await nft.firewall.blockIP('192.168.0.0/16', { direction: 'forward' });

Allow Only Specific IPs on a Port

// Only these IPs can reach port 3306 — everything else is dropped
await nft.firewall.allowOnlyIPs('db-access', ['10.0.0.1', '10.0.0.2'], 3306, 'tcp');

Stateful Connection Tracking

// Allow established/related, drop invalid — on the input chain
await nft.firewall.enableStatefulTracking('input');

IP Sets

Create named sets and match against them:

// Create a set of blocked IPs
await nft.firewall.createIPSet({
  name: 'blocklist',
  type: 'ipv4_addr',
  elements: ['10.0.0.50', '10.0.0.51'],
});

// Dynamically add/remove elements
await nft.firewall.addToIPSet('blocklist', ['10.0.0.52']);
await nft.firewall.removeFromIPSet('blocklist', ['10.0.0.50']);

// Clean up
await nft.firewall.deleteIPSet('blocklist');

You can also build set-matching rules directly with the low-level builder:

import { buildIPSetMatchRule } from '@push.rocks/smartnftables';

const rule = buildIPSetMatchRule('smartnftables', 'ip', {
  setName: 'blocklist',
  direction: 'input',
  matchField: 'saddr',
  action: 'drop',
});

⏱️ Rate Limiting — nft.rateLimit

Packet Rate Limiting

// Global: drop packets over 1000/second on port 80
await nft.rateLimit.addRateLimit('http-global', {
  port: 80,
  protocol: 'tcp',
  rate: '1000/second',
  burst: 50,
  action: 'drop',
});

// Per-IP: each source IP gets its own 100/second limit
await nft.rateLimit.addRateLimit('http-per-ip', {
  port: 80,
  protocol: 'tcp',
  rate: '100/second',
  perSourceIP: true,
});

await nft.rateLimit.removeRateLimit('http-per-ip');

Connection Rate Limiting

Limit the rate of new connections (uses ct state new):

await nft.rateLimit.addConnectionRateLimit('ssh-connrate', {
  port: 22,
  protocol: 'tcp',
  rate: '5/second',
  perSourceIP: true,
});

await nft.rateLimit.removeConnectionRateLimit('ssh-connrate');

🔧 Low-Level Rule Builders

For advanced use cases, you can generate raw nft command strings without applying them:

import {
  buildDnatRules,
  buildSnatRule,
  buildMasqueradeRule,
  buildFirewallRule,
  buildRateLimitRule,
  buildPerIpRateLimitRule,
  buildConnectionRateRule,
  buildIPSetCreate,
  buildIPSetAddElements,
  buildIPSetRemoveElements,
  buildIPSetDelete,
  buildIPSetMatchRule,
  buildTableSetup,
  buildFilterChains,
  buildTableCleanup,
} from '@push.rocks/smartnftables';

const commands = buildDnatRules('mytable', 'ip', {
  sourcePort: 8080,
  targetHost: '10.0.0.5',
  targetPort: 80,
});
// → ['nft add rule ip mytable prerouting tcp dport 8080 dnat to 10.0.0.5:80',
//    'nft add rule ip mytable postrouting tcp dport 80 masquerade']

Dry Run Mode 🧪

Generate commands without touching the kernel — perfect for testing, debugging, or CI:

const nft = new SmartNftables({ dryRun: true });
await nft.initialize();
await nft.nat.addPortForwarding('test', {
  sourcePort: 80,
  targetHost: '10.0.0.1',
  targetPort: 8080,
});

console.log(nft.status());
// Rules tracked in memory, nothing executed

Status Reporting 📊

const status = nft.status();
// {
//   initialized: true,
//   tableName: 'smartnftables',
//   family: 'ip',
//   isRoot: true,
//   activeGroups: 3,
//   groups: {
//     'nat:web': { ruleCount: 2, createdAt: 1711411200000 },
//     'fw:block-10_0_0_99': { ruleCount: 1, createdAt: 1711411200100 },
//     'ratelimit:http-limit': { ruleCount: 1, createdAt: 1711411200200 },
//   }
// }

Types

All interfaces and types are fully exported for use in your own code:

Type Description
INftDnatRule DNAT port forwarding rule config
INftSnatRule Source NAT rule config
INftMasqueradeRule Masquerade rule config
INftFirewallRule Firewall filter rule config
INftIPSetConfig IP set creation config
INftRateLimitRule Rate limiting rule config
INftConnectionRateRule New-connection rate limit config
ISmartNftablesOptions Constructor options
INftStatus Status report shape
TNftProtocol 'tcp' | 'udp' | 'both'
TNftFamily 'ip' | 'ip6' | 'inet'
TFirewallAction 'accept' | 'drop' | 'reject'
TCtState 'new' | 'established' | 'related' | 'invalid'

This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the repository license file.

Please note: The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.

Trademarks

This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.

Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.

Company Information

Task Venture Capital GmbH
Registered at District Court Bremen HRB 35230 HB, Germany

For any legal inquiries or further information, please contact us via email at hello@task.vc.

By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.

S
Description
No description provided
Readme
997 KiB
Languages
HTML 50.4%
Rust 33.7%
TypeScript 11.7%
Python 4%
JavaScript 0.2%