Skip to content

Commit 8c1fcdf

Browse files
committed
feat: Add shell completion for firewall rule flags
Implements intelligent shell completion for: - --dest-port: Suggests common ports (SSH, HTTP, HTTPS, databases) and parses /etc/services for well-known TCP/UDP ports - --src-address: Suggests common IP ranges (private networks, localhost, any IPv4/IPv6) - --skip-confirmation: Suggests 0 (always confirm) and 10 (batch operations) This improves UX by helping users discover valid values without consulting documentation.
1 parent 714fc0d commit 8c1fcdf

2 files changed

Lines changed: 178 additions & 3 deletions

File tree

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
package serverfirewall
2+
3+
import (
4+
"bufio"
5+
"os"
6+
"strconv"
7+
"strings"
8+
9+
"github.com/spf13/cobra"
10+
)
11+
12+
// completeCommonPorts returns completion for well-known port numbers from /etc/services
13+
func completeCommonPorts(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
14+
ports := parseServicesFile()
15+
16+
var suggestions []string
17+
for _, port := range ports {
18+
if strings.HasPrefix(port, toComplete) {
19+
suggestions = append(suggestions, port)
20+
}
21+
if len(suggestions) >= 50 { // Limit suggestions
22+
break
23+
}
24+
}
25+
26+
return suggestions, cobra.ShellCompDirectiveNoFileComp
27+
}
28+
29+
// parseServicesFile parses /etc/services to extract common port numbers
30+
func parseServicesFile() []string {
31+
var ports []string
32+
seen := make(map[string]bool)
33+
34+
// Add commonly used ports first
35+
commonPorts := []struct {
36+
port string
37+
desc string
38+
}{
39+
{"22", "SSH"},
40+
{"80", "HTTP"},
41+
{"443", "HTTPS"},
42+
{"21", "FTP"},
43+
{"25", "SMTP"},
44+
{"53", "DNS"},
45+
{"110", "POP3"},
46+
{"143", "IMAP"},
47+
{"3306", "MySQL"},
48+
{"5432", "PostgreSQL"},
49+
{"6379", "Redis"},
50+
{"8080", "HTTP-Alt"},
51+
{"8443", "HTTPS-Alt"},
52+
{"3000", "Dev-Server"},
53+
{"5000", "Dev-Server"},
54+
{"8000", "Dev-Server"},
55+
}
56+
57+
for _, cp := range commonPorts {
58+
entry := cp.port + "\t" + cp.desc
59+
ports = append(ports, entry)
60+
seen[cp.port] = true
61+
}
62+
63+
// Try to read /etc/services for additional ports
64+
file, err := os.Open("/etc/services")
65+
if err != nil {
66+
return ports // Return common ports if file can't be read
67+
}
68+
defer file.Close()
69+
70+
scanner := bufio.NewScanner(file)
71+
for scanner.Scan() {
72+
line := strings.TrimSpace(scanner.Text())
73+
74+
// Skip comments and empty lines
75+
if line == "" || strings.HasPrefix(line, "#") {
76+
continue
77+
}
78+
79+
// Parse line: service-name port/protocol [aliases] [# comment]
80+
fields := strings.Fields(line)
81+
if len(fields) < 2 {
82+
continue
83+
}
84+
85+
serviceName := fields[0]
86+
portProto := fields[1]
87+
88+
// Extract port number (before the /)
89+
parts := strings.Split(portProto, "/")
90+
if len(parts) < 2 {
91+
continue
92+
}
93+
94+
port := parts[0]
95+
protocol := parts[1]
96+
97+
// Only include TCP and UDP ports
98+
if protocol != "tcp" && protocol != "udp" {
99+
continue
100+
}
101+
102+
// Skip if we've already added this port
103+
if seen[port] {
104+
continue
105+
}
106+
107+
// Validate port number
108+
portNum, err := strconv.Atoi(port)
109+
if err != nil || portNum < 1 || portNum > 65535 {
110+
continue
111+
}
112+
113+
seen[port] = true
114+
entry := port + "\t" + serviceName + "/" + protocol
115+
ports = append(ports, entry)
116+
117+
if len(ports) >= 200 { // Limit total entries
118+
break
119+
}
120+
}
121+
122+
return ports
123+
}
124+
125+
// completeIPAddress returns completion suggestions for IP addresses
126+
func completeIPAddress(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
127+
var suggestions []string
128+
129+
// Common private network ranges
130+
commonRanges := []struct {
131+
addr string
132+
desc string
133+
}{
134+
{"10.0.0.0/8", "Private-ClassA"},
135+
{"172.16.0.0/12", "Private-ClassB"},
136+
{"192.168.0.0/16", "Private-ClassC"},
137+
{"192.168.1.0/24", "Private-Common"},
138+
{"0.0.0.0/0", "Any-IPv4"},
139+
{"::/0", "Any-IPv6"},
140+
{"127.0.0.1", "Localhost-IPv4"},
141+
{"::1", "Localhost-IPv6"},
142+
}
143+
144+
for _, range_ := range commonRanges {
145+
if strings.HasPrefix(range_.addr, toComplete) {
146+
entry := range_.addr + "\t" + range_.desc
147+
suggestions = append(suggestions, entry)
148+
}
149+
}
150+
151+
// If user is typing a partial IP, suggest completing octets
152+
if len(toComplete) > 0 && (strings.Contains(toComplete, ".") || strings.Contains(toComplete, ":")) {
153+
// Don't auto-complete partial IPs - let user type them
154+
// Just provide the common ranges
155+
}
156+
157+
return suggestions, cobra.ShellCompDirectiveNoFileComp | cobra.ShellCompDirectiveNoSpace
158+
}
159+
160+
// completeSkipConfirmation returns completion suggestions for skip-confirmation flag
161+
func completeSkipConfirmation(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
162+
suggestions := []string{
163+
"0\tAlways require confirmation",
164+
"10\tSkip confirmation for up to 10 rules",
165+
}
166+
167+
var filtered []string
168+
for _, s := range suggestions {
169+
if strings.HasPrefix(s, toComplete) {
170+
filtered = append(filtered, s)
171+
}
172+
}
173+
174+
return filtered, cobra.ShellCompDirectiveNoFileComp
175+
}

