645 lines
22 KiB
Go
645 lines
22 KiB
Go
package main
|
||
|
||
import (
|
||
"crypto/rand"
|
||
"encoding/hex"
|
||
"fmt"
|
||
"os/exec"
|
||
"regexp"
|
||
"strconv"
|
||
"strings"
|
||
)
|
||
|
||
// =============================================================================
|
||
// firewall — просмотр и упрощённое управление ufw / nftables / iptables
|
||
// =============================================================================
|
||
//
|
||
// Backend автоопределяется (ufw, если активен → nftables, если есть непустой
|
||
// ruleset → iptables как fallback). Каждое новое правило получает уникальный
|
||
// тег-комментарий "auspex-XXXXXXXX", по которому модуль находит его в списке
|
||
// после применения (для undo) и разрешает удаление ТОЛЬКО управляемых им
|
||
// правил — системные/сторонние правила показываются, но не редактируются.
|
||
//
|
||
// ГЛАВНАЯ ЗАЩИТА: любое новое правило после применения ждёт подтверждения
|
||
// (клавиша 'y') в течение cfgFWRollbackSeconds секунд. Если не подтвердить —
|
||
// правило автоматически откатывается. Это защищает от случайной блокировки
|
||
// себя же (например по SSH) при удалённой работе.
|
||
//
|
||
// ВАЖНО: команды для nftables/iptables написаны по документированному
|
||
// синтаксису, но не протестированы на живой системе в песочнице — используйте
|
||
// dry-run (клавиша 'D') при первом знакомстве с инструментом на новом сервере.
|
||
// =============================================================================
|
||
|
||
// ── Общие типы ───────────────────────────────────────────────────────────────
|
||
|
||
type fwRule struct {
|
||
Backend string
|
||
Chain string // INPUT, PREROUTING(nat), inet/auspex/input, ...
|
||
ID string // line number (iptables/ufw) или handle (nft)
|
||
Action string // ACCEPT/DROP/DNAT/LIMIT...
|
||
Proto string
|
||
Port string
|
||
Source string
|
||
Destination string
|
||
Comment string // "auspex-xxxxxxxx", если правило наше
|
||
Kind string // "rule" | "forward" | "ratelimit" | "" (не наше)
|
||
Managed bool
|
||
Extra string
|
||
}
|
||
|
||
// fwBackend — унифицированный интерфейс для ufw/nftables/iptables.
|
||
type fwBackend interface {
|
||
Name() string
|
||
ListRules() ([]fwRule, error)
|
||
// Rule — разрешить/запретить трафик. action: "allow" | "deny".
|
||
// port=="" — правило на весь источник (все порты); source=="" — отовсюду.
|
||
Rule(action, proto, port, source string) (apply func() error, undo func() error, err error)
|
||
PortForward(proto, extPort, dstIP, dstPort string) (apply func() error, undo func() error, err error)
|
||
RateLimit(proto, port string, hitCount, seconds int) (apply func() error, undo func() error, err error)
|
||
DeleteRule(r fwRule) error
|
||
}
|
||
|
||
func fwNewTag() string {
|
||
b := make([]byte, 4)
|
||
_, _ = rand.Read(b)
|
||
return "auspex-" + hex.EncodeToString(b)
|
||
}
|
||
|
||
func fwExec(name string, args ...string) error {
|
||
out, err := exec.Command(name, args...).CombinedOutput()
|
||
if err != nil {
|
||
return fmt.Errorf("%s %s: %v\n%s", name, strings.Join(args, " "), err, strings.TrimSpace(string(out)))
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func fwExecOut(name string, args ...string) (string, error) {
|
||
out, err := exec.Command(name, args...).CombinedOutput()
|
||
return string(out), err
|
||
}
|
||
|
||
// fwCmdPreview — человекочитаемая команда для dry-run / экрана подтверждения.
|
||
func fwCmdPreview(name string, args ...string) string {
|
||
return name + " " + strings.Join(args, " ")
|
||
}
|
||
|
||
// ── Автоопределение бэкенда ────────────────────────────────────────────────
|
||
|
||
func fwDetectBackend() (fwBackend, error) {
|
||
if _, err := exec.LookPath("ufw"); err == nil {
|
||
out, _ := fwExecOut("ufw", "status")
|
||
if strings.Contains(out, "Status: active") {
|
||
return fwUFWBackend{}, nil
|
||
}
|
||
}
|
||
if _, err := exec.LookPath("nft"); err == nil {
|
||
out, err := fwExecOut("nft", "list", "ruleset")
|
||
if err == nil && strings.TrimSpace(out) != "" {
|
||
return fwNFTBackend{}, nil
|
||
}
|
||
}
|
||
if _, err := exec.LookPath("iptables"); err == nil {
|
||
return fwIPTBackend{}, nil
|
||
}
|
||
return nil, fmt.Errorf("не найден ни ufw (активный), ни nftables (непустой ruleset), ни iptables")
|
||
}
|
||
|
||
func fwPersistenceHint(backendName string) string {
|
||
switch backendName {
|
||
case "ufw":
|
||
return "ufw сохраняет правила автоматически (/etc/ufw/*.rules)."
|
||
case "nftables":
|
||
return "nftables НЕ сохраняет правила между рестартами автоматически — добавьте вручную в /etc/nftables.conf (например 'nft list ruleset >> /etc/nftables.conf' и проверьте файл руками)."
|
||
case "iptables":
|
||
return "iptables НЕ сохраняет правила автоматически — выполните 'netfilter-persistent save' (Debian/Ubuntu) или аналог для вашего дистрибутива."
|
||
default:
|
||
return ""
|
||
}
|
||
}
|
||
|
||
// =============================================================================
|
||
// UFW backend
|
||
// =============================================================================
|
||
|
||
type fwUFWBackend struct{}
|
||
|
||
func (b fwUFWBackend) Name() string { return "ufw" }
|
||
|
||
var fwUFWLineRe = regexp.MustCompile(`^\[\s*(\d+)\]\s+(.+?)\s{2,}(ALLOW|DENY|REJECT|LIMIT)\s+(IN|OUT)\s{2,}(.+)$`)
|
||
|
||
func (b fwUFWBackend) ListRules() ([]fwRule, error) {
|
||
out, err := fwExecOut("ufw", "status", "numbered")
|
||
if err != nil {
|
||
return nil, fmt.Errorf("ufw status numbered: %v\n%s", err, out)
|
||
}
|
||
var rules []fwRule
|
||
for _, line := range strings.Split(out, "\n") {
|
||
line = strings.TrimRight(line, " \r")
|
||
m := fwUFWLineRe.FindStringSubmatch(line)
|
||
if m == nil {
|
||
continue
|
||
}
|
||
to := strings.TrimSpace(m[2])
|
||
proto := ""
|
||
port := to
|
||
if idx := strings.LastIndex(to, "/"); idx != -1 {
|
||
proto = to[idx+1:]
|
||
port = to[:idx]
|
||
}
|
||
rules = append(rules, fwRule{
|
||
Backend: "ufw", Chain: "ufw", ID: m[1],
|
||
Action: m[3], Proto: proto, Port: port,
|
||
Source: strings.TrimSpace(m[5]), Extra: line,
|
||
})
|
||
}
|
||
return rules, nil
|
||
}
|
||
|
||
// fwUFWNewestRule возвращает правило с максимальным номером — сразу после
|
||
// добавления ufw обычно дописывает новое правило в конец списка.
|
||
func (b fwUFWBackend) fwUFWNewestRule() (fwRule, error) {
|
||
rules, err := b.ListRules()
|
||
if err != nil {
|
||
return fwRule{}, err
|
||
}
|
||
if len(rules) == 0 {
|
||
return fwRule{}, fmt.Errorf("список правил ufw пуст после применения")
|
||
}
|
||
best := rules[0]
|
||
bestNum := 0
|
||
for _, r := range rules {
|
||
if n, err := strconv.Atoi(r.ID); err == nil && n > bestNum {
|
||
bestNum = n
|
||
best = r
|
||
}
|
||
}
|
||
return best, nil
|
||
}
|
||
|
||
func (b fwUFWBackend) Rule(action, proto, port, source string) (func() error, func() error, error) {
|
||
verb := "allow"
|
||
if action == "deny" {
|
||
verb = "deny"
|
||
}
|
||
var args []string
|
||
if source != "" {
|
||
args = []string{verb, "from", source}
|
||
if port != "" {
|
||
args = append(args, "to", "any", "port", port)
|
||
if proto != "" {
|
||
args = append(args, "proto", proto)
|
||
}
|
||
}
|
||
} else if port != "" {
|
||
spec := port
|
||
if proto != "" {
|
||
spec += "/" + proto
|
||
}
|
||
args = []string{verb, spec}
|
||
} else {
|
||
return nil, nil, fmt.Errorf("нужно указать порт и/или источник")
|
||
}
|
||
|
||
apply := func() error { return fwExec("ufw", args...) }
|
||
undo := func() error {
|
||
r, err := b.fwUFWNewestRule()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return b.DeleteRule(r)
|
||
}
|
||
return apply, undo, nil
|
||
}
|
||
|
||
func (b fwUFWBackend) PortForward(proto, extPort, dstIP, dstPort string) (func() error, func() error, error) {
|
||
// ufw не даёт нативного простого CLI для DNAT — работаем напрямую через
|
||
// iptables (ufw и так построен поверх него), но это НЕ персистентно через
|
||
// конфиг ufw — нужно вручную добавить в /etc/ufw/before.rules для сохранения.
|
||
tag := fwNewTag()
|
||
dnatArgs := []string{"-t", "nat", "-I", "PREROUTING", "1", "-p", proto,
|
||
"--dport", extPort, "-j", "DNAT", "--to-destination", dstIP + ":" + dstPort}
|
||
fwdArgs := []string{"-I", "FORWARD", "1", "-p", proto, "-d", dstIP,
|
||
"--dport", dstPort, "-j", "ACCEPT", "-m", "comment", "--comment", tag}
|
||
|
||
apply := func() error {
|
||
if err := fwExec("iptables", dnatArgs...); err != nil {
|
||
return err
|
||
}
|
||
return fwExec("iptables", fwdArgs...)
|
||
}
|
||
undo := func() error {
|
||
undnat := []string{"-t", "nat", "-D", "PREROUTING", "-p", proto,
|
||
"--dport", extPort, "-j", "DNAT", "--to-destination", dstIP + ":" + dstPort}
|
||
_ = fwExec("iptables", undnat...)
|
||
unfwd := []string{"-D", "FORWARD", "-p", proto, "-d", dstIP,
|
||
"--dport", dstPort, "-j", "ACCEPT", "-m", "comment", "--comment", tag}
|
||
return fwExec("iptables", unfwd...)
|
||
}
|
||
return apply, undo, nil
|
||
}
|
||
|
||
func (b fwUFWBackend) RateLimit(proto, port string, hitCount, seconds int) (func() error, func() error, error) {
|
||
// ufw имеет нативный 'limit' (примерно 6 попыток / 30 сек, пороги не кастомизируются
|
||
// через CLI) — используем его напрямую, кастомные hitCount/seconds здесь игнорируются.
|
||
spec := port + "/" + proto
|
||
apply := func() error { return fwExec("ufw", "limit", spec) }
|
||
undo := func() error { return fwExec("ufw", "delete", "limit", spec) }
|
||
return apply, undo, nil
|
||
}
|
||
|
||
func (b fwUFWBackend) DeleteRule(r fwRule) error {
|
||
return fwExec("ufw", "--force", "delete", r.ID)
|
||
}
|
||
|
||
// =============================================================================
|
||
// nftables backend
|
||
// =============================================================================
|
||
|
||
const fwNFTTable = "auspex"
|
||
const fwNFTChain = "input"
|
||
const fwNFTNatTable = "auspex_nat"
|
||
|
||
type fwNFTBackend struct{}
|
||
|
||
func (b fwNFTBackend) Name() string { return "nftables" }
|
||
|
||
func (b fwNFTBackend) ensureFilterChain() error {
|
||
if err := fwExec("nft", "add", "table", "inet", fwNFTTable); err != nil {
|
||
return err
|
||
}
|
||
return fwExec("nft", "add", "chain", "inet", fwNFTTable, fwNFTChain,
|
||
"{", "type", "filter", "hook", "input", "priority", "0", ";", "policy", "accept", ";", "}")
|
||
}
|
||
|
||
func (b fwNFTBackend) ensureNatChain() error {
|
||
if err := fwExec("nft", "add", "table", "ip", fwNFTNatTable); err != nil {
|
||
return err
|
||
}
|
||
return fwExec("nft", "add", "chain", "ip", fwNFTNatTable, "prerouting",
|
||
"{", "type", "nat", "hook", "prerouting", "priority", "-100", ";", "}")
|
||
}
|
||
|
||
var fwNFTRuleRe = regexp.MustCompile(`^\s*(.+?)\s*#\s*handle\s+(\d+)\s*$`)
|
||
var fwNFTCommentRe = regexp.MustCompile(`comment\s+"?(auspex-[0-9a-f]+)"?`)
|
||
var fwNFTPortRe = regexp.MustCompile(`dport\s+(\S+)`)
|
||
var fwNFTProtoRe = regexp.MustCompile(`^\s*(tcp|udp)\b`)
|
||
var fwNFTSaddrRe = regexp.MustCompile(`ip6?\s+saddr\s+(\S+)`)
|
||
|
||
func fwNFTParseLines(out, chainLabel string) []fwRule {
|
||
var rules []fwRule
|
||
for _, line := range strings.Split(out, "\n") {
|
||
m := fwNFTRuleRe.FindStringSubmatch(line)
|
||
if m == nil {
|
||
continue
|
||
}
|
||
body, handle := m[1], m[2]
|
||
action := "accept"
|
||
switch {
|
||
case strings.Contains(body, "drop"):
|
||
action = "drop"
|
||
case strings.Contains(body, "dnat"):
|
||
action = "dnat"
|
||
case strings.Contains(body, "reject"):
|
||
action = "reject"
|
||
}
|
||
kind := "rule"
|
||
if strings.Contains(body, "dnat") {
|
||
kind = "forward"
|
||
} else if strings.Contains(body, "limit rate") {
|
||
kind = "ratelimit"
|
||
}
|
||
proto := ""
|
||
if pm := fwNFTProtoRe.FindStringSubmatch(body); pm != nil {
|
||
proto = pm[1]
|
||
}
|
||
port := ""
|
||
if pm := fwNFTPortRe.FindStringSubmatch(body); pm != nil {
|
||
port = pm[1]
|
||
}
|
||
source := ""
|
||
if sm := fwNFTSaddrRe.FindStringSubmatch(body); sm != nil {
|
||
source = sm[1]
|
||
}
|
||
comment := ""
|
||
managed := false
|
||
if cm := fwNFTCommentRe.FindStringSubmatch(body); cm != nil {
|
||
comment = cm[1]
|
||
managed = true
|
||
}
|
||
rules = append(rules, fwRule{
|
||
Backend: "nftables", Chain: chainLabel, ID: handle,
|
||
Action: action, Proto: proto, Port: port, Source: source,
|
||
Comment: comment, Kind: kind, Managed: managed, Extra: strings.TrimSpace(body),
|
||
})
|
||
}
|
||
return rules
|
||
}
|
||
|
||
func (b fwNFTBackend) ListRules() ([]fwRule, error) {
|
||
var rules []fwRule
|
||
if out, err := fwExecOut("nft", "-a", "list", "table", "inet", fwNFTTable); err == nil {
|
||
rules = append(rules, fwNFTParseLines(out, "inet/"+fwNFTTable+"/"+fwNFTChain)...)
|
||
}
|
||
if out, err := fwExecOut("nft", "-a", "list", "table", "ip", fwNFTNatTable); err == nil {
|
||
rules = append(rules, fwNFTParseLines(out, "ip/"+fwNFTNatTable+"/prerouting")...)
|
||
}
|
||
return rules, nil
|
||
}
|
||
|
||
func (b fwNFTBackend) findByTag(tag string) (fwRule, error) {
|
||
rules, err := b.ListRules()
|
||
if err != nil {
|
||
return fwRule{}, err
|
||
}
|
||
for _, r := range rules {
|
||
if r.Comment == tag {
|
||
return r, nil
|
||
}
|
||
}
|
||
return fwRule{}, fmt.Errorf("правило с тегом %s не найдено после применения (проверьте синтаксис nft вручную)", tag)
|
||
}
|
||
|
||
func (b fwNFTBackend) Rule(action, proto, port, source string) (func() error, func() error, error) {
|
||
if err := b.ensureFilterChain(); err != nil {
|
||
return nil, nil, fmt.Errorf("не удалось подготовить таблицу/цепочку auspex: %w", err)
|
||
}
|
||
tag := fwNewTag()
|
||
verb := "accept"
|
||
if action == "deny" {
|
||
verb = "drop"
|
||
}
|
||
|
||
args := []string{"add", "rule", "inet", fwNFTTable, fwNFTChain}
|
||
if proto != "" {
|
||
args = append(args, proto)
|
||
}
|
||
if port != "" {
|
||
args = append(args, "dport", port)
|
||
}
|
||
if source != "" {
|
||
fam := "ip"
|
||
if strings.Contains(source, ":") {
|
||
fam = "ip6"
|
||
}
|
||
args = append(args, fam, "saddr", source)
|
||
}
|
||
args = append(args, verb, "comment", tag)
|
||
|
||
apply := func() error { return fwExec("nft", args...) }
|
||
undo := func() error {
|
||
r, err := b.findByTag(tag)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return b.DeleteRule(r)
|
||
}
|
||
return apply, undo, nil
|
||
}
|
||
|
||
func (b fwNFTBackend) PortForward(proto, extPort, dstIP, dstPort string) (func() error, func() error, error) {
|
||
if err := b.ensureNatChain(); err != nil {
|
||
return nil, nil, fmt.Errorf("не удалось подготовить nat-таблицу auspex: %w", err)
|
||
}
|
||
tag := fwNewTag()
|
||
args := []string{"add", "rule", "ip", fwNFTNatTable, "prerouting",
|
||
proto, "dport", extPort, "dnat", "to", dstIP + ":" + dstPort, "comment", tag}
|
||
|
||
apply := func() error { return fwExec("nft", args...) }
|
||
undo := func() error {
|
||
r, err := b.findByTag(tag)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return b.DeleteRule(r)
|
||
}
|
||
return apply, undo, nil
|
||
}
|
||
|
||
func (b fwNFTBackend) RateLimit(proto, port string, hitCount, seconds int) (func() error, func() error, error) {
|
||
if err := b.ensureFilterChain(); err != nil {
|
||
return nil, nil, fmt.Errorf("не удалось подготовить таблицу/цепочку auspex: %w", err)
|
||
}
|
||
tag := fwNewTag()
|
||
perMinute := hitCount
|
||
if seconds > 0 && seconds != 60 {
|
||
perMinute = hitCount * 60 / seconds
|
||
if perMinute < 1 {
|
||
perMinute = 1
|
||
}
|
||
}
|
||
// Вставляем В НАЧАЛО цепочки (insert без позиции = начало) — порядок важен:
|
||
// правило лимита должно идти РАНЬШЕ любого общего accept для этого порта,
|
||
// иначе более ранний accept "перехватит" пакет первым.
|
||
args := []string{"insert", "rule", "inet", fwNFTTable, fwNFTChain,
|
||
proto, "dport", port, "ct", "state", "new",
|
||
"limit", "rate", "over", fmt.Sprintf("%d/minute", perMinute),
|
||
"drop", "comment", tag}
|
||
|
||
apply := func() error { return fwExec("nft", args...) }
|
||
undo := func() error {
|
||
r, err := b.findByTag(tag)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return b.DeleteRule(r)
|
||
}
|
||
return apply, undo, nil
|
||
}
|
||
|
||
func (b fwNFTBackend) DeleteRule(r fwRule) error {
|
||
parts := strings.SplitN(r.Chain, "/", 3)
|
||
if len(parts) != 3 {
|
||
return fmt.Errorf("некорректная цепочка nft: %s", r.Chain)
|
||
}
|
||
family, table, chain := parts[0], parts[1], parts[2]
|
||
return fwExec("nft", "delete", "rule", family, table, chain, "handle", r.ID)
|
||
}
|
||
|
||
// =============================================================================
|
||
// iptables backend
|
||
// =============================================================================
|
||
|
||
type fwIPTBackend struct{}
|
||
|
||
func (b fwIPTBackend) Name() string { return "iptables" }
|
||
|
||
var fwIPTDptRe = regexp.MustCompile(`dpts?:(\S+)`)
|
||
var fwIPTCommentRe = regexp.MustCompile(`/\*\s*(auspex-[0-9a-f]+)\s*\*/`)
|
||
|
||
func fwIPTParseChain(out, chainLabel string) []fwRule {
|
||
var rules []fwRule
|
||
for _, line := range strings.Split(out, "\n") {
|
||
fields := strings.Fields(strings.TrimSpace(line))
|
||
if len(fields) < 10 {
|
||
continue
|
||
}
|
||
if _, err := strconv.Atoi(fields[0]); err != nil {
|
||
continue // не строка данных (заголовок/Chain ...)
|
||
}
|
||
num, target, proto := fields[0], fields[3], fields[4]
|
||
source, destination := fields[8], fields[9]
|
||
extra := strings.Join(fields[10:], " ")
|
||
|
||
port := ""
|
||
if m := fwIPTDptRe.FindStringSubmatch(extra); m != nil {
|
||
port = m[1]
|
||
}
|
||
comment := ""
|
||
managed := false
|
||
if m := fwIPTCommentRe.FindStringSubmatch(extra); m != nil {
|
||
comment = m[1]
|
||
managed = true
|
||
}
|
||
kind := "rule"
|
||
if strings.Contains(chainLabel, "nat") {
|
||
kind = "forward"
|
||
} else if strings.Contains(extra, "recent") {
|
||
kind = "ratelimit"
|
||
}
|
||
|
||
rules = append(rules, fwRule{
|
||
Backend: "iptables", Chain: chainLabel, ID: num,
|
||
Action: target, Proto: proto, Port: port,
|
||
Source: source, Destination: destination,
|
||
Comment: comment, Kind: kind, Managed: managed, Extra: extra,
|
||
})
|
||
}
|
||
return rules
|
||
}
|
||
|
||
func (b fwIPTBackend) ListRules() ([]fwRule, error) {
|
||
var rules []fwRule
|
||
out, err := fwExecOut("iptables", "-L", "INPUT", "-n", "-v", "--line-numbers")
|
||
if err != nil {
|
||
return nil, fmt.Errorf("iptables -L INPUT: %v\n%s", err, out)
|
||
}
|
||
rules = append(rules, fwIPTParseChain(out, "INPUT")...)
|
||
|
||
if natOut, err := fwExecOut("iptables", "-t", "nat", "-L", "PREROUTING", "-n", "-v", "--line-numbers"); err == nil {
|
||
rules = append(rules, fwIPTParseChain(natOut, "PREROUTING(nat)")...)
|
||
}
|
||
return rules, nil
|
||
}
|
||
|
||
func (b fwIPTBackend) findByTag(tag, chain string) (fwRule, error) {
|
||
rules, err := b.ListRules()
|
||
if err != nil {
|
||
return fwRule{}, err
|
||
}
|
||
for _, r := range rules {
|
||
if r.Comment == tag && strings.HasPrefix(r.Chain, chain) {
|
||
return r, nil
|
||
}
|
||
}
|
||
return fwRule{}, fmt.Errorf("правило с тегом %s не найдено после применения", tag)
|
||
}
|
||
|
||
func (b fwIPTBackend) Rule(action, proto, port, source string) (func() error, func() error, error) {
|
||
target := "ACCEPT"
|
||
if action == "deny" {
|
||
target = "DROP"
|
||
}
|
||
tag := fwNewTag()
|
||
args := []string{"-I", "INPUT", "1"}
|
||
if proto != "" {
|
||
args = append(args, "-p", proto)
|
||
}
|
||
if port != "" {
|
||
args = append(args, "--dport", port)
|
||
}
|
||
if source != "" {
|
||
args = append(args, "-s", source)
|
||
}
|
||
args = append(args, "-j", target, "-m", "comment", "--comment", tag)
|
||
|
||
apply := func() error { return fwExec("iptables", args...) }
|
||
undo := func() error {
|
||
r, err := b.findByTag(tag, "INPUT")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return b.DeleteRule(r)
|
||
}
|
||
return apply, undo, nil
|
||
}
|
||
|
||
func (b fwIPTBackend) PortForward(proto, extPort, dstIP, dstPort string) (func() error, func() error, error) {
|
||
tag := fwNewTag()
|
||
dnatArgs := []string{"-t", "nat", "-I", "PREROUTING", "1", "-p", proto,
|
||
"--dport", extPort, "-j", "DNAT", "--to-destination", dstIP + ":" + dstPort,
|
||
"-m", "comment", "--comment", tag}
|
||
fwdArgs := []string{"-I", "FORWARD", "1", "-p", proto, "-d", dstIP,
|
||
"--dport", dstPort, "-j", "ACCEPT", "-m", "comment", "--comment", tag}
|
||
|
||
apply := func() error {
|
||
if err := fwExec("iptables", dnatArgs...); err != nil {
|
||
return err
|
||
}
|
||
return fwExec("iptables", fwdArgs...)
|
||
}
|
||
undo := func() error {
|
||
// FORWARD-цепочка не входит в ListRules() (там только INPUT и nat/PREROUTING),
|
||
// поэтому удаляем оба правила напрямую по полной спецификации, без поиска по тегу.
|
||
nat, errNat := b.findByTag(tag, "PREROUTING")
|
||
if errNat == nil {
|
||
_ = b.DeleteRule(nat)
|
||
} else {
|
||
undnat := []string{"-t", "nat", "-D", "PREROUTING", "-p", proto,
|
||
"--dport", extPort, "-j", "DNAT", "--to-destination", dstIP + ":" + dstPort,
|
||
"-m", "comment", "--comment", tag}
|
||
_ = fwExec("iptables", undnat...)
|
||
}
|
||
return fwExec("iptables", "-D", "FORWARD", "-p", proto, "-d", dstIP,
|
||
"--dport", dstPort, "-j", "ACCEPT", "-m", "comment", "--comment", tag)
|
||
}
|
||
return apply, undo, nil
|
||
}
|
||
|
||
func (b fwIPTBackend) RateLimit(proto, port string, hitCount, seconds int) (func() error, func() error, error) {
|
||
tag := fwNewTag()
|
||
name := "AUSPEX_" + port
|
||
setArgs := []string{"-I", "INPUT", "1", "-p", proto, "--dport", port,
|
||
"-m", "conntrack", "--ctstate", "NEW",
|
||
"-m", "recent", "--set", "--name", name,
|
||
"-m", "comment", "--comment", tag}
|
||
dropArgs := []string{"-I", "INPUT", "1", "-p", proto, "--dport", port,
|
||
"-m", "conntrack", "--ctstate", "NEW",
|
||
"-m", "recent", "--update", "--seconds", strconv.Itoa(seconds),
|
||
"--hitcount", strconv.Itoa(hitCount), "--name", name,
|
||
"-j", "DROP", "-m", "comment", "--comment", tag}
|
||
|
||
apply := func() error {
|
||
// dropArgs добавляем первым (-I ... 1), затем setArgs тоже в позицию 1 —
|
||
// в итоге set оказывается ПЕРЕД drop, именно такой порядок нужен модулю recent.
|
||
if err := fwExec("iptables", dropArgs...); err != nil {
|
||
return err
|
||
}
|
||
return fwExec("iptables", setArgs...)
|
||
}
|
||
undo := func() error {
|
||
rules, err := b.ListRules()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
var lastErr error
|
||
for _, r := range rules {
|
||
if r.Comment == tag && r.Chain == "INPUT" {
|
||
if err := b.DeleteRule(r); err != nil {
|
||
lastErr = err
|
||
}
|
||
}
|
||
}
|
||
return lastErr
|
||
}
|
||
return apply, undo, nil
|
||
}
|
||
|
||
func (b fwIPTBackend) DeleteRule(r fwRule) error {
|
||
chain := r.Chain
|
||
table := "filter"
|
||
if strings.Contains(chain, "(nat)") {
|
||
chain = strings.TrimSuffix(chain, "(nat)")
|
||
table = "nat"
|
||
}
|
||
return fwExec("iptables", "-t", table, "-D", chain, r.ID)
|
||
}
|