@@ -2,6 +2,7 @@ package serverfirewall
22
33import (
44 "fmt"
5+ "strings"
56
67 "github.com/UpCloudLtd/upcloud-cli/v3/internal/commands"
78 "github.com/UpCloudLtd/upcloud-cli/v3/internal/completion"
@@ -15,7 +16,13 @@ import (
1516
1617type ruleDisableCommand struct {
1718 * commands.BaseCommand
18- rulePosition int
19+ rulePosition int
20+ ruleComment string
21+ ruleDirection string
22+ ruleProtocol string
23+ ruleDestPort string
24+ ruleSrcAddress string
25+ skipConfirmation int
1926 completion.Server
2027 resolver.CachingServer
2128}
@@ -25,29 +32,58 @@ func RuleDisableCommand() commands.Command {
2532 return & ruleDisableCommand {
2633 BaseCommand : commands .New (
2734 "disable" ,
28- "Disable a specific firewall rule by changing its action to drop" ,
35+ "Disable firewall rules by changing their action to drop" ,
36+ "upctl server firewall rule disable 00038afc-d526-4148-af0e-d2f1eeaded9b --comment \" Dev ports\" " ,
37+ "upctl server firewall rule disable 00038afc-d526-4148-af0e-d2f1eeaded9b --direction in --protocol tcp --dest-port 8080" ,
38+ "upctl server firewall rule disable 00038afc-d526-4148-af0e-d2f1eeaded9b --comment \" Test\" --direction in --skip-confirmation 10" ,
2939 "upctl server firewall rule disable 00038afc-d526-4148-af0e-d2f1eeaded9b --position 5" ,
3040 ),
41+ skipConfirmation : 1 ,
3142 }
3243}
3344
3445// InitCommand implements Command.InitCommand
3546func (s * ruleDisableCommand ) InitCommand () {
47+ directions := []string {upcloud .FirewallRuleDirectionIn , upcloud .FirewallRuleDirectionOut }
48+ protocols := []string {upcloud .FirewallRuleProtocolTCP , upcloud .FirewallRuleProtocolUDP , upcloud .FirewallRuleProtocolICMP }
49+
3650 flagSet := & pflag.FlagSet {}
3751 flagSet .IntVar (& s .rulePosition , "position" , 0 , "Rule position. Available: 1-1000" )
52+ flagSet .StringVar (& s .ruleComment , "comment" , "" , "Filter by comment (partial match, case-insensitive)" )
53+ flagSet .StringVar (& s .ruleDirection , "direction" , "" , "Filter by direction. Available: " + strings .Join (directions , ", " ))
54+ flagSet .StringVar (& s .ruleProtocol , "protocol" , "" , "Filter by protocol. Available: " + strings .Join (protocols , ", " ))
55+ flagSet .StringVar (& s .ruleDestPort , "dest-port" , "" , "Filter by destination port (matches both start and end)" )
56+ flagSet .StringVar (& s .ruleSrcAddress , "src-address" , "" , "Filter by source address (partial match)" )
57+ flagSet .IntVar (& s .skipConfirmation , "skip-confirmation" , 1 , "Maximum rules to modify without confirmation (0 = always confirm)" )
3858 s .AddFlags (flagSet )
3959
40- commands .Must (s .Cobra ().MarkFlagRequired ("position" ))
60+ s .Cobra ().MarkFlagsMutuallyExclusive ("position" , "comment" )
61+ s .Cobra ().MarkFlagsMutuallyExclusive ("position" , "direction" )
62+ s .Cobra ().MarkFlagsMutuallyExclusive ("position" , "protocol" )
63+ s .Cobra ().MarkFlagsMutuallyExclusive ("position" , "dest-port" )
64+ s .Cobra ().MarkFlagsMutuallyExclusive ("position" , "src-address" )
4165 commands .Must (s .Cobra ().RegisterFlagCompletionFunc ("position" , cobra .NoFileCompletions ))
66+ commands .Must (s .Cobra ().RegisterFlagCompletionFunc ("comment" , cobra .NoFileCompletions ))
67+ commands .Must (s .Cobra ().RegisterFlagCompletionFunc ("direction" , cobra .FixedCompletions (directions , cobra .ShellCompDirectiveNoFileComp )))
68+ commands .Must (s .Cobra ().RegisterFlagCompletionFunc ("protocol" , cobra .FixedCompletions (protocols , cobra .ShellCompDirectiveNoFileComp )))
69+ commands .Must (s .Cobra ().RegisterFlagCompletionFunc ("dest-port" , cobra .NoFileCompletions ))
70+ commands .Must (s .Cobra ().RegisterFlagCompletionFunc ("src-address" , cobra .NoFileCompletions ))
71+ commands .Must (s .Cobra ().RegisterFlagCompletionFunc ("skip-confirmation" , cobra .NoFileCompletions ))
4272}
4373
4474// Execute implements commands.MultipleArgumentCommand
4575func (s * ruleDisableCommand ) Execute (exec commands.Executor , arg string ) (output.Output , error ) {
46- if s .rulePosition < 1 || s .rulePosition > 1000 {
76+ // Validation
77+ hasFilters := s .rulePosition != 0 || s .ruleComment != "" || s .ruleDirection != "" ||
78+ s .ruleProtocol != "" || s .ruleDestPort != "" || s .ruleSrcAddress != ""
79+ if ! hasFilters {
80+ return nil , fmt .Errorf ("at least one filter must be specified (--comment, --direction, --protocol, --dest-port, --src-address, or --position)" )
81+ }
82+ if s .rulePosition != 0 && (s .rulePosition < 1 || s .rulePosition > 1000 ) {
4783 return nil , fmt .Errorf ("invalid position (1-1000 allowed)" )
4884 }
4985
50- msg := fmt .Sprintf ("Disabling firewall rule %d on server %v" , s . rulePosition , arg )
86+ msg := fmt .Sprintf ("Disabling firewall rules on server %v" , arg )
5187 exec .PushProgressStarted (msg )
5288
5389 // Fetch current firewall rules
@@ -58,18 +94,95 @@ func (s *ruleDisableCommand) Execute(exec commands.Executor, arg string) (output
5894 return commands .HandleError (exec , msg , err )
5995 }
6096
61- // Find and modify the target rule
62- ruleFound := false
97+ // Find matching rules
98+ var matchedIndices [] int
6399 for i := range currentRules .FirewallRules {
64- if currentRules .FirewallRules [i ].Position == s .rulePosition {
65- currentRules .FirewallRules [i ].Action = upcloud .FirewallRuleActionDrop
66- ruleFound = true
67- break
100+ rule := & currentRules .FirewallRules [i ]
101+
102+ // Position-based filter (exact match, exclusive)
103+ if s .rulePosition != 0 {
104+ if rule .Position == s .rulePosition {
105+ matchedIndices = append (matchedIndices , i )
106+ }
107+ continue
108+ }
109+
110+ // Apply all specified filters (AND logic)
111+ match := true
112+
113+ if s .ruleComment != "" {
114+ if ! strings .Contains (strings .ToLower (rule .Comment ), strings .ToLower (s .ruleComment )) {
115+ match = false
116+ }
117+ }
118+
119+ if s .ruleDirection != "" {
120+ if ! strings .EqualFold (rule .Direction , s .ruleDirection ) {
121+ match = false
122+ }
123+ }
124+
125+ if s .ruleProtocol != "" {
126+ if ! strings .EqualFold (rule .Protocol , s .ruleProtocol ) {
127+ match = false
128+ }
129+ }
130+
131+ if s .ruleDestPort != "" {
132+ // Match if either start or end matches the specified port
133+ if rule .DestinationPortStart != s .ruleDestPort && rule .DestinationPortEnd != s .ruleDestPort {
134+ match = false
135+ }
136+ }
137+
138+ if s .ruleSrcAddress != "" {
139+ // Partial match on either start or end address
140+ addrLower := strings .ToLower (s .ruleSrcAddress )
141+ if ! strings .Contains (strings .ToLower (rule .SourceAddressStart ), addrLower ) &&
142+ ! strings .Contains (strings .ToLower (rule .SourceAddressEnd ), addrLower ) {
143+ match = false
144+ }
145+ }
146+
147+ if match {
148+ matchedIndices = append (matchedIndices , i )
149+ }
150+ }
151+
152+ if len (matchedIndices ) == 0 {
153+ if s .rulePosition != 0 {
154+ return nil , fmt .Errorf ("firewall rule at position %d not found on server %s" , s .rulePosition , arg )
155+ }
156+ return nil , fmt .Errorf ("no firewall rules matching the specified filters found on server %s" , arg )
157+ }
158+
159+ // Confirmation check
160+ if len (matchedIndices ) > s .skipConfirmation {
161+ var ruleDescriptions []string
162+ for _ , idx := range matchedIndices {
163+ rule := currentRules .FirewallRules [idx ]
164+ desc := fmt .Sprintf (" - Position %d: %s %s" , rule .Position , rule .Direction , rule .Protocol )
165+ if rule .Comment != "" {
166+ desc += fmt .Sprintf (" (comment: %q)" , rule .Comment )
167+ }
168+ ruleDescriptions = append (ruleDescriptions , desc )
169+ }
170+
171+ return nil , fmt .Errorf ("would disable %d rules (exceeds skip-confirmation=%d). Matching rules:\n %s\n \n Increase --skip-confirmation to proceed" ,
172+ len (matchedIndices ), s .skipConfirmation , strings .Join (ruleDescriptions , "\n " ))
173+ }
174+
175+ // Modify matched rules
176+ modifiedCount := 0
177+ for _ , idx := range matchedIndices {
178+ if currentRules .FirewallRules [idx ].Action != upcloud .FirewallRuleActionDrop {
179+ currentRules .FirewallRules [idx ].Action = upcloud .FirewallRuleActionDrop
180+ modifiedCount ++
68181 }
69182 }
70183
71- if ! ruleFound {
72- return nil , fmt .Errorf ("firewall rule at position %d not found on server %s " , s . rulePosition , arg )
184+ if modifiedCount == 0 {
185+ return nil , fmt .Errorf ("all %d matching rules already disabled " , len ( matchedIndices ) )
73186 }
74187
75188 // Replace entire ruleset atomically
@@ -81,6 +194,7 @@ func (s *ruleDisableCommand) Execute(exec commands.Executor, arg string) (output
81194 return commands .HandleError (exec , msg , err )
82195 }
83196
197+ msg = fmt .Sprintf ("Disabled %d firewall rule(s) on server %v" , modifiedCount , arg )
84198 exec .PushProgressSuccess (msg )
85199
86200 return output.None {}, nil
0 commit comments