Files
flvx/go-backend/internal/http/handler/nftables_runtime.go
T
2026-06-01 19:47:26 +08:00

366 lines
10 KiB
Go

package handler
import (
"context"
"database/sql"
"errors"
"fmt"
"net/http"
"strings"
"time"
"go-backend/internal/http/response"
runtimenft "go-backend/internal/runtime/nftables"
"go-backend/internal/store/model"
"go-backend/internal/store/repo"
"gorm.io/gorm"
)
type nftablesRuntimeManager interface {
Test(ctx context.Context, cfg runtimenft.SSHConfig) error
Reconcile(ctx context.Context, cfg runtimenft.SSHConfig, plan runtimenft.NodePlan) (runtimenft.ApplyResult, error)
Clear(ctx context.Context, cfg runtimenft.SSHConfig) error
}
func isNftablesForwardMode(mode string) bool {
return strings.EqualFold(strings.TrimSpace(mode), runtimenft.ModeNftables)
}
func (h *Handler) nodeUsesNftables(nodeID int64) (bool, error) {
return h.nodeUsesNftablesTx(nil, nodeID)
}
func (h *Handler) nodeUsesNftablesTx(tx *gorm.DB, nodeID int64) (bool, error) {
if h == nil || h.repo == nil {
return false, errors.New("handler not initialized")
}
var (
mode string
err error
)
if tx != nil {
mode, err = h.repo.GetNodeForwardModeTx(tx, nodeID)
} else {
mode, err = h.repo.GetNodeForwardMode(nodeID)
}
if err != nil {
return false, err
}
return isNftablesForwardMode(mode), nil
}
func (h *Handler) tunnelUsesNftables(tunnelID int64) (bool, []int64, error) {
entryNodeIDs, err := h.tunnelEntryNodeIDs(tunnelID)
if err != nil {
return false, nil, err
}
for _, nodeID := range entryNodeIDs {
ok, modeErr := h.nodeUsesNftables(nodeID)
if modeErr != nil {
return false, nil, modeErr
}
if ok {
return true, entryNodeIDs, nil
}
}
return false, entryNodeIDs, nil
}
func (h *Handler) validateNftablesForwardRequest(tunnel *tunnelRecord, remoteAddr string, entryNodeIDs []int64) error {
if tunnel == nil {
return errors.New("隧道不存在")
}
if tunnel.Type != 1 {
return errors.New("nftables 节点仅支持直连隧道")
}
if len(entryNodeIDs) != 1 {
return errors.New("nftables 节点仅支持单入口隧道")
}
if _, err := runtimenft.ParseSingleTarget(remoteAddr); err != nil {
return err
}
return nil
}
func sshConfigFromModel(cfg *model.NodeSSHConfig) (runtimenft.SSHConfig, error) {
if cfg == nil {
return runtimenft.SSHConfig{}, errors.New("节点缺少 SSH 配置")
}
if strings.TrimSpace(cfg.Host) == "" || strings.TrimSpace(cfg.Username) == "" {
return runtimenft.SSHConfig{}, errors.New("节点 SSH 配置不完整")
}
return runtimenft.SSHConfig{
Host: strings.TrimSpace(cfg.Host),
Port: cfg.Port,
Username: strings.TrimSpace(cfg.Username),
AuthType: strings.TrimSpace(cfg.AuthType),
Password: cfg.Password.String,
PrivateKey: cfg.PrivateKey.String,
Passphrase: cfg.Passphrase.String,
SudoMode: strings.TrimSpace(cfg.SudoMode),
}, nil
}
func (h *Handler) validateNftablesTunnelState(entryNodeIDs []int64) error {
return h.validateNftablesTunnelStateTx(nil, entryNodeIDs)
}
func (h *Handler) validateNftablesTunnelStateTx(tx *gorm.DB, entryNodeIDs []int64) error {
if h == nil || h.repo == nil {
return errors.New("handler not initialized")
}
for _, nodeID := range entryNodeIDs {
isNft, err := h.nodeUsesNftablesTx(tx, nodeID)
if err != nil {
return err
}
if !isNft {
continue
}
var cfg *model.NodeSSHConfig
if tx != nil {
cfg, err = h.repo.GetNodeSSHConfigTx(tx, nodeID)
} else {
cfg, err = h.repo.GetNodeSSHConfig(nodeID)
}
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return errors.New("nftables 节点缺少 SSH 配置")
}
return err
}
sshCfg, err := sshConfigFromModel(cfg)
if err != nil {
return err
}
if h.nftablesManager == nil {
return errors.New("nftables manager not initialized")
}
if err := h.nftablesManager.Test(context.Background(), sshCfg); err != nil {
return fmt.Errorf("nftables 节点能力校验失败: %w", err)
}
}
return nil
}
func (h *Handler) buildNftablesNodePlan(nodeID int64) (runtimenft.NodePlan, *model.NodeSSHConfig, error) {
cfg, err := h.repo.GetNodeSSHConfig(nodeID)
if err != nil {
return runtimenft.NodePlan{}, nil, err
}
forwards, err := h.repo.ListActiveForwardsByNode(nodeID)
if err != nil {
return runtimenft.NodePlan{}, nil, err
}
plan := runtimenft.NodePlan{NodeID: nodeID, Rules: make([]runtimenft.Rule, 0, len(forwards))}
for i := range forwards {
forward := &forwards[i]
tunnel, err := h.getTunnelRecord(forward.TunnelID)
if err != nil || tunnel == nil || tunnel.Status != 1 {
continue
}
entryNodeIDs, err := h.tunnelEntryNodeIDs(forward.TunnelID)
if err != nil {
return runtimenft.NodePlan{}, nil, err
}
if len(entryNodeIDs) != 1 || entryNodeIDs[0] != nodeID {
continue
}
if err := h.validateNftablesForwardRequest(tunnel, forward.RemoteAddr, entryNodeIDs); err != nil {
return runtimenft.NodePlan{}, nil, err
}
ports, err := h.listForwardPorts(forward.ID)
if err != nil {
return runtimenft.NodePlan{}, nil, err
}
for _, fp := range ports {
if fp.NodeID != nodeID {
continue
}
target, err := runtimenft.ParseSingleTarget(forward.RemoteAddr)
if err != nil {
return runtimenft.NodePlan{}, nil, err
}
plan.Rules = append(plan.Rules, runtimenft.Rule{
ForwardID: forward.ID,
InPort: fp.Port,
BindIP: strings.TrimSpace(fp.InIP),
TargetHost: target.Host,
TargetPort: target.Port,
Protocols: []string{"tcp", "udp"},
})
}
}
return plan, cfg, nil
}
func (h *Handler) syncNftablesNode(nodeID int64) error {
if h == nil || h.repo == nil {
return errors.New("handler not initialized")
}
plan, cfgModel, err := h.buildNftablesNodePlan(nodeID)
if err != nil {
return err
}
sshCfg, err := sshConfigFromModel(cfgModel)
if err != nil {
return err
}
result, err := h.nftablesManager.Reconcile(context.Background(), sshCfg, plan)
now := time.Now().UnixMilli()
if err != nil {
bindings, _ := h.repo.ListNftRuleBindingsByNode(nodeID)
for _, binding := range bindings {
_ = h.repo.MarkNftRuleBindingError(binding.ForwardID, nodeID, err.Error(), now)
}
return err
}
activeForwardIDs := make(map[int64]struct{}, len(plan.Rules))
for _, rule := range plan.Rules {
activeForwardIDs[rule.ForwardID] = struct{}{}
hash := result.Hashes[rule.ForwardID]
_ = h.repo.UpsertNftRuleBinding(modelToRuleBindingInput(nodeID, rule, hash), now)
}
bindings, _ := h.repo.ListNftRuleBindingsByNode(nodeID)
for _, binding := range bindings {
if _, ok := activeForwardIDs[binding.ForwardID]; ok {
continue
}
_ = h.repo.DeleteNftRuleBindingsByForward(binding.ForwardID)
}
return nil
}
func modelToRuleBindingInput(nodeID int64, rule runtimenft.Rule, hash string) repo.NftRuleBindingInput {
return repo.NftRuleBindingInput{
ForwardID: rule.ForwardID,
NodeID: nodeID,
InPort: rule.InPort,
Protocols: strings.Join(rule.Protocols, ","),
TargetAddr: fmt.Sprintf("%s:%d", rule.TargetHost, rule.TargetPort),
BindIP: rule.BindIP,
RuleHash: hash,
Status: runtimenft.StatusApplied,
}
}
func (h *Handler) nftablesNodeIDFromRequest(r *http.Request, w http.ResponseWriter) (int64, bool) {
nodeID := asInt64FromBodyKey(r, w, "nodeId")
if nodeID <= 0 {
return 0, false
}
return nodeID, true
}
func (h *Handler) loadNftablesSSHConfig(nodeID int64) (runtimenft.SSHConfig, error) {
cfg, err := h.repo.GetNodeSSHConfig(nodeID)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return runtimenft.SSHConfig{}, errors.New("nftables 节点缺少 SSH 配置")
}
return runtimenft.SSHConfig{}, err
}
return sshConfigFromModel(cfg)
}
func (h *Handler) clearNftablesNode(nodeID int64) error {
if h == nil || h.repo == nil {
return errors.New("handler not initialized")
}
sshCfg, err := h.loadNftablesSSHConfig(nodeID)
if err != nil {
return err
}
if h.nftablesManager == nil {
return errors.New("nftables manager not initialized")
}
if err := h.nftablesManager.Clear(context.Background(), sshCfg); err != nil {
return err
}
bindings, listErr := h.repo.ListNftRuleBindingsByNode(nodeID)
if listErr != nil {
return listErr
}
for _, binding := range bindings {
if err := h.repo.DeleteNftRuleBindingsByForward(binding.ForwardID); err != nil {
return err
}
}
return nil
}
func (h *Handler) reconcileNftablesNodeByRequest(nodeID int64) error {
usesNft, err := h.nodeUsesNftables(nodeID)
if err != nil {
return err
}
if !usesNft {
return errors.New("节点未启用 nftables 转发模式")
}
return h.syncNftablesNode(nodeID)
}
func (h *Handler) nodeNftablesTest(w http.ResponseWriter, r *http.Request) {
nodeID, ok := h.nftablesNodeIDFromRequest(r, w)
if !ok {
return
}
usesNft, err := h.nodeUsesNftables(nodeID)
if err != nil {
response.WriteJSON(w, response.Err(-2, err.Error()))
return
}
if !usesNft {
response.WriteJSON(w, response.ErrDefault("节点未启用 nftables 转发模式"))
return
}
sshCfg, err := h.loadNftablesSSHConfig(nodeID)
if err != nil {
response.WriteJSON(w, response.ErrDefault(err.Error()))
return
}
if h.nftablesManager == nil {
response.WriteJSON(w, response.Err(-2, "nftables manager not initialized"))
return
}
if err := h.nftablesManager.Test(context.Background(), sshCfg); err != nil {
response.WriteJSON(w, response.ErrDefault(err.Error()))
return
}
response.WriteJSON(w, response.OKEmpty())
}
func (h *Handler) nodeNftablesReconcile(w http.ResponseWriter, r *http.Request) {
nodeID, ok := h.nftablesNodeIDFromRequest(r, w)
if !ok {
return
}
if err := h.reconcileNftablesNodeByRequest(nodeID); err != nil {
response.WriteJSON(w, response.ErrDefault(err.Error()))
return
}
response.WriteJSON(w, response.OKEmpty())
}
func (h *Handler) nodeNftablesClear(w http.ResponseWriter, r *http.Request) {
nodeID, ok := h.nftablesNodeIDFromRequest(r, w)
if !ok {
return
}
usesNft, err := h.nodeUsesNftables(nodeID)
if err != nil {
response.WriteJSON(w, response.Err(-2, err.Error()))
return
}
if !usesNft {
response.WriteJSON(w, response.ErrDefault("节点未启用 nftables 转发模式"))
return
}
if err := h.clearNftablesNode(nodeID); err != nil {
response.WriteJSON(w, response.ErrDefault(err.Error()))
return
}
response.WriteJSON(w, response.OKEmpty())
}