@@ -82,6 +82,9 @@ func NewManagerWithOptions(opts Options) *Manager {
8282// Uses the same mutex as AddResolvedIPs so a /policy update never overlaps a DNS
8383// callback: without this, add-element could run while the table is being deleted/recreated
8484// and fail, causing a transient deny for a client that already got an allowed DNS answer.
85+ //
86+ // On every call (startup and /policy updates), static allow/deny and DoH blocklist
87+ // interval sets are normalized so overlapping CIDR/host pairs do not make nft fail.
8588func (m * Manager ) ApplyStatic (ctx context.Context , p * policy.NetworkPolicy ) error {
8689 if p == nil {
8790 p = policy .DefaultDenyPolicy ()
@@ -91,7 +94,10 @@ func (m *Manager) ApplyStatic(ctx context.Context, p *policy.NetworkPolicy) erro
9194 p .DefaultAction , len (allowV4 ), len (allowV6 ), len (denyV4 ), len (denyV6 ))
9295 m .mu .Lock ()
9396 defer m .mu .Unlock ()
94- script := buildRuleset (p , m .opts )
97+ script , err := buildRuleset (p , m .opts )
98+ if err != nil {
99+ return err
100+ }
95101 if _ , err := m .run (ctx , script ); err != nil {
96102 // On a fresh host the delete-table may fail; retry once without the delete line.
97103 if isMissingTableError (err ) {
@@ -109,7 +115,8 @@ func (m *Manager) ApplyStatic(ctx context.Context, p *policy.NetworkPolicy) erro
109115}
110116
111117// AddResolvedIPs adds DNS-learned IPs to dynamic allow sets with TTL-based timeout.
112- // TTL is clamped to minTTLSec–maxTTLSec. Call only when table exists (dns+nft mode).
118+ // Each element timeout is DNS TTL + 60s, then clamped to minTTLSec–maxTTLSec.
119+ // Call only when table exists (dns+nft mode).
113120func (m * Manager ) AddResolvedIPs (ctx context.Context , ips []ResolvedIP ) error {
114121 if len (ips ) == 0 {
115122 return nil
@@ -126,8 +133,33 @@ func (m *Manager) AddResolvedIPs(ctx context.Context, ips []ResolvedIP) error {
126133 return err
127134}
128135
129- func buildRuleset (p * policy.NetworkPolicy , opts Options ) string {
136+ func buildRuleset (p * policy.NetworkPolicy , opts Options ) ( string , error ) {
130137 allowV4 , allowV6 , denyV4 , denyV6 := p .StaticIPSets ()
138+ var err error
139+ if allowV4 , err = normalizeNFTIntervalSet (allowV4 ); err != nil {
140+ return "" , err
141+ }
142+ if allowV6 , err = normalizeNFTIntervalSet (allowV6 ); err != nil {
143+ return "" , err
144+ }
145+ if denyV4 , err = normalizeNFTIntervalSet (denyV4 ); err != nil {
146+ return "" , err
147+ }
148+ if denyV6 , err = normalizeNFTIntervalSet (denyV6 ); err != nil {
149+ return "" , err
150+ }
151+ dohBlockV4 := opts .DoHBlocklistV4
152+ dohBlockV6 := opts .DoHBlocklistV6
153+ if len (dohBlockV4 ) > 0 {
154+ if dohBlockV4 , err = normalizeNFTIntervalSet (dohBlockV4 ); err != nil {
155+ return "" , err
156+ }
157+ }
158+ if len (dohBlockV6 ) > 0 {
159+ if dohBlockV6 , err = normalizeNFTIntervalSet (dohBlockV6 ); err != nil {
160+ return "" , err
161+ }
162+ }
131163
132164 var b strings.Builder
133165 // Reset and re-create table, sets, and chain.
@@ -141,19 +173,19 @@ func buildRuleset(p *policy.NetworkPolicy, opts Options) string {
141173 fmt .Fprintf (& b , "add set inet %s %s { type ipv4_addr; timeout %ds; }\n " , tableName , dynAllowV4Set , dynSetTimeoutS )
142174 fmt .Fprintf (& b , "add set inet %s %s { type ipv6_addr; timeout %ds; }\n " , tableName , dynAllowV6Set , dynSetTimeoutS )
143175
144- if len (opts . DoHBlocklistV4 ) > 0 {
176+ if len (dohBlockV4 ) > 0 {
145177 fmt .Fprintf (& b , "add set inet %s %s { type ipv4_addr; flags interval; }\n " , tableName , dohBlockV4Set )
146178 }
147- if len (opts . DoHBlocklistV6 ) > 0 {
179+ if len (dohBlockV6 ) > 0 {
148180 fmt .Fprintf (& b , "add set inet %s %s { type ipv6_addr; flags interval; }\n " , tableName , dohBlockV6Set )
149181 }
150182
151183 writeElements (& b , allowV4Set , allowV4 )
152184 writeElements (& b , denyV4Set , denyV4 )
153185 writeElements (& b , allowV6Set , allowV6 )
154186 writeElements (& b , denyV6Set , denyV6 )
155- writeElements (& b , dohBlockV4Set , opts . DoHBlocklistV4 )
156- writeElements (& b , dohBlockV6Set , opts . DoHBlocklistV6 )
187+ writeElements (& b , dohBlockV4Set , dohBlockV4 )
188+ writeElements (& b , dohBlockV6Set , dohBlockV6 )
157189
158190 chainPolicy := "drop"
159191 if p .DefaultAction == policy .ActionAllow {
@@ -168,14 +200,14 @@ func buildRuleset(p *policy.NetworkPolicy, opts Options) string {
168200 fmt .Fprintf (& b , "add rule inet %s %s udp dport 853 drop\n " , tableName , chainName )
169201 }
170202 if opts .BlockDoH443 {
171- if len (opts . DoHBlocklistV4 ) == 0 && len (opts . DoHBlocklistV6 ) == 0 {
203+ if len (dohBlockV4 ) == 0 && len (dohBlockV6 ) == 0 {
172204 // strict: drop all 443 when enabled but no blocklist provided
173205 fmt .Fprintf (& b , "add rule inet %s %s tcp dport 443 drop\n " , tableName , chainName )
174206 } else {
175- if len (opts . DoHBlocklistV4 ) > 0 {
207+ if len (dohBlockV4 ) > 0 {
176208 fmt .Fprintf (& b , "add rule inet %s %s ip daddr @%s tcp dport 443 drop\n " , tableName , chainName , dohBlockV4Set )
177209 }
178- if len (opts . DoHBlocklistV6 ) > 0 {
210+ if len (dohBlockV6 ) > 0 {
179211 fmt .Fprintf (& b , "add rule inet %s %s ip6 daddr @%s tcp dport 443 drop\n " , tableName , chainName , dohBlockV6Set )
180212 }
181213 }
@@ -190,7 +222,7 @@ func buildRuleset(p *policy.NetworkPolicy, opts Options) string {
190222 fmt .Fprintf (& b , "add rule inet %s %s counter drop\n " , tableName , chainName )
191223 }
192224
193- return b .String ()
225+ return b .String (), nil
194226}
195227
196228func writeElements (b * strings.Builder , setName string , elems []string ) {
0 commit comments