@@ -46,9 +46,10 @@ type IPTablesManager struct {
4646 mainChainName string
4747 defaultAction string
4848 dryRun bool
49+ logDrops bool
4950}
5051
51- func NewIPTablesManager (mainChainName , defaultAction string , dryRun bool ) (Manager , error ) {
52+ func NewIPTablesManager (mainChainName , defaultAction string , dryRun , logDrops bool ) (Manager , error ) {
5253 ipt , err := newIPTables ()
5354 if err != nil {
5455 return nil , fmt .Errorf ("failed to initialize iptables: %v" , err )
@@ -67,6 +68,7 @@ func NewIPTablesManager(mainChainName, defaultAction string, dryRun bool) (Manag
6768 mainChainName : mainChainName ,
6869 defaultAction : defaultAction ,
6970 dryRun : dryRun ,
71+ logDrops : logDrops ,
7072 }, nil
7173}
7274
@@ -107,7 +109,7 @@ func (m *IPTablesManager) CreateContainerChain(containerChain string) error {
107109 // In dry-run mode, add a logging rule for anything that reaches the default action
108110 logRuleSpec := []string {
109111 "-j" , "LOG" ,
110- "--log-prefix" , fmt .Sprintf ("[CNI-OUTBOUND-DEFAULT -%s] " , m .defaultAction ),
112+ "--log-prefix" , fmt .Sprintf (` "[CNI-OUTBOUND-%s -%s]"` , containerChain , m .defaultAction ),
111113 }
112114 if err := m .ipt .Append ("filter" , containerChain , logRuleSpec ... ); err != nil {
113115 return fmt .Errorf ("failed to add default action logging rule: %v" , err )
@@ -127,36 +129,46 @@ func (m *IPTablesManager) CreateContainerChain(containerChain string) error {
127129 return nil
128130}
129131
130- func (m * IPTablesManager ) AddRule (chainName string , rule OutboundRule ) error {
131- // Build basic rule specification
132- ruleSpec := []string {"-d" , rule .Host , "-p" , rule .Proto , "--dport" , rule .Port }
132+ // buildRuleSpecs returns one or more rules to insert into the chain.
133+ // Each returned element is a slice of strings representing the iptables arguments.
134+ func (m * IPTablesManager ) buildRuleSpecs (chainName , host , proto , port , action string ) [][]string {
135+ // Base rule spec
136+ baseSpec := []string {"-d" , host , "-p" , proto , "--dport" , port }
133137
138+ // If dry-run => Always log + then ACCEPT
134139 if m .dryRun {
135- // Add logging rule with prefix based on original action
136- logRuleSpec := append ([]string {}, ruleSpec ... )
137- var logPrefix string
138- if rule .Action == "DROP" {
139- logPrefix = "[CNI-OUTBOUND-BLOCKED]"
140- } else {
141- logPrefix = "[CNI-OUTBOUND-ACCEPTED]"
140+ return [][]string {
141+ append (append ([]string {}, baseSpec ... ), "-j" , "LOG" , "--log-prefix" , fmt .Sprintf (`"[CNI-OUTBOUND-%s-ACCEPTED]"` , chainName )),
142+ append (append ([]string {}, baseSpec ... ), "-j" , "ACCEPT" ),
142143 }
144+ }
143145
144- logRuleSpec = append (logRuleSpec ,
145- "-j" , "LOG" ,
146- "--log-prefix" , logPrefix )
147-
148- if err := m .ipt .Insert ("filter" , chainName , 1 , logRuleSpec ... ); err != nil {
149- return fmt .Errorf ("failed to add logging rule: %v" , err )
146+ // Normal mode. If this is a drop and logDrops == true => log + then drop
147+ if m .logDrops && strings .EqualFold (action , "DROP" ) {
148+ return [][]string {
149+ append (append ([]string {}, baseSpec ... ), "-j" , "LOG" , "--log-prefix" , fmt .Sprintf (`"[CNI-OUTBOUND-%s-BLOCKED]"` , chainName )),
150+ append (append ([]string {}, baseSpec ... ), "-j" , "DROP" ),
150151 }
152+ }
153+
154+ // Otherwise, just a single final rule: -j <action>
155+ return [][]string {
156+ append (append ([]string {}, baseSpec ... ), "-j" , action ),
157+ }
158+ }
151159
152- // In dry-run mode, always ACCEPT after logging
153- ruleSpec = append (ruleSpec , "-j" , "ACCEPT" )
154- } else {
155- // Normal mode - use the specified action
156- ruleSpec = append (ruleSpec , "-j" , rule .Action )
160+ func (m * IPTablesManager ) AddRule (chainName string , rule OutboundRule ) error {
161+ ruleSpecs := m .buildRuleSpecs (chainName , rule .Host , rule .Proto , rule .Port , rule .Action )
162+
163+ // Add rules in reverse order so they end up in the correct order
164+ // (since we're using Insert at position 1 each time)
165+ for i := len (ruleSpecs ) - 1 ; i >= 0 ; i -- {
166+ if err := m .ipt .Insert ("filter" , chainName , 1 , ruleSpecs [i ]... ); err != nil {
167+ return fmt .Errorf ("failed to add rule: %v" , err )
168+ }
157169 }
158170
159- return m . ipt . Insert ( "filter" , chainName , 1 , ruleSpec ... )
171+ return nil
160172}
161173
162174func (m * IPTablesManager ) AddJumpRule (sourceIP , targetChain string ) error {
@@ -185,63 +197,51 @@ func (m *IPTablesManager) ChainExists(chainName string) (bool, error) {
185197 return m .ipt .ChainExists ("filter" , chainName )
186198}
187199
200+ // buildExpectedRuleLines constructs the strings we'll search for in `iptables -S <chain>` output.
201+ func (m * IPTablesManager ) buildExpectedRuleLines (chainName string , host , proto , port , action string ) []string {
202+ var lines []string
203+ ruleSets := m .buildRuleSpecs (chainName , host , proto , port , action )
204+
205+ // iptables -S lines typically look like:
206+ // -A <chainName> -d <host> -p <proto> --dport <port> -j <ACTION> ...
207+ // We'll create lines that we can search with strings.Contains().
208+ for _ , rs := range ruleSets {
209+ // Start with `-A chainName` then the rest:
210+ line := "-A " + chainName + " " + strings .Join (rs , " " )
211+ lines = append (lines , line )
212+ }
213+ return lines
214+ }
215+
188216func (m * IPTablesManager ) VerifyRules (chainName string , rules []OutboundRule ) error {
189217 existingRules , err := m .ipt .List ("filter" , chainName )
190218 if err != nil {
191219 return err
192220 }
193221
222+ // Verify each OutboundRule
194223 for _ , rule := range rules {
195- ruleSpec := fmt .Sprintf ("-A %s -d %s -p %s --dport %s" , chainName , rule .Host , rule .Proto , rule .Port )
196-
197- if m .dryRun {
198- // Check for logging rule
199- logRuleSpec := ruleSpec + " -j LOG"
200- found := false
201- for _ , existingRule := range existingRules {
202- if strings .Contains (existingRule , logRuleSpec ) {
203- found = true
204- break
205- }
206- }
207- if ! found {
208- return fmt .Errorf ("logging rule not found: %s" , logRuleSpec )
209- }
210-
211- // Check for ACCEPT rule
212- acceptRuleSpec := ruleSpec + " -j ACCEPT"
213- found = false
214- for _ , existingRule := range existingRules {
215- if strings .Contains (existingRule , acceptRuleSpec ) {
216- found = true
217- break
218- }
219- }
220- if ! found {
221- return fmt .Errorf ("ACCEPT rule not found: %s" , acceptRuleSpec )
222- }
223- } else {
224- // Original rule verification
225- ruleSpec = ruleSpec + fmt .Sprintf (" -j %s" , rule .Action )
224+ expectedLines := m .buildExpectedRuleLines (chainName , rule .Host , rule .Proto , rule .Port , rule .Action )
225+ for _ , expectedLine := range expectedLines {
226226 found := false
227227 for _ , existingRule := range existingRules {
228- if strings .Contains (existingRule , ruleSpec ) {
228+ if strings .Contains (existingRule , expectedLine ) {
229229 found = true
230230 break
231231 }
232232 }
233233 if ! found {
234- return fmt .Errorf ("rule not found: %s" , ruleSpec )
234+ return fmt .Errorf ("rule not found: %s" , expectedLine )
235235 }
236236 }
237237 }
238238
239239 // Verify default action logging rule in dry-run mode
240240 if m .dryRun {
241- logPrefix := fmt .Sprintf ("[CNI-OUTBOUND-DEFAULT-%s]" , m .defaultAction )
241+ defaultLogLine := fmt .Sprintf ("-A %s -j LOG --log-prefix [CNI-OUTBOUND-DEFAULT-%s]" , chainName , m .defaultAction )
242242 found := false
243243 for _ , existingRule := range existingRules {
244- if strings .Contains (existingRule , "-j LOG" ) && strings . Contains ( existingRule , logPrefix ) {
244+ if strings .Contains (existingRule , defaultLogLine ) {
245245 found = true
246246 break
247247 }
0 commit comments