From e9cbc8e9452d6df91836d9968b63bcc965fb7ebe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=97=E5=AD=90?= Date: Fri, 18 Oct 2024 02:08:55 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84=E9=98=B2?= =?UTF-8?q?=E7=81=AB=E5=A2=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/data/safe.go | 2 +- internal/http/request/firewall.go | 15 + internal/route/http.go | 6 + internal/service/cli.go | 2 +- internal/service/firewall.go | 148 +++++++++- pkg/firewall/consts.go | 17 +- pkg/firewall/firewall.go | 68 +++-- pkg/os/os.go | 22 ++ pkg/shell/exec.go | 6 +- web/src/api/panel/firewall/index.ts | 36 +++ web/src/api/panel/safe/index.ts | 14 - web/src/views/safe/CreateForwardModal.vue | 95 ++++++ web/src/views/safe/CreateIpModal.vue | 132 +++++++++ web/src/views/safe/CreateModal.vue | 4 +- web/src/views/safe/ForwardView.vue | 230 +++++++++++++++ web/src/views/safe/IndexView.vue | 341 ++-------------------- web/src/views/safe/IpRuleView.vue | 264 +++++++++++++++++ web/src/views/safe/RuleView.vue | 303 +++++++++++++++++++ web/src/views/safe/SettingView.vue | 67 +++++ 19 files changed, 1399 insertions(+), 373 deletions(-) create mode 100644 web/src/api/panel/firewall/index.ts create mode 100644 web/src/views/safe/CreateForwardModal.vue create mode 100644 web/src/views/safe/CreateIpModal.vue create mode 100644 web/src/views/safe/ForwardView.vue create mode 100644 web/src/views/safe/IpRuleView.vue create mode 100644 web/src/views/safe/RuleView.vue create mode 100644 web/src/views/safe/SettingView.vue diff --git a/internal/data/safe.go b/internal/data/safe.go index 608bb0367..c2953acab 100644 --- a/internal/data/safe.go +++ b/internal/data/safe.go @@ -60,7 +60,7 @@ func (r *safeRepo) UpdateSSH(port uint, status bool) error { } func (r *safeRepo) GetPingStatus() (bool, error) { - out, err := shell.Execf(`firewall-cmd --list-all`) + out, err := shell.Execf(`firewall-cmd --list-rich-rules`) if err != nil { return true, errors.New(out) } diff --git a/internal/http/request/firewall.go b/internal/http/request/firewall.go index a183179e0..93bc84656 100644 --- a/internal/http/request/firewall.go +++ b/internal/http/request/firewall.go @@ -13,3 +13,18 @@ type FirewallRule struct { Strategy string `json:"strategy" validate:"required,oneof=accept drop reject"` Direction string `json:"direction"` } + +type FirewallIPRule struct { + Family string `json:"family" validate:"required,oneof=ipv4 ipv6"` + Protocol string `json:"protocol" validate:"min=1,oneof=tcp udp tcp/udp"` + Address string `json:"address"` + Strategy string `json:"strategy" validate:"required,oneof=accept drop reject"` + Direction string `json:"direction"` +} + +type FirewallForward struct { + Protocol string `json:"protocol" validate:"min=1,oneof=tcp udp tcp/udp"` + Port uint `json:"port" validate:"required,gte=1,lte=65535"` + TargetIP string `json:"target_ip" validate:"required"` + TargetPort uint `json:"target_port" validate:"required,gte=1,lte=65535"` +} diff --git a/internal/route/http.go b/internal/route/http.go index 5dfcaca5e..f7666ec22 100644 --- a/internal/route/http.go +++ b/internal/route/http.go @@ -148,6 +148,12 @@ func Http(r chi.Router) { r.Get("/rule", firewall.GetRules) r.Post("/rule", firewall.CreateRule) r.Delete("/rule", firewall.DeleteRule) + r.Get("/ipRule", firewall.GetIPRules) + r.Post("/ipRule", firewall.CreateIPRule) + r.Delete("/ipRule", firewall.DeleteIPRule) + r.Get("/forward", firewall.GetForwards) + r.Post("/forward", firewall.CreateForward) + r.Delete("/forward", firewall.DeleteForward) }) r.Route("/ssh", func(r chi.Router) { diff --git a/internal/service/cli.go b/internal/service/cli.go index 36a8ec0e5..71dd1b6a4 100644 --- a/internal/service/cli.go +++ b/internal/service/cli.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "github.com/TheTNB/panel/pkg/ntp" "path/filepath" "time" @@ -21,6 +20,7 @@ import ( "github.com/TheTNB/panel/internal/http/request" "github.com/TheTNB/panel/pkg/api" "github.com/TheTNB/panel/pkg/io" + "github.com/TheTNB/panel/pkg/ntp" "github.com/TheTNB/panel/pkg/str" "github.com/TheTNB/panel/pkg/systemctl" "github.com/TheTNB/panel/pkg/tools" diff --git a/internal/service/firewall.go b/internal/service/firewall.go index 4562dcabb..c1a857b21 100644 --- a/internal/service/firewall.go +++ b/internal/service/firewall.go @@ -2,11 +2,13 @@ package service import ( "net/http" + "slices" "github.com/go-rat/chix" "github.com/TheTNB/panel/internal/http/request" "github.com/TheTNB/panel/pkg/firewall" + "github.com/TheTNB/panel/pkg/os" "github.com/TheTNB/panel/pkg/systemctl" ) @@ -64,7 +66,38 @@ func (s *FirewallService) GetRules(w http.ResponseWriter, r *http.Request) { return } - paged, total := Paginate(r, rules) + var filledRules []map[string]any + for rule := range slices.Values(rules) { + // 去除IP规则 + if rule.PortStart == 1 && rule.PortEnd == 65535 { + continue + } + isUse := false + for port := rule.PortStart; port <= rule.PortEnd; port++ { + if rule.Protocol == firewall.ProtocolTCP { + isUse = os.TCPPortInUse(port) + } else if rule.Protocol == firewall.ProtocolUDP { + isUse = os.UDPPortInUse(port) + } else { + isUse = os.TCPPortInUse(port) || os.UDPPortInUse(port) + } + if isUse { + break + } + } + filledRules = append(filledRules, map[string]any{ + "family": rule.Family, + "port_start": rule.PortStart, + "port_end": rule.PortEnd, + "protocol": rule.Protocol, + "address": rule.Address, + "strategy": rule.Strategy, + "direction": rule.Direction, + "in_use": isUse, + }) + } + + paged, total := Paginate(r, filledRules) Success(w, chix.M{ "total": total, @@ -105,3 +138,116 @@ func (s *FirewallService) DeleteRule(w http.ResponseWriter, r *http.Request) { Success(w, nil) } + +func (s *FirewallService) GetIPRules(w http.ResponseWriter, r *http.Request) { + rules, err := s.firewall.ListRule() + if err != nil { + Error(w, http.StatusInternalServerError, "%v", err) + return + } + + var filledRules []map[string]any + for rule := range slices.Values(rules) { + // 保留IP规则 + if rule.PortStart != 1 || rule.PortEnd != 65535 || rule.Address == "" { + continue + } + filledRules = append(filledRules, map[string]any{ + "family": rule.Family, + "protocol": rule.Protocol, + "address": rule.Address, + "strategy": rule.Strategy, + "direction": rule.Direction, + }) + } + + paged, total := Paginate(r, filledRules) + + Success(w, chix.M{ + "total": total, + "items": paged, + }) +} + +func (s *FirewallService) CreateIPRule(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FirewallIPRule](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, "%v", err) + return + } + + if err = s.firewall.RichRules(firewall.FireInfo{ + Family: req.Family, Address: req.Address, Protocol: firewall.Protocol(req.Protocol), Strategy: firewall.Strategy(req.Strategy), Direction: firewall.Direction(req.Direction), + }, firewall.OperationAdd); err != nil { + Error(w, http.StatusInternalServerError, "%v", err) + return + } + + Success(w, nil) +} + +func (s *FirewallService) DeleteIPRule(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FirewallIPRule](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, "%v", err) + return + } + + if err = s.firewall.RichRules(firewall.FireInfo{ + Family: req.Family, Address: req.Address, Protocol: firewall.Protocol(req.Protocol), Strategy: firewall.Strategy(req.Strategy), Direction: firewall.Direction(req.Direction), + }, firewall.OperationRemove); err != nil { + Error(w, http.StatusInternalServerError, "%v", err) + return + } + + Success(w, nil) +} + +func (s *FirewallService) GetForwards(w http.ResponseWriter, r *http.Request) { + forwards, err := s.firewall.ListForward() + if err != nil { + Error(w, http.StatusInternalServerError, "%v", err) + return + } + + paged, total := Paginate(r, forwards) + + Success(w, chix.M{ + "total": total, + "items": paged, + }) +} + +func (s *FirewallService) CreateForward(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FirewallForward](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, "%v", err) + return + } + + if err = s.firewall.Forward(firewall.Forward{ + Protocol: firewall.Protocol(req.Protocol), Port: req.Port, TargetIP: req.TargetIP, TargetPort: req.TargetPort, + }, firewall.OperationAdd); err != nil { + Error(w, http.StatusInternalServerError, "%v", err) + return + } + + Success(w, nil) +} + +func (s *FirewallService) DeleteForward(w http.ResponseWriter, r *http.Request) { + req, err := Bind[request.FirewallForward](r) + if err != nil { + Error(w, http.StatusUnprocessableEntity, "%v", err) + return + } + + if err = s.firewall.Forward(firewall.Forward{ + Protocol: firewall.Protocol(req.Protocol), Port: req.Port, TargetIP: req.TargetIP, TargetPort: req.TargetPort, + }, firewall.OperationRemove); err != nil { + Error(w, http.StatusInternalServerError, "%v", err) + return + } + + Success(w, nil) +} diff --git a/pkg/firewall/consts.go b/pkg/firewall/consts.go index 13b82a17f..2e74f2c6a 100644 --- a/pkg/firewall/consts.go +++ b/pkg/firewall/consts.go @@ -41,16 +41,15 @@ type FireInfo struct { } type FireForwardInfo struct { - Address string `json:"address"` // 源地址 - Port uint `json:"port"` // 1-65535 - Protocol Protocol `json:"protocol"` // tcp udp tcp/udp - TargetIP string `json:"targetIP"` // 目标地址 - TargetPort string `json:"targetPort"` // 1-65535 + Port uint `json:"port"` // 1-65535 + Protocol Protocol `json:"protocol"` // tcp udp tcp/udp + TargetIP string `json:"target_ip"` // 目标地址 + TargetPort uint `json:"target_port"` // 1-65535 } type Forward struct { - Protocol Protocol `json:"protocol"` // tcp udp tcp/udp - Port uint `json:"port"` // 1-65535 - TargetIP string `json:"targetIP"` // 目标地址 - TargetPort uint `json:"targetPort"` // 1-65535 + Protocol Protocol `json:"protocol"` // tcp udp tcp/udp + Port uint `json:"port"` // 1-65535 + TargetIP string `json:"target_ip"` // 目标地址 + TargetPort uint `json:"target_port"` // 1-65535 } diff --git a/pkg/firewall/firewall.go b/pkg/firewall/firewall.go index 7be1defa8..6a5a5d7a0 100644 --- a/pkg/firewall/firewall.go +++ b/pkg/firewall/firewall.go @@ -3,6 +3,7 @@ package firewall import ( "errors" "fmt" + "net" "regexp" "slices" "strings" @@ -22,7 +23,7 @@ type Firewall struct { func NewFirewall() *Firewall { firewall := &Firewall{ forwardListRegex: regexp.MustCompile(`^port=(\d{1,5}):proto=(.+?):toport=(\d{1,5}):toaddr=(.*)$`), - richRuleRegex: regexp.MustCompile(`^rule family="([^"]+)"(?: .*?(source|destination) address="([^"]+)")?(?: .*?port port="([^"]+)")?(?: .*?protocol="([^"]+)")?.*?(accept|drop|reject)$`), + richRuleRegex: regexp.MustCompile(`^rule family="([^"]+)"(?: .*?(source|destination) address="([^"]+)")?(?: .*?port port="([^"]+)")?(?: .*?protocol(?: value)?="([^"]+)")?.*?(accept|drop|reject)$`), } return firewall @@ -107,7 +108,7 @@ func (r *Firewall) ListForward() ([]FireForwardInfo, error) { Port: cast.ToUint(match[1]), Protocol: Protocol(match[2]), TargetIP: match[4], - TargetPort: match[3], + TargetPort: cast.ToUint(match[3]), }) } } @@ -182,10 +183,18 @@ func (r *Firewall) RichRules(rule FireInfo, operation Operation) error { ruleBuilder.WriteString(fmt.Sprintf(`port port="%d-%d" `, rule.PortStart, rule.PortEnd)) } if operation == OperationRemove && protocol != "" && rule.Protocol != "tcp/udp" { // 删除操作,可以不指定协议 - ruleBuilder.WriteString(fmt.Sprintf(`protocol="%s" `, protocol)) + ruleBuilder.WriteString(`protocol`) + if rule.PortStart == 0 && rule.PortEnd == 0 { // IP 规则下,必须添加 value + ruleBuilder.WriteString(` value`) + } + ruleBuilder.WriteString(fmt.Sprintf(`="%s" `, protocol)) } if operation == OperationAdd && protocol != "" { - ruleBuilder.WriteString(fmt.Sprintf(`protocol="%s" `, protocol)) + ruleBuilder.WriteString(`protocol`) + if rule.PortStart == 0 && rule.PortEnd == 0 { // IP 规则下,必须添加 value + ruleBuilder.WriteString(` value`) + } + ruleBuilder.WriteString(fmt.Sprintf(`="%s" `, protocol)) } ruleBuilder.WriteString(string(rule.Strategy)) @@ -199,26 +208,29 @@ func (r *Firewall) RichRules(rule FireInfo, operation Operation) error { return err } -func (r *Firewall) PortForward(info Forward, operation Operation) error { +func (r *Firewall) Forward(rule Forward, operation Operation) error { if err := r.enableForward(); err != nil { return err } - var ruleStr strings.Builder - ruleStr.WriteString(fmt.Sprintf("firewall-cmd --zone=public --%s-forward-port=port=%d:proto=%s:", operation, info.Port, info.Protocol)) - if info.TargetIP != "" && info.TargetIP != "127.0.0.1" && info.TargetIP != "localhost" { - ruleStr.WriteString(fmt.Sprintf("toaddr=%s:toport=%d", info.TargetIP, info.TargetPort)) - } else { - ruleStr.WriteString(fmt.Sprintf("toport=%d", info.TargetPort)) - } - ruleStr.WriteString(" --permanent") + protocols := strings.Split(string(rule.Protocol), "/") + for protocol := range slices.Values(protocols) { + var ruleBuilder strings.Builder + ruleBuilder.WriteString(fmt.Sprintf("firewall-cmd --zone=public --%s-forward-port=port=%d:proto=%s:", operation, rule.Port, protocol)) + if rule.TargetIP != "" && !r.isLocalAddress(rule.TargetIP) { + ruleBuilder.WriteString(fmt.Sprintf("toport=%d:toaddr=%s", rule.TargetPort, rule.TargetIP)) + } else { + ruleBuilder.WriteString(fmt.Sprintf("toport=%d", rule.TargetPort)) + } + ruleBuilder.WriteString(" --permanent") - _, err := shell.Execf(ruleStr.String()) // nolint: govet - if err != nil { - return fmt.Errorf("%s port forward failed, err: %v", operation, err) + _, err := shell.Execf(ruleBuilder.String()) // nolint: govet + if err != nil { + return fmt.Errorf("%s port forward failed, err: %v", operation, err) + } } - _, err = shell.Execf("firewall-cmd --reload") + _, err := shell.Execf("firewall-cmd --reload") return err } @@ -270,15 +282,31 @@ func (r *Firewall) enableForward() error { if out == "no" { out, err = shell.Execf("firewall-cmd --zone=public --add-masquerade --permanent") if err != nil { - return fmt.Errorf("%s: %s", err, out) + return fmt.Errorf("%v: %s", err, out) } - _, err = shell.Execf("firewall-cmd --reload") return err } - return fmt.Errorf("%v: %s", err, out) } return nil } + +func (r *Firewall) isLocalAddress(ip string) bool { + parsed := net.ParseIP(ip) + if parsed == nil { + return false + } + if parsed.IsLoopback() { + return true + } + if parsed.IsUnspecified() { + return true + } + if strings.ToLower(ip) == "localhost" { + return true + } + + return false +} diff --git a/pkg/os/os.go b/pkg/os/os.go index 8b8833d50..f3a4f05b0 100644 --- a/pkg/os/os.go +++ b/pkg/os/os.go @@ -2,6 +2,8 @@ package os import ( "bufio" + "fmt" + "net" "os" "strings" ) @@ -58,3 +60,23 @@ func IsUbuntu() bool { id, idLike := osRelease["ID"], osRelease["ID_LIKE"] return id == "ubuntu" || strings.Contains(idLike, "ubuntu") } + +func TCPPortInUse(port uint) bool { + addr := fmt.Sprintf(":%d", port) + conn, err := net.Listen("tcp", addr) + if err != nil { + return true + } + defer conn.Close() + return false +} + +func UDPPortInUse(port uint) bool { + addr := fmt.Sprintf(":%d", port) + conn, err := net.ListenPacket("udp", addr) + if err != nil { + return true + } + defer conn.Close() + return false +} diff --git a/pkg/shell/exec.go b/pkg/shell/exec.go index 013d54826..e716c2379 100644 --- a/pkg/shell/exec.go +++ b/pkg/shell/exec.go @@ -22,7 +22,7 @@ func Execf(shell string, args ...any) (string, error) { err := cmd.Run() if err != nil { - return "", errors.New(strings.TrimSpace(stderr.String())) + return strings.TrimSpace(stdout.String()), errors.New(strings.TrimSpace(stderr.String())) } return strings.TrimSpace(stdout.String()), err @@ -71,10 +71,10 @@ func ExecfWithTimeout(timeout time.Duration, shell string, args ...any) (string, select { case <-time.After(timeout): _ = cmd.Process.Kill() - return "", errors.New("执行超时") + return strings.TrimSpace(stdout.String()), errors.New("执行超时") case err = <-done: if err != nil { - return "", errors.New(strings.TrimSpace(stderr.String())) + return strings.TrimSpace(stdout.String()), errors.New(strings.TrimSpace(stderr.String())) } } diff --git a/web/src/api/panel/firewall/index.ts b/web/src/api/panel/firewall/index.ts new file mode 100644 index 000000000..f0a43eb0d --- /dev/null +++ b/web/src/api/panel/firewall/index.ts @@ -0,0 +1,36 @@ +import type { AxiosResponse } from 'axios' + +import { request } from '@/utils' + +export default { + // 获取防火墙状态 + status: (): Promise> => request.get('/firewall/status'), + // 设置防火墙状态 + updateStatus: (status: boolean): Promise> => + request.post('/firewall/status', { status }), + // 获取防火墙规则 + rules: (page: number, limit: number): Promise> => + request.get('/firewall/rule', { params: { page, limit } }), + // 创建防火墙规则 + createRule: (rule: any): Promise> => request.post('/firewall/rule', rule), + // 删除防火墙规则 + deleteRule: (rule: any): Promise> => + request.delete('/firewall/rule', { data: rule }), + // 获取防火墙IP规则 + ipRules: (page: number, limit: number): Promise> => + request.get('/firewall/ipRule', { params: { page, limit } }), + // 创建防火墙IP规则 + createIpRule: (rule: any): Promise> => request.post('/firewall/ipRule', rule), + // 删除防火墙IP规则 + deleteIpRule: (rule: any): Promise> => + request.delete('/firewall/ipRule', { data: rule }), + // 获取防火墙转发规则 + forwards: (page: number, limit: number): Promise> => + request.get('/firewall/forward', { params: { page, limit } }), + // 创建防火墙转发规则 + createForward: (rule: any): Promise> => + request.post('/firewall/forward', rule), + // 删除防火墙转发规则 + deleteForward: (rule: any): Promise> => + request.delete('/firewall/forward', { data: rule }) +} diff --git a/web/src/api/panel/safe/index.ts b/web/src/api/panel/safe/index.ts index 8080e69d1..91630dfa4 100644 --- a/web/src/api/panel/safe/index.ts +++ b/web/src/api/panel/safe/index.ts @@ -3,20 +3,6 @@ import type { AxiosResponse } from 'axios' import { request } from '@/utils' export default { - // 获取防火墙状态 - firewallStatus: (): Promise> => request.get('/firewall/status'), - // 设置防火墙状态 - setFirewallStatus: (status: boolean): Promise> => - request.post('/firewall/status', { status }), - // 获取防火墙规则 - firewallRules: (page: number, limit: number): Promise> => - request.get('/firewall/rule', { params: { page, limit } }), - // 创建防火墙规则 - createFirewallRule: (rule: any): Promise> => - request.post('/firewall/rule', rule), - // 删除防火墙规则 - deleteFirewallRule: (rule: any): Promise> => - request.delete('/firewall/rule', { data: rule }), // 获取SSH ssh: (): Promise> => request.get('/safe/ssh'), // 设置SSH diff --git a/web/src/views/safe/CreateForwardModal.vue b/web/src/views/safe/CreateForwardModal.vue new file mode 100644 index 000000000..f096f9375 --- /dev/null +++ b/web/src/views/safe/CreateForwardModal.vue @@ -0,0 +1,95 @@ + + + + + diff --git a/web/src/views/safe/CreateIpModal.vue b/web/src/views/safe/CreateIpModal.vue new file mode 100644 index 000000000..b15d7a92d --- /dev/null +++ b/web/src/views/safe/CreateIpModal.vue @@ -0,0 +1,132 @@ + + + + + diff --git a/web/src/views/safe/CreateModal.vue b/web/src/views/safe/CreateModal.vue index 0b34d6654..a496709d1 100644 --- a/web/src/views/safe/CreateModal.vue +++ b/web/src/views/safe/CreateModal.vue @@ -1,5 +1,5 @@ + + + + diff --git a/web/src/views/safe/IndexView.vue b/web/src/views/safe/IndexView.vue index 8b81e5d71..159c355b7 100644 --- a/web/src/views/safe/IndexView.vue +++ b/web/src/views/safe/IndexView.vue @@ -1,332 +1,29 @@ diff --git a/web/src/views/safe/IpRuleView.vue b/web/src/views/safe/IpRuleView.vue new file mode 100644 index 000000000..5a69db4ed --- /dev/null +++ b/web/src/views/safe/IpRuleView.vue @@ -0,0 +1,264 @@ + + + + + diff --git a/web/src/views/safe/RuleView.vue b/web/src/views/safe/RuleView.vue new file mode 100644 index 000000000..64e000ccc --- /dev/null +++ b/web/src/views/safe/RuleView.vue @@ -0,0 +1,303 @@ + + + + + diff --git a/web/src/views/safe/SettingView.vue b/web/src/views/safe/SettingView.vue new file mode 100644 index 000000000..0e5fe30ea --- /dev/null +++ b/web/src/views/safe/SettingView.vue @@ -0,0 +1,67 @@ + + + + +