144 lines
5.0 KiB
Go
144 lines
5.0 KiB
Go
package firewall
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"strconv"
|
||
"strings"
|
||
)
|
||
|
||
func (m *manager) openLinux(ctx context.Context, port int) (Result, error) {
|
||
ufw, ufwErr := m.runner.LookPath("ufw")
|
||
if ufwErr == nil {
|
||
status, err := m.runner.Run(ctx, ufw, "status")
|
||
if err != nil || status.ExitCode != 0 {
|
||
return Result{}, commandError("检查 UFW 状态", ufw, status, err)
|
||
}
|
||
if ufwIsActive(status.Output) {
|
||
return m.openUFW(ctx, ufw, port, status.Output)
|
||
}
|
||
}
|
||
|
||
firewallCMD, firewalldErr := m.runner.LookPath("firewall-cmd")
|
||
if firewalldErr == nil {
|
||
state, err := m.runner.Run(ctx, firewallCMD, "--state")
|
||
if err != nil {
|
||
return Result{}, commandError("检查 firewalld 状态", firewallCMD, state, err)
|
||
}
|
||
if state.ExitCode == 0 && strings.TrimSpace(strings.ToLower(string(state.Output))) == "running" {
|
||
return m.openFirewalld(ctx, firewallCMD, port)
|
||
}
|
||
if state.ExitCode != 0 && state.ExitCode != 252 && !strings.Contains(strings.ToLower(string(state.Output)), "not running") {
|
||
return Result{}, commandError("检查 firewalld 状态", firewallCMD, state, nil)
|
||
}
|
||
}
|
||
|
||
return Result{
|
||
Backend: BackendNone, Port: port,
|
||
Message: "未检测到已启用的 UFW 或 firewalld,无需创建主服务端口入站规则",
|
||
}, nil
|
||
}
|
||
|
||
func (m *manager) openUFW(ctx context.Context, executable string, port int, statusOutput []byte) (Result, error) {
|
||
name := ruleName(port)
|
||
if ufwHasRule(statusOutput, port) {
|
||
return Result{
|
||
Backend: backendUFW, RuleName: name, Port: port,
|
||
Message: fmt.Sprintf("UFW 已放行主服务 TCP 端口 %d", port),
|
||
}, nil
|
||
}
|
||
rule := strconv.Itoa(port) + "/tcp"
|
||
added, err := m.runner.Run(ctx, executable, "allow", rule, "comment", name)
|
||
if err != nil || added.ExitCode != 0 {
|
||
return Result{}, commandError("创建 UFW 入站规则", executable, added, err)
|
||
}
|
||
return Result{
|
||
Backend: backendUFW, RuleName: name, Port: port, Created: true,
|
||
Message: fmt.Sprintf("已通过 UFW 放行主服务 TCP 端口 %d", port),
|
||
}, nil
|
||
}
|
||
|
||
func (m *manager) rollbackUFW(ctx context.Context, result Result) error {
|
||
executable, err := m.runner.LookPath("ufw")
|
||
if err != nil {
|
||
return fmt.Errorf("未找到 UFW,无法回滚防火墙规则:%w", err)
|
||
}
|
||
rule := strconv.Itoa(result.Port) + "/tcp"
|
||
removed, err := m.runner.Run(ctx, executable, "--force", "delete", "allow", rule)
|
||
if err != nil || removed.ExitCode != 0 {
|
||
return commandError("回滚 UFW 入站规则", executable, removed, err)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (m *manager) openFirewalld(ctx context.Context, executable string, port int) (Result, error) {
|
||
name := ruleName(port)
|
||
rule := strconv.Itoa(port) + "/tcp"
|
||
query, err := m.runner.Run(ctx, executable, "--permanent", "--query-port="+rule)
|
||
if err != nil {
|
||
return Result{}, commandError("检查 firewalld 规则", executable, query, err)
|
||
}
|
||
if query.ExitCode == 0 {
|
||
return Result{
|
||
Backend: backendFirewalld, RuleName: name, Port: port,
|
||
Message: fmt.Sprintf("firewalld 已放行主服务 TCP 端口 %d", port),
|
||
}, nil
|
||
}
|
||
if query.ExitCode != 1 {
|
||
return Result{}, commandError("检查 firewalld 规则", executable, query, nil)
|
||
}
|
||
added, err := m.runner.Run(ctx, executable, "--permanent", "--add-port="+rule)
|
||
if err != nil || added.ExitCode != 0 {
|
||
return Result{}, commandError("创建 firewalld 永久规则", executable, added, err)
|
||
}
|
||
reloaded, err := m.runner.Run(ctx, executable, "--reload")
|
||
if err != nil || reloaded.ExitCode != 0 {
|
||
rollback, rollbackErr := m.runner.Run(ctx, executable, "--permanent", "--remove-port="+rule)
|
||
if rollbackErr == nil && rollback.ExitCode == 0 {
|
||
_, _ = m.runner.Run(ctx, executable, "--reload")
|
||
}
|
||
return Result{}, commandError("重新加载 firewalld", executable, reloaded, err)
|
||
}
|
||
return Result{
|
||
Backend: backendFirewalld, RuleName: name, Port: port, Created: true,
|
||
Message: fmt.Sprintf("已通过 firewalld 放行主服务 TCP 端口 %d", port),
|
||
}, nil
|
||
}
|
||
|
||
func (m *manager) rollbackFirewalld(ctx context.Context, result Result) error {
|
||
executable, err := m.runner.LookPath("firewall-cmd")
|
||
if err != nil {
|
||
return fmt.Errorf("未找到 firewalld,无法回滚防火墙规则:%w", err)
|
||
}
|
||
rule := strconv.Itoa(result.Port) + "/tcp"
|
||
removed, err := m.runner.Run(ctx, executable, "--permanent", "--remove-port="+rule)
|
||
if err != nil || removed.ExitCode != 0 {
|
||
return commandError("回滚 firewalld 永久规则", executable, removed, err)
|
||
}
|
||
reloaded, err := m.runner.Run(ctx, executable, "--reload")
|
||
if err != nil || reloaded.ExitCode != 0 {
|
||
return commandError("重新加载 firewalld", executable, reloaded, err)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func ufwIsActive(output []byte) bool {
|
||
for _, line := range strings.Split(strings.ToLower(string(output)), "\n") {
|
||
if strings.TrimSpace(line) == "status: active" {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
func ufwHasRule(output []byte, port int) bool {
|
||
prefix := strconv.Itoa(port) + "/tcp"
|
||
for _, line := range strings.Split(strings.ToLower(string(output)), "\n") {
|
||
line = strings.TrimSpace(line)
|
||
if strings.HasPrefix(line, prefix) && strings.Contains(line, "allow") {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|