internal/commands/server/firewall/rule_modify_shared.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,9 @@ func configureRuleFilterFlagsPostAdd(cobraCmd *cobra.Command) {
5353
commands.Must(cobraCmd.RegisterFlagCompletionFunc("comment", cobra.NoFileCompletions))
5454
commands.Must(cobraCmd.RegisterFlagCompletionFunc("direction", cobra.FixedCompletions(directions, cobra.ShellCompDirectiveNoFileComp)))
5555
commands.Must(cobraCmd.RegisterFlagCompletionFunc("protocol", cobra.FixedCompletions(protocols, cobra.ShellCompDirectiveNoFileComp)))
56-
commands.Must(cobraCmd.RegisterFlagCompletionFunc("dest-port", cobra.NoFileCompletions))
57-
commands.Must(cobraCmd.RegisterFlagCompletionFunc("src-address", cobra.NoFileCompletions))
58-
commands.Must(cobraCmd.RegisterFlagCompletionFunc("skip-confirmation", cobra.NoFileCompletions))
56+
commands.Must(cobraCmd.RegisterFlagCompletionFunc("dest-port", completeCommonPorts))
57+
commands.Must(cobraCmd.RegisterFlagCompletionFunc("src-address", completeIPAddress))
58+
commands.Must(cobraCmd.RegisterFlagCompletionFunc("skip-confirmation", completeSkipConfirmation))
5959
}
6060

6161
// findMatchingRules finds rules matching the specified filters

0 commit comments

Comments
 (0)