128 lines
4.3 KiB
Go
128 lines
4.3 KiB
Go
package agent
|
||||
|
|
|
|||
|
|
import (
|
|||
|
|
"encoding/json"
|
|||
|
|
"errors"
|
|||
|
|
"net/http"
|
|||
|
|
"os/exec"
|
|||
|
|
"strconv"
|
|||
|
|
"strings"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
type firewallStatus struct {
|
|||
|
|
Tool string `json:"tool"`
|
|||
|
|
Active bool `json:"active"`
|
|||
|
|
PortOpen bool `json:"port_open"`
|
|||
|
|
Message string `json:"message,omitempty"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// firewallStatus 只检测主机防火墙,不修改规则;云安全组无法从 VPS 内部感知。
|
|||
|
|
func (s *Server) firewallStatus(w http.ResponseWriter, r *http.Request) {
|
|||
|
|
port := 7443
|
|||
|
|
if raw := strings.TrimSpace(r.URL.Query().Get("port")); raw != "" {
|
|||
|
|
if parsed, err := strconv.Atoi(raw); err == nil && parsed > 0 && parsed <= 65535 {
|
|||
|
|
port = parsed
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
writeJSON(w, http.StatusOK, detectFirewall(port))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// firewallAllow 仅在用户显式确认后调用;只自动处理 ufw 与 firewalld。
|
|||
|
|
func (s *Server) firewallAllow(w http.ResponseWriter, r *http.Request) {
|
|||
|
|
var req struct {
|
|||
|
|
Port int `json:"port"`
|
|||
|
|
}
|
|||
|
|
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 4<<10)).Decode(&req); err != nil {
|
|||
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
if req.Port <= 0 || req.Port > 65535 {
|
|||
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "端口无效"})
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
output, err := allowFirewallPort(req.Port)
|
|||
|
|
if err != nil {
|
|||
|
|
writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()})
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
writeJSON(w, http.StatusOK, map[string]any{"ok": true, "output": output, "status": detectFirewall(req.Port)})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func detectFirewall(port int) firewallStatus {
|
|||
|
|
if path, err := exec.LookPath("ufw"); err == nil {
|
|||
|
|
if out, err := exec.Command(path, "status").Output(); err == nil {
|
|||
|
|
active, open := parseUFWStatus(string(out), port)
|
|||
|
|
if active {
|
|||
|
|
return firewallStatus{Tool: "ufw", Active: true, PortOpen: open}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if path, err := exec.LookPath("firewall-cmd"); err == nil {
|
|||
|
|
if out, err := exec.Command(path, "--state").Output(); err == nil && strings.TrimSpace(string(out)) == "running" {
|
|||
|
|
return firewallStatus{Tool: "firewalld", Active: true, PortOpen: firewalldPortOpen(path, port)}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if _, err := exec.LookPath("nft"); err == nil {
|
|||
|
|
return firewallStatus{Tool: "nftables", Active: true, Message: "nftables 需要手工放行,Agent 不会自动修改规则"}
|
|||
|
|
}
|
|||
|
|
if _, err := exec.LookPath("iptables"); err == nil {
|
|||
|
|
return firewallStatus{Tool: "iptables", Active: true, Message: "iptables 需要手工放行,Agent 不会自动修改规则"}
|
|||
|
|
}
|
|||
|
|
return firewallStatus{Tool: "none", Message: "未检测到主机防火墙"}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// parseUFWStatus 纯解析 ufw status 输出:是否启用、目标端口是否已放行。
|
|||
|
|
func parseUFWStatus(output string, port int) (bool, bool) {
|
|||
|
|
active := false
|
|||
|
|
open := false
|
|||
|
|
needle := strconv.Itoa(port)
|
|||
|
|
for _, line := range strings.Split(output, "\n") {
|
|||
|
|
line = strings.TrimSpace(line)
|
|||
|
|
if strings.HasPrefix(line, "Status:") {
|
|||
|
|
active = strings.EqualFold(strings.TrimSpace(strings.TrimPrefix(line, "Status:")), "active")
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
if !strings.Contains(line, needle) || !strings.Contains(line, "ALLOW") {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
fields := strings.Fields(line)
|
|||
|
|
if len(fields) > 0 && (fields[0] == needle || strings.HasPrefix(fields[0], needle+"/")) {
|
|||
|
|
open = true
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return active, open
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func firewalldPortOpen(path string, port int) bool {
|
|||
|
|
out, err := exec.Command(path, "--list-ports").Output()
|
|||
|
|
if err != nil {
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
for _, item := range strings.Fields(string(out)) {
|
|||
|
|
if strings.HasPrefix(item, strconv.Itoa(port)+"/") {
|
|||
|
|
return true
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func allowFirewallPort(port int) (string, error) {
|
|||
|
|
status := detectFirewall(port)
|
|||
|
|
switch status.Tool {
|
|||
|
|
case "ufw":
|
|||
|
|
out, err := exec.Command("ufw", "allow", strconv.Itoa(port)+"/tcp").CombinedOutput()
|
|||
|
|
return strings.TrimSpace(string(out)), err
|
|||
|
|
case "firewalld":
|
|||
|
|
first, err := exec.Command("firewall-cmd", "--permanent", "--add-port="+strconv.Itoa(port)+"/tcp").CombinedOutput()
|
|||
|
|
if err != nil {
|
|||
|
|
return strings.TrimSpace(string(first)), err
|
|||
|
|
}
|
|||
|
|
second, err := exec.Command("firewall-cmd", "--reload").CombinedOutput()
|
|||
|
|
return strings.TrimSpace(string(first) + "\n" + string(second)), err
|
|||
|
|
case "nftables", "iptables":
|
|||
|
|
return "", errors.New("当前防火墙需要手工放行,Agent 不会自动修改 nftables / iptables 规则")
|
|||
|
|
default:
|
|||
|
|
return "", errors.New("未检测到可自动放行的主机防火墙(仅支持 ufw / firewalld)")
|
|||
|
|
}
|
|||
|
|
}
|