{"id":19429772,"url":"https://github.com/chayleaf/notnft","last_synced_at":"2025-06-10T20:36:52.027Z","repository":{"id":174780057,"uuid":"652703179","full_name":"chayleaf/notnft","owner":"chayleaf","description":"Nix DSL for nftables","archived":false,"fork":false,"pushed_at":"2024-09-17T20:45:28.000Z","size":307,"stargazers_count":40,"open_issues_count":0,"forks_count":2,"subscribers_count":4,"default_branch":"master","last_synced_at":"2024-09-18T01:36:03.339Z","etag":null,"topics":["dsl","nftables","nix","nixos","nixos-module"],"latest_commit_sha":null,"homepage":"","language":"Nix","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/chayleaf.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"COPYING","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2023-06-12T16:06:46.000Z","updated_at":"2024-09-17T20:45:31.000Z","dependencies_parsed_at":"2023-12-20T14:13:47.053Z","dependency_job_id":"6a71c85f-e52d-4cf1-9e79-82dc082130c2","html_url":"https://github.com/chayleaf/notnft","commit_stats":{"total_commits":56,"total_committers":1,"mean_commits":56.0,"dds":0.0,"last_synced_commit":"b3e6a023a13a81d70a6a30997e2f1aaf36feafb3"},"previous_names":["chayleaf/notnft"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chayleaf%2Fnotnft","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chayleaf%2Fnotnft/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chayleaf%2Fnotnft/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chayleaf%2Fnotnft/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/chayleaf","download_url":"https://codeload.github.com/chayleaf/notnft/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":223962607,"owners_count":17232503,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dsl","nftables","nix","nixos","nixos-module"],"created_at":"2024-11-10T14:21:11.472Z","updated_at":"2024-11-10T14:21:12.041Z","avatar_url":"https://github.com/chayleaf.png","language":"Nix","funding_links":[],"categories":["NixOS Modules"],"sub_categories":[],"readme":"# Not Nft\n\nThis is a partly-typed version of nftables's JSON format for Nix (it\nchecks JSON structure and expression contexts; it doesn't check the\ntypes themselves). It uses nixpkgs's option system, which means it can\nintegrate with Nix very well. A DSL is provided to write JSON nftables\nwith more safety and convenience, but you can alternatively simply\ndirectly follow the official schema if you're more comfortable with\nthat.\n\nDocs are available in [DOCS.md](./DOCS.md).\n\nnftables' documentation is *really* poor, so to the extent possible I\ncollected personal internal documentation in [NOTES.md](./NOTES.md). The\nmost important thing is not to rely on the official documentation for\n~~anything~~ obscure bits (it's fine for the common use cases, but in\ngeneral you really can't trust it). I'll try to push some documentation\nchanges upstream.\n\n- Q: How can I quickly test it?\n- A: Clone this repo and edit/run `./sample.nix`\n- Q: Why?\n- A: I'm working on a fresh NixOS router config, and wanted to nixify\n  the nftables syntax. Since this project uses nixpkgs's module system,\n  I can easily add options that directly map to nftables concepts.\n- Q: Is this limited in any way?\n- A: I fully support the current JSON specification... no, I refuse to\n  call it a specification - I fully support whatever JSON parsing code\n  there is in nftables, but the nftables DSL has a different feature set\n  compared to the JSON API (some features are only present in the\n  former, some only in the latter). I might add a compiler to .nft files\n  some day.\n- Q: Why the name?\n- A: I already created [notlua](https://github.com/chayleaf/notlua), so\n  this is the next project in that \"series\".\n- Q: Does this have any relation to Non-Fungible Tokens?\n- A: As the name implies, no.\n- Q: What license is this available under?\n- A: GPL2.0-or-later, same as nftables (some parts of nftables are GPL2\n  only though).\n- Q: How do I apply JSON rules?\n- A: If you run `nft` with the `-j` flag, it allows you to load json\n  rulesets. Also, you can quickly apply small commands via `nft -j\n  '{\"nftables\":[ ...your rules here ]}'`. You can't currently do this\n  via `networking.nftables`, but you can do it in\n  [nixos-router](https://github.com/chayleaf/nixos-router).\n- Q: How to use this in my config?\n- A: Add this flake's `nixosModules.default` output, and then either use\n  `config.notnft` or the module argument `notnft`.\n\n## Example using the \"fancy\" DSL\n\n```nix\nwith notnft.dsl; with payload; ruleset {\n  # nftables has a loooot of enums. You can access them directly (e.g.\n  # notnft.families.netdev), but it really is hard to remember them all.\n  # While the nftables language just dynamically figures out what you\n  # wanted to say, I tried to implement the same logic, but to pass the\n  # info back to the user I have to use lambdas.\n  # Of course, you can simply use strings instead (e.g. \"netdev\"), but\n  # that way you won't be aware of typos/wrongly used values.\n  # There's an advanced feature called \"One Enum to Rule Them All\" if\n  # you find this syntax bulky. See docs for more info.\n  filter = add table { family = f: f.netdev; } {\n    # chains are created by adding lists of statements to them, one list\n    # for each rule. You can alternatively pass a list of lists, in that\n    # case each sub-list will be considered a separate rule.\n    ingress_common = add chain \n      # is.eq is an alias for the match statement with the == operator\n      # payload.tcp.flags is the same as \"tcp flags\" in nftables\n      # language, and accesses the field \"flags\" of \"tcp\" payload\n      [(is.eq (bit.and tcp.flags (f: bit.or f.fin f.syn)) (f: bit.or f.fin f.syn)) drop]\n      [(is.eq (bit.and tcp.flags (f: bit.or f.syn f.rst)) (f: bit.or f.syn f.rst)) drop]\n      [(is.eq (bit.and tcp.flags (f: with f; bit.or fin syn rst psh ack urg)) 0) drop]\n      # In the nftables language, you often see stuff like\n      # \"tcp flags syn\" to check if syn is set in tcp flags, not using\n      # any operator between the two values. The same logic is available\n      # in notnft via \"is\" for automatically inferring the operation.\n      # tcpOpt is for getting the value of a tcp option field\n      # (or checking for presence of a tcp option)\n      [(is tcp.flags (f: f.syn)) (is.eq tcpOpt.maxseg.size (range 0 500)) drop]\n      [(is.eq ip.saddr \"127.0.0.1\") drop]\n      [(is.eq ip6.saddr \"::1\") drop]\n      [(is.eq (fib (f: with f; [ saddr iif ]) (f: f.oif)) missing) drop]\n      [return];\n\n    ingress_lan = add chain { type = f: f.filter; hook = f: f.ingress; dev = \"lan0\"; prio = -500; policy = f: f.accept; }\n      [(jump \"ingress_common\")];\n\n    ingress_wan = add chain { type = f: f.filter; hook = f: f.ingress; dev = \"wan0\"; prio = -500; policy = f: f.drop; }\n      [(jump \"ingress_common\")]\n      # in nftables language, anonymous sets are used quite often via\n      # the syntax { a, b, c }. Here you have to create them using \"set\"\n      [(is.ne (fib (f: with f; [ daddr iif ]) (f: f.type)) (f: with f; set [ local broadcast multicast ])) drop]\n      [(is.eq ip.protocol (f: f.icmp)) (is.eq icmp.type (f: with f; set [ info-request address-mask-request router-advertisement router-solicitation redirect ])) drop]\n      [(is.eq ip6.nexthdr (f: f.ipv6-icmp)) (is.eq icmpv6.type (f: with f; set [ mld-listener-query mld-listener-report mld-listener-reduction nd-router-solicit nd-router-advert nd-redirect router-renumbering ])) drop]\n      [(is.eq ip.protocol (f: f.icmp)) (limit { rate = 20; per = f: f.second; }) accept]\n      [(is.eq ip6.nexthdr (f: f.ipv6-icmp)) (limit { rate = 20; per = f: f.second; }) accept]\n      [(is.eq ip.protocol (f: f.icmp)) drop]\n      [(is.eq ip6.nexthdr (f: f.ipv6-icmp)) drop]\n      [(is.eq ip.protocol (f: with f; set [ tcp udp ])) (is.eq th.dport (set [ 22 53 80 443 853 ])) accept]\n      [(is.eq ip6.nexthdr (f: with f; set [ tcp udp ])) (is.eq th.dport (set [ 22 53 80 443 853 ])) accept];\n  };\n\n  global = add table { family = f: f.inet; } {\n    inbound_wan = add chain\n      [(is.eq ip.protocol (f: f.icmp)) (is.ne icmp.type (f: with f; set [ destination-unreachable echo-request time-exceeded parameter-problem ])) drop]\n      [(is.eq ip6.nexthdr (f: f.ipv6-icmp)) (is.ne icmpv6.type (f: with f; set [ destination-unreachable echo-request time-exceeded parameter-problem packet-too-big nd-neighbor-solicit ])) drop]\n      [(is.eq ip.protocol (f: f.icmp)) accept]\n      [(is.eq ip6.nexthdr (f: f.ipv6-icmp)) accept]\n      [(is.eq th.dport 22) accept];\n\n    inbound_lan = add chain\n      [accept];\n\n    inbound = add chain { type = f: f.filter; hook = f: f.input; prio = f: f.filter; policy = f: f.drop; }\n      [(vmap ct.state { established = accept; related = accept; invalid = drop; })]\n      [(is.eq (bit.and tcp.flags (f: f.syn)) 0) (is.eq ct.state (f: f.new)) drop]\n      [(vmap meta.iifname { lo = accept; wan0 = jump \"inbound_wan\"; lan0 = jump \"inbound_lan\"; })];\n\n    forward = add chain { type = f: f.filter; hook = f: f.forward; prio = f: f.filter; policy = f: f.drop; }\n      [(vmap ct.state { established = accept; related = accept; invalid = drop; })]\n      [(is.eq meta.iifname \"wan0\") (is.eq meta.oifname \"lan0\") accept]\n      [(is.eq meta.iifname \"lan0\") accept]\n      [(is.eq meta.iifname \"wan0\") (is.eq meta.oifname \"wan0\") accept];\n\n    postrouting = add chain { type = f: f.nat; hook = f: f.postrouting; prio = f: f.filter; policy = f: f.accept; }\n      [(is.eq meta.protocol (f: with f; set [ ip ip6 ])) (is.eq meta.iifname \"lan0\") (is.eq meta.oifname \"wan0\") masquerade];\n\n    block4 = add set { type = f: f.ipv4_addr; flags = f: with f; [ interval ]; } [\n      (cidr \"194.190.137.0\" 24)\n      (cidr \"194.190.157.0\" 24)\n      (cidr \"194.190.21.0\" 24)\n      (cidr \"194.226.130.0\" 23)\n    ];\n\n    block6 = add set { type = f: f.ipv6_addr; flags = f: with f; [ interval ]; };\n\n    force_unvpn4 = add set { type = f: f.ipv4_addr; flags = f: with f; [ interval ]; };\n\n    force_unvpn6 = add set { type = f: f.ipv6_addr; flags = f: with f; [ interval ]; };\n\n    prerouting = add chain { type = f: f.filter; hook = f: f.prerouting; prio = f: f.filter; policy = f: f.accept; }\n      # mangle means \"set A to B\", the nftables language analog to the\n      # following would be \"meta mark set ct mark\"\n      [(mangle meta.mark ct.mark)]\n      [(is.ne meta.mark 0) accept]\n      [(is.eq meta.iifname \"lan0\") (mangle meta.mark 2)]\n      # you can access named sets via \"@set_name\"\n      [(is.eq ip.daddr \"@force_unvpn4\") (mangle meta.mark 1)]\n      [(is.eq ip6.daddr \"@force_unvpn6\") (mangle meta.mark 1)]\n      [(is.eq ip.daddr \"@block4\") drop]\n      [(is.eq ip6.daddr \"@block6\") drop]\n      [(mangle ct.mark meta.mark)];\n  };\n}\n```\n\nYou can use `add existing chain` or `add existing table` if you want to\nextend an existing chain/table without issuing a command for creating\nit.\n\nEquvalent nftables config:\n\n```nftables\ntable netdev filter {\n  chain ingress_common {\n    tcp flags \u0026 (fin|syn) == (fin|syn) drop\n    tcp flags \u0026 (syn|rst) == (syn|rst) drop\n    tcp flags \u0026 (fin|syn|rst|psh|ack|urg) == 0 drop\n    tcp flags syn tcp option maxseg size 0-500 drop\n    ip saddr 127.0.0.1 drop\n    ip6 saddr ::1 drop\n    fib saddr . iif oif missing drop\n    return\n  }\n\n  chain ingress_lan {\n    type filter hook ingress device \"lan0\" priority -500; policy accept;\n    jump ingress_common\n  }\n\n  chain ingress_wan {\n    type filter hook ingress devices = { $EXT } priority -500; policy drop;\n    jump ingress_common\n    fib daddr . iif type != { local, broadcast, multicast } drop\n    ip protocol == icmp icmp type == { info-request, address-mask-request, router-advertisement, router-solicitation, redirect } drop\n    ip6 nexthdr == ipv6-icmp icmpv6 type == { mld-listener-query, mld-listener-report, mld-listener-reduction, nd-router-solicit, nd-router-advert, nd-redirect, router-renumbering } drop\n    ip protocol == icmp limit rate 20/second accept\n    ip6 nexthdr == ipv6-icmp limit rate 20/second accept\n    ip protocol == icmp drop\n    ip6 nexthdr == ipv6-icmp drop\n    ip protocol == { tcp, udp } th.dport == { 22, 53, 80, 443, 853 } accept\n    ip6 nexthdr == { tcp, udp } th.dport == { 22, 53, 80, 443, 853 } accept\n  }\n}\n\ntable inet global {\n  chain inbound_wan {\n    ip protocol == icmp icmp type != { destination-unreachable, echo-request, time-exceeded, parameter-problem } drop\n    ip6 nexthdr == ipv6-icmp icmpv6 type != { destination-unreachable, echo-request, time-exceeded, parameter-problem, packet-too-big, nd-neighbor-solicit } drop\n    ip protocol == icmp accept\n    ip6 nexthdr == ipv6-icmp accept\n    th dport == 22 accept\n  }\n\n  chain inbound_lan {\n    accept\n  }\n\n  chain inbound {\n    type filter hook input priority filter; policy drop;\n\n    ct state vmap { established : accept, related : accept, invalid : drop }\n\n    tcp flags \u0026 syn == 0 ct state new drop\n\n    iifname vmap {\n      lo : accept,\n      wan0 : jump inbound_wan,\n      lan0 : jump inbound_lan\n    }\n  }\n\n  chain forward {\n    type filter hook forward priority filter; policy drop;\n\n    ct state vmap { established : accept, related : accept, invalid : drop }\n    iifname == \"wan0\" oifname == \"lan0\" accept\n    iifname == \"lan0\" accept\n    iifname == \"wan0\" oifname == \"wan0\" accept\n  }\n\n  chain postrouting {\n    type nat hook postrouting priority filter; policy accept;\n    meta protocol == { ip, ip6 } iifname == \"lan0\" oifname == \"wan0\" masquerade;\n  }\n\n  set block4 {\n    type ipv4_addr;\n    flags interval;\n    elements = {\n      194.190.137.0/24,\n      194.190.157.0/24,\n      194.190.21.0/24,\n      194.226.130.0/23\n    };\n  }\n\n  set block6 {\n    type ipv6_addr;\n    flags interval;\n  }\n\n  set force_unvpn4 {\n    type ipv4_addr;\n    flags interval;\n  }\n\n  set force_unvpn6 {\n    type ipv6_addr;\n    flags interval;\n  }\n\n  chain prerouting {\n    type filter hook prerouting priority filter; policy accept;\n    meta mark set ct mark\n    meta mark != 0x0 accept\n    iifname == \"lan0\" meta mark set 0x2\n    ip daddr @force_unvpn4 meta mark set 0x1\n    ip6 daddr @force_unvpn6 meta mark set 0x1\n    ip daddr @block4 drop\n    ip daddr @block6 drop\n    ct mark set meta mark\n  }\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchayleaf%2Fnotnft","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fchayleaf%2Fnotnft","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchayleaf%2Fnotnft/lists"}