mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-09-26 16:00:55 +00:00
408 lines
12 KiB
Go
408 lines
12 KiB
Go
package docker_guard
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
NftTable = "nft_1panel_docker"
|
|
NftBaseChain = "NFT_1PANEL_DOCKER_FORWARD"
|
|
NftChain = "NFT_1PANEL_DOCKER"
|
|
|
|
dockerNftTable = "docker-bridges"
|
|
)
|
|
|
|
type NftablesManager struct {
|
|
runner Runner
|
|
}
|
|
|
|
func NewNftablesManager() *NftablesManager { return &NftablesManager{runner: commandRunner{}} }
|
|
|
|
func NewNftablesManagerWithRunner(runner Runner) *NftablesManager {
|
|
return &NftablesManager{runner: runner}
|
|
}
|
|
|
|
func (m *NftablesManager) Initialize(policies []Policy) error {
|
|
mutationMu.Lock()
|
|
defer mutationMu.Unlock()
|
|
if !m.runner.Exists("nft") {
|
|
return errors.New("nft is not installed")
|
|
}
|
|
if err := m.ensureFamily(FamilyIPv4, true); err != nil {
|
|
return err
|
|
}
|
|
if err := m.ensureFamily(FamilyIPv6, false); err != nil {
|
|
return &FamilyError{Family: FamilyIPv6, Err: err}
|
|
}
|
|
return m.rebuildLocked(policies)
|
|
}
|
|
|
|
func (m *NftablesManager) Bind() error {
|
|
mutationMu.Lock()
|
|
defer mutationMu.Unlock()
|
|
if err := m.bindExistingFamily(FamilyIPv4, true); err != nil {
|
|
return err
|
|
}
|
|
if err := m.bindExistingFamily(FamilyIPv6, false); err != nil {
|
|
return &FamilyError{Family: FamilyIPv6, Err: err}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *NftablesManager) Reconcile(policies []Policy) error {
|
|
mutationMu.Lock()
|
|
defer mutationMu.Unlock()
|
|
return m.rebuildLocked(policies)
|
|
}
|
|
|
|
func (m *NftablesManager) ListPolicies() ([]Policy, error) {
|
|
if !m.runner.Exists("nft") {
|
|
return nil, nil
|
|
}
|
|
policies := make([]Policy, 0)
|
|
for _, family := range []string{FamilyIPv4, FamilyIPv6} {
|
|
tableFamily := nftTableFamily(family)
|
|
if !m.objectExists("chain", tableFamily, NftTable, NftChain) {
|
|
continue
|
|
}
|
|
output, err := m.run("-a", "list", "chain", tableFamily, NftTable, NftChain)
|
|
if err != nil {
|
|
return nil, &FamilyError{Family: family, Err: fmt.Errorf("list %s chain: %w", NftChain, err)}
|
|
}
|
|
parsed, err := parseDockerGuardPolicies(output, family)
|
|
if err != nil {
|
|
return nil, &FamilyError{Family: family, Err: err}
|
|
}
|
|
policies = append(policies, parsed...)
|
|
}
|
|
return policies, nil
|
|
}
|
|
|
|
func (m *NftablesManager) Unbind() error {
|
|
mutationMu.Lock()
|
|
defer mutationMu.Unlock()
|
|
for _, family := range []string{FamilyIPv4, FamilyIPv6} {
|
|
if err := m.unbindFamily(family); err != nil {
|
|
return &FamilyError{Family: family, Err: err}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *NftablesManager) Cleanup() error {
|
|
mutationMu.Lock()
|
|
defer mutationMu.Unlock()
|
|
if !m.runner.Exists("nft") {
|
|
return nil
|
|
}
|
|
commands := make([][]string, 0, 2)
|
|
for _, family := range []string{FamilyIPv4, FamilyIPv6} {
|
|
tableFamily := nftTableFamily(family)
|
|
if !m.objectExists("table", tableFamily, NftTable) {
|
|
continue
|
|
}
|
|
commands = append(commands, []string{"delete", "table", tableFamily, NftTable})
|
|
}
|
|
return m.runBatch(commands)
|
|
}
|
|
|
|
func (m *NftablesManager) Initialized(family string) (bool, error) {
|
|
if nftTableFamily(family) == "" || !m.runner.Exists("nft") {
|
|
return false, nil
|
|
}
|
|
tableFamily := nftTableFamily(family)
|
|
if !m.objectExists("chain", tableFamily, NftTable, NftBaseChain) {
|
|
return false, nil
|
|
}
|
|
return m.objectExists("chain", tableFamily, NftTable, NftChain), nil
|
|
}
|
|
|
|
func (m *NftablesManager) Status(family string) FamilyStatus {
|
|
tableFamily := nftTableFamily(family)
|
|
if tableFamily == "" || !m.runner.Exists("nft") {
|
|
return FamilyStatus{State: StatusDisabled, Reason: ReasonCommandMissing}
|
|
}
|
|
if !m.objectExists("table", tableFamily, dockerNftTable) {
|
|
return FamilyStatus{State: StatusDisabled, Reason: ReasonDockerChainMissing}
|
|
}
|
|
if !m.objectExists("chain", tableFamily, NftTable, NftBaseChain) ||
|
|
!m.objectExists("chain", tableFamily, NftTable, NftChain) {
|
|
return FamilyStatus{State: StatusDisabled, Reason: ReasonGuardChainMissing}
|
|
}
|
|
status := FamilyStatus{State: StatusNotEffective, Initialized: true}
|
|
rules, err := m.run("-a", "list", "chain", tableFamily, NftTable, NftBaseChain)
|
|
if err != nil {
|
|
status.Reason = ReasonInspectFailed
|
|
return status
|
|
}
|
|
jumps := nftJumpHandles(rules)
|
|
if len(jumps) == 0 {
|
|
status.Reason = ReasonJumpMissing
|
|
return status
|
|
}
|
|
if len(jumps) > 1 {
|
|
status.Reason = ReasonJumpDuplicate
|
|
return status
|
|
}
|
|
if !nftHasFirstUniqueJump(rules) {
|
|
status.Reason = ReasonJumpNotFirst
|
|
return status
|
|
}
|
|
status.State = StatusEffective
|
|
status.Bound = true
|
|
status.Effective = true
|
|
return status
|
|
}
|
|
|
|
func (m *NftablesManager) ensureFamily(family string, required bool) error {
|
|
tableFamily := nftTableFamily(family)
|
|
if tableFamily == "" {
|
|
return fmt.Errorf("unsupported address family %q", family)
|
|
}
|
|
if !m.objectExists("table", tableFamily, dockerNftTable) {
|
|
if required {
|
|
return fmt.Errorf("%w for nftables %s", ErrDockerChainUnavailable, family)
|
|
}
|
|
return nil
|
|
}
|
|
commands := make([][]string, 0, 6)
|
|
tableExists := m.objectExists("table", tableFamily, NftTable)
|
|
if !tableExists {
|
|
commands = append(commands, []string{"add", "table", tableFamily, NftTable})
|
|
}
|
|
baseExists := tableExists && m.objectExists("chain", tableFamily, NftTable, NftBaseChain)
|
|
if !baseExists {
|
|
commands = append(commands, []string{
|
|
"add", "chain", tableFamily, NftTable, NftBaseChain,
|
|
"{", "type", "filter", "hook", "forward", "priority", "filter", "-", "1", ";", "policy", "accept", ";", "}",
|
|
})
|
|
}
|
|
if !tableExists || !m.objectExists("chain", tableFamily, NftTable, NftChain) {
|
|
commands = append(commands, []string{"add", "chain", tableFamily, NftTable, NftChain})
|
|
}
|
|
if baseExists {
|
|
output, err := m.run("-a", "list", "chain", tableFamily, NftTable, NftBaseChain)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, handle := range nftJumpHandles(output) {
|
|
commands = append(commands, []string{"delete", "rule", tableFamily, NftTable, NftBaseChain, "handle", handle})
|
|
}
|
|
}
|
|
commands = append(commands, []string{"insert", "rule", tableFamily, NftTable, NftBaseChain, "jump", NftChain})
|
|
return m.runBatch(commands)
|
|
}
|
|
|
|
func (m *NftablesManager) bindExistingFamily(family string, required bool) error {
|
|
tableFamily := nftTableFamily(family)
|
|
if !m.runner.Exists("nft") {
|
|
if required {
|
|
return errors.New("nft is not installed")
|
|
}
|
|
return nil
|
|
}
|
|
if !m.objectExists("table", tableFamily, dockerNftTable) {
|
|
if required {
|
|
return fmt.Errorf("%w for nftables %s", ErrDockerChainUnavailable, family)
|
|
}
|
|
return nil
|
|
}
|
|
if !m.objectExists("chain", tableFamily, NftTable, NftBaseChain) ||
|
|
!m.objectExists("chain", tableFamily, NftTable, NftChain) {
|
|
if required {
|
|
return fmt.Errorf("%s chain is not initialized for nftables %s", NftChain, family)
|
|
}
|
|
return nil
|
|
}
|
|
return m.ensureJump(family)
|
|
}
|
|
|
|
func (m *NftablesManager) ensureJump(family string) error {
|
|
tableFamily := nftTableFamily(family)
|
|
output, err := m.run("-a", "list", "chain", tableFamily, NftTable, NftBaseChain)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
commands := make([][]string, 0, len(nftJumpHandles(output))+1)
|
|
for _, handle := range nftJumpHandles(output) {
|
|
commands = append(commands, []string{"delete", "rule", tableFamily, NftTable, NftBaseChain, "handle", handle})
|
|
}
|
|
commands = append(commands, []string{"insert", "rule", tableFamily, NftTable, NftBaseChain, "jump", NftChain})
|
|
return m.runBatch(commands)
|
|
}
|
|
|
|
func (m *NftablesManager) rebuildLocked(policies []Policy) error {
|
|
if !m.runner.Exists("nft") {
|
|
return nil
|
|
}
|
|
for _, family := range []string{FamilyIPv4, FamilyIPv6} {
|
|
tableFamily := nftTableFamily(family)
|
|
if !m.objectExists("chain", tableFamily, NftTable, NftChain) {
|
|
continue
|
|
}
|
|
commands := [][]string{{"flush", "chain", tableFamily, NftTable, NftChain}}
|
|
commands = append(commands, []string{"add", "rule", tableFamily, NftTable, NftChain, "ct", "state", "{", "established,related", "}", "return"})
|
|
for _, policy := range policies {
|
|
if policy.Family == family {
|
|
commands = append(commands, compileNftPolicy(policy)...)
|
|
}
|
|
}
|
|
commands = append(commands, []string{"add", "rule", tableFamily, NftTable, NftChain, "return"})
|
|
script, err := buildNftScript(commands)
|
|
if err != nil {
|
|
return &FamilyError{Family: family, Err: err}
|
|
}
|
|
if _, err := m.runner.RunInput("nft", script, "-f", "-"); err != nil {
|
|
return &FamilyError{Family: family, Err: fmt.Errorf("restore rules: %w", err)}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func compileNftPolicy(policy Policy) [][]string {
|
|
tableFamily := nftTableFamily(policy.Family)
|
|
addressKeyword := tableFamily
|
|
base := []string{"add", "rule", tableFamily, NftTable, NftChain, "meta", "l4proto", policy.Protocol}
|
|
if !isWildcardHost(policy.Family, policy.HostIP) {
|
|
base = append(base, "ct", "original", addressKeyword, "daddr", policy.HostIP)
|
|
}
|
|
base = append(base, "ct", "original", "proto-dst", strconv.Itoa(int(policy.HostPort)))
|
|
comment := strconv.Quote("1panel-docker:" + policy.UUID)
|
|
if policy.Mode == ModeAll {
|
|
return [][]string{append(append([]string{}, base...), "comment", comment, "drop")}
|
|
}
|
|
target := "drop"
|
|
capacity := len(policy.Sources)
|
|
if policy.Mode == ModeAllow {
|
|
target = "return"
|
|
capacity++
|
|
}
|
|
rules := make([][]string, 0, capacity)
|
|
for _, source := range policy.Sources {
|
|
args := append([]string{}, base...)
|
|
args = append(args, addressKeyword, "saddr", source, "comment", comment, target)
|
|
rules = append(rules, args)
|
|
}
|
|
if policy.Mode == ModeAllow {
|
|
rules = append(rules, append(append([]string{}, base...), "comment", comment, "drop"))
|
|
}
|
|
return rules
|
|
}
|
|
|
|
func buildNftScript(commands [][]string) (string, error) {
|
|
var script strings.Builder
|
|
for _, command := range commands {
|
|
for index, token := range command {
|
|
if !validNftToken(token) {
|
|
return "", fmt.Errorf("invalid nftables token %q", token)
|
|
}
|
|
if index > 0 {
|
|
script.WriteByte(' ')
|
|
}
|
|
script.WriteString(token)
|
|
}
|
|
script.WriteByte('\n')
|
|
}
|
|
return script.String(), nil
|
|
}
|
|
|
|
func validNftToken(token string) bool {
|
|
if token == "" || strings.ContainsAny(token, "\r\n") {
|
|
return false
|
|
}
|
|
if strings.HasPrefix(token, `"`) {
|
|
_, err := strconv.Unquote(token)
|
|
return err == nil
|
|
}
|
|
return !strings.ContainsAny(token, " \t\\\"'")
|
|
}
|
|
|
|
func (m *NftablesManager) unbindFamily(family string) error {
|
|
if !m.runner.Exists("nft") {
|
|
return nil
|
|
}
|
|
tableFamily := nftTableFamily(family)
|
|
if !m.objectExists("chain", tableFamily, NftTable, NftBaseChain) {
|
|
return nil
|
|
}
|
|
output, err := m.run("-a", "list", "chain", tableFamily, NftTable, NftBaseChain)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
commands := make([][]string, 0, len(nftJumpHandles(output)))
|
|
for _, handle := range nftJumpHandles(output) {
|
|
commands = append(commands, []string{"delete", "rule", tableFamily, NftTable, NftBaseChain, "handle", handle})
|
|
}
|
|
return m.runBatch(commands)
|
|
}
|
|
|
|
func (m *NftablesManager) runBatch(commands [][]string) error {
|
|
if len(commands) == 0 {
|
|
return nil
|
|
}
|
|
script, err := buildNftScript(commands)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := m.runner.RunInput("nft", script, "-f", "-"); err != nil {
|
|
return fmt.Errorf("batch update Docker guard lifecycle: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *NftablesManager) objectExists(kind string, args ...string) bool {
|
|
command := append([]string{"list", kind}, args...)
|
|
_, err := m.run(command...)
|
|
return err == nil
|
|
}
|
|
|
|
func (m *NftablesManager) run(args ...string) (string, error) {
|
|
return m.runner.Run("nft", args...)
|
|
}
|
|
|
|
func nftTableFamily(family string) string {
|
|
switch family {
|
|
case FamilyIPv4:
|
|
return "ip"
|
|
case FamilyIPv6:
|
|
return "ip6"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func nftJumpHandles(output string) []string {
|
|
handles := make([]string, 0)
|
|
for _, line := range strings.Split(output, "\n") {
|
|
fields := strings.Fields(strings.TrimSpace(line))
|
|
if len(fields) < 4 || fields[0] != "jump" || fields[1] != NftChain {
|
|
continue
|
|
}
|
|
for index := 2; index+1 < len(fields); index++ {
|
|
if fields[index] == "handle" {
|
|
handles = append(handles, fields[index+1])
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return handles
|
|
}
|
|
|
|
func nftHasFirstUniqueJump(output string) bool {
|
|
if len(nftJumpHandles(output)) != 1 {
|
|
return false
|
|
}
|
|
for _, line := range strings.Split(output, "\n") {
|
|
line = strings.TrimSpace(line)
|
|
if !strings.Contains(line, "# handle ") {
|
|
continue
|
|
}
|
|
fields := strings.Fields(line)
|
|
return len(fields) >= 2 && fields[0] == "jump" && fields[1] == NftChain
|
|
}
|
|
return false
|
|
}
|