mirror of
https://github.com/netbirdio/netbird.git
synced 2026-07-16 19:49:56 +00:00
286 lines
8.5 KiB
Go
286 lines
8.5 KiB
Go
//go:build !android
|
|
|
|
package iptables
|
|
|
|
import (
|
|
"fmt"
|
|
"net/netip"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/go-multierror"
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
nberrors "github.com/netbirdio/netbird/client/errors"
|
|
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
|
)
|
|
|
|
func (r *family) AddDNATRule(rule firewall.ForwardRule) (firewall.Rule, error) {
|
|
ruleID := rule.ID()
|
|
if _, exists := r.rules[ruleID+dnatSuffix]; exists {
|
|
return rule, nil
|
|
}
|
|
|
|
toDestination := rule.TranslatedAddress.String()
|
|
switch {
|
|
case len(rule.TranslatedPort.Values) == 0:
|
|
// no translated port, use original port
|
|
case len(rule.TranslatedPort.Values) == 1:
|
|
toDestination += fmt.Sprintf(":%d", rule.TranslatedPort.Values[0])
|
|
case rule.TranslatedPort.IsRange && len(rule.TranslatedPort.Values) == 2:
|
|
// need the "/originalport" suffix to avoid dnat port randomization
|
|
toDestination += fmt.Sprintf(":%d-%d/%d", rule.TranslatedPort.Values[0], rule.TranslatedPort.Values[1], rule.DestinationPort.Values[0])
|
|
default:
|
|
return nil, fmt.Errorf("invalid translated port: %v", rule.TranslatedPort)
|
|
}
|
|
|
|
proto := strings.ToLower(string(rule.Protocol))
|
|
|
|
rules := make(map[firewall.RuleID]ruleInfo, 3)
|
|
|
|
// DNAT rule
|
|
dnatRule := []string{
|
|
"!", "-i", r.wgIface.Name(),
|
|
"-p", proto,
|
|
"-j", "DNAT",
|
|
"--to-destination", toDestination,
|
|
}
|
|
dnatRule = append(dnatRule, applyPort("--dport", &rule.DestinationPort)...)
|
|
rules[ruleID+dnatSuffix] = ruleInfo{
|
|
table: tableNat,
|
|
chain: chainRTRdr,
|
|
rule: dnatRule,
|
|
}
|
|
|
|
// SNAT rule
|
|
snatRule := []string{
|
|
"-o", r.wgIface.Name(),
|
|
"-p", proto,
|
|
"-d", rule.TranslatedAddress.String(),
|
|
"-j", "MASQUERADE",
|
|
}
|
|
snatRule = append(snatRule, applyPort("--dport", &rule.TranslatedPort)...)
|
|
rules[ruleID+snatSuffix] = ruleInfo{
|
|
table: tableNat,
|
|
chain: chainRTNAT,
|
|
rule: snatRule,
|
|
}
|
|
|
|
// Forward filtering rule, if fwd policy is DROP
|
|
forwardRule := []string{
|
|
"-o", r.wgIface.Name(),
|
|
"-p", proto,
|
|
"-d", rule.TranslatedAddress.String(),
|
|
"-j", "ACCEPT",
|
|
}
|
|
forwardRule = append(forwardRule, applyPort("--dport", &rule.TranslatedPort)...)
|
|
rules[ruleID+fwdSuffix] = ruleInfo{
|
|
table: tableFilter,
|
|
chain: chainRTFwdOut,
|
|
rule: forwardRule,
|
|
}
|
|
|
|
// Request forwarding once the rule is about to be installed, releasing
|
|
// it if installation fails so the refcount tracks the real rules.
|
|
if err := r.ipFwdState.RequestForwarding(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for key, ruleInfo := range rules {
|
|
if err := r.iptablesClient.Append(ruleInfo.table, ruleInfo.chain, ruleInfo.rule...); err != nil {
|
|
if rollbackErr := r.rollbackRules(rules); rollbackErr != nil {
|
|
log.Errorf("rollback failed: %v", rollbackErr)
|
|
}
|
|
r.releaseForwarding()
|
|
return nil, fmt.Errorf("add rule %s: %w", key, err)
|
|
}
|
|
r.rules[key] = ruleInfo.rule
|
|
}
|
|
|
|
r.updateState()
|
|
return rule, nil
|
|
}
|
|
|
|
func (r *family) rollbackRules(rules map[firewall.RuleID]ruleInfo) error {
|
|
var merr *multierror.Error
|
|
for key, ruleInfo := range rules {
|
|
if err := r.iptablesClient.DeleteIfExists(ruleInfo.table, ruleInfo.chain, ruleInfo.rule...); err != nil {
|
|
merr = multierror.Append(merr, fmt.Errorf("rollback rule %s: %w", key, err))
|
|
// On rollback error, add to rules map for next cleanup
|
|
r.rules[key] = ruleInfo.rule
|
|
}
|
|
}
|
|
if merr != nil {
|
|
r.updateState()
|
|
}
|
|
return nberrors.FormatErrorOrNil(merr)
|
|
}
|
|
|
|
func (r *family) DeleteDNATRule(rule firewall.Rule) error {
|
|
ruleID := rule.ID()
|
|
|
|
var merr *multierror.Error
|
|
var found bool
|
|
if dnatRule, exists := r.rules[ruleID+dnatSuffix]; exists {
|
|
found = true
|
|
if err := r.iptablesClient.Delete(tableNat, chainRTRdr, dnatRule...); err != nil {
|
|
merr = multierror.Append(merr, fmt.Errorf("delete DNAT rule: %w", err))
|
|
}
|
|
delete(r.rules, ruleID+dnatSuffix)
|
|
}
|
|
|
|
if snatRule, exists := r.rules[ruleID+snatSuffix]; exists {
|
|
found = true
|
|
if err := r.iptablesClient.Delete(tableNat, chainRTNAT, snatRule...); err != nil {
|
|
merr = multierror.Append(merr, fmt.Errorf("delete SNAT rule: %w", err))
|
|
}
|
|
delete(r.rules, ruleID+snatSuffix)
|
|
}
|
|
|
|
if fwdRule, exists := r.rules[ruleID+fwdSuffix]; exists {
|
|
found = true
|
|
if err := r.iptablesClient.Delete(tableFilter, chainRTFwdOut, fwdRule...); err != nil {
|
|
merr = multierror.Append(merr, fmt.Errorf("delete forward rule: %w", err))
|
|
}
|
|
delete(r.rules, ruleID+fwdSuffix)
|
|
}
|
|
|
|
r.updateState()
|
|
|
|
// Release once, only if the rule was present and removed.
|
|
if merr == nil && found {
|
|
r.releaseForwarding()
|
|
}
|
|
|
|
return nberrors.FormatErrorOrNil(merr)
|
|
}
|
|
|
|
// releaseForwarding drops one IP forwarding reference, logging any error.
|
|
func (r *family) releaseForwarding() {
|
|
if err := r.ipFwdState.ReleaseForwarding(); err != nil {
|
|
log.Errorf("release IP forwarding: %v", err)
|
|
}
|
|
}
|
|
|
|
func (r *family) AddInboundDNAT(localAddr netip.Addr, protocol firewall.Protocol, originalPort, translatedPort uint16) error {
|
|
ruleID := firewall.RuleID(fmt.Sprintf("inbound-dnat-%s-%s-%d-%d", localAddr.String(), protocol, originalPort, translatedPort))
|
|
|
|
if _, exists := r.rules[ruleID]; exists {
|
|
return nil
|
|
}
|
|
|
|
dnatRule := []string{
|
|
"-i", r.wgIface.Name(),
|
|
"-p", strings.ToLower(protoForFamily(protocol, r.v6)),
|
|
"--dport", strconv.Itoa(int(originalPort)),
|
|
"-d", localAddr.String(),
|
|
"-m", "addrtype", "--dst-type", "LOCAL",
|
|
"-j", "DNAT",
|
|
"--to-destination", ":" + strconv.Itoa(int(translatedPort)),
|
|
}
|
|
|
|
info := ruleInfo{
|
|
table: tableNat,
|
|
chain: chainRTRdr,
|
|
rule: dnatRule,
|
|
}
|
|
|
|
if err := r.iptablesClient.Append(info.table, info.chain, info.rule...); err != nil {
|
|
return fmt.Errorf("add inbound DNAT rule: %w", err)
|
|
}
|
|
r.rules[ruleID] = info.rule
|
|
|
|
r.updateState()
|
|
return nil
|
|
}
|
|
|
|
// RemoveInboundDNAT removes an inbound DNAT rule.
|
|
func (r *family) RemoveInboundDNAT(localAddr netip.Addr, protocol firewall.Protocol, originalPort, translatedPort uint16) error {
|
|
ruleID := firewall.RuleID(fmt.Sprintf("inbound-dnat-%s-%s-%d-%d", localAddr.String(), protocol, originalPort, translatedPort))
|
|
|
|
if dnatRule, exists := r.rules[ruleID]; exists {
|
|
if err := r.iptablesClient.Delete(tableNat, chainRTRdr, dnatRule...); err != nil {
|
|
return fmt.Errorf("delete inbound DNAT rule: %w", err)
|
|
}
|
|
delete(r.rules, ruleID)
|
|
}
|
|
|
|
r.updateState()
|
|
return nil
|
|
}
|
|
|
|
// ensureNATOutputChain lazily creates the OUTPUT NAT chain and jump rule on first use.
|
|
func (r *family) ensureNATOutputChain() error {
|
|
if _, exists := r.rules[jumpNATOutput]; exists {
|
|
return nil
|
|
}
|
|
|
|
chainExists, err := r.iptablesClient.ChainExists(tableNat, chainNATOutput)
|
|
if err != nil {
|
|
return fmt.Errorf("check chain %s: %w", chainNATOutput, err)
|
|
}
|
|
if !chainExists {
|
|
if err := r.iptablesClient.NewChain(tableNat, chainNATOutput); err != nil {
|
|
return fmt.Errorf("create chain %s: %w", chainNATOutput, err)
|
|
}
|
|
}
|
|
|
|
jumpRule := jumpRuleSpec(chainNATOutput)
|
|
if err := r.iptablesClient.Insert(tableNat, chainOutput, 1, jumpRule...); err != nil {
|
|
if !chainExists {
|
|
if delErr := r.iptablesClient.ClearAndDeleteChain(tableNat, chainNATOutput); delErr != nil {
|
|
log.Warnf("failed to rollback chain %s: %v", chainNATOutput, delErr)
|
|
}
|
|
}
|
|
return fmt.Errorf("add OUTPUT jump rule: %w", err)
|
|
}
|
|
r.rules[jumpNATOutput] = jumpRule
|
|
|
|
r.updateState()
|
|
return nil
|
|
}
|
|
|
|
// AddOutputDNAT adds an OUTPUT chain DNAT rule for locally-generated traffic.
|
|
func (r *family) AddOutputDNAT(localAddr netip.Addr, protocol firewall.Protocol, originalPort, translatedPort uint16) error {
|
|
ruleID := firewall.RuleID(fmt.Sprintf("output-dnat-%s-%s-%d-%d", localAddr.String(), protocol, originalPort, translatedPort))
|
|
|
|
if _, exists := r.rules[ruleID]; exists {
|
|
return nil
|
|
}
|
|
|
|
if err := r.ensureNATOutputChain(); err != nil {
|
|
return err
|
|
}
|
|
|
|
dnatRule := []string{
|
|
"-p", strings.ToLower(protoForFamily(protocol, localAddr.Is6())),
|
|
"--dport", strconv.Itoa(int(originalPort)),
|
|
"-d", localAddr.String(),
|
|
"-j", "DNAT",
|
|
"--to-destination", ":" + strconv.Itoa(int(translatedPort)),
|
|
}
|
|
|
|
if err := r.iptablesClient.Append(tableNat, chainNATOutput, dnatRule...); err != nil {
|
|
return fmt.Errorf("add output DNAT rule: %w", err)
|
|
}
|
|
r.rules[ruleID] = dnatRule
|
|
|
|
r.updateState()
|
|
return nil
|
|
}
|
|
|
|
// RemoveOutputDNAT removes an OUTPUT chain DNAT rule.
|
|
func (r *family) RemoveOutputDNAT(localAddr netip.Addr, protocol firewall.Protocol, originalPort, translatedPort uint16) error {
|
|
ruleID := firewall.RuleID(fmt.Sprintf("output-dnat-%s-%s-%d-%d", localAddr.String(), protocol, originalPort, translatedPort))
|
|
|
|
if dnatRule, exists := r.rules[ruleID]; exists {
|
|
if err := r.iptablesClient.Delete(tableNat, chainNATOutput, dnatRule...); err != nil {
|
|
return fmt.Errorf("delete output DNAT rule: %w", err)
|
|
}
|
|
delete(r.rules, ruleID)
|
|
}
|
|
|
|
r.updateState()
|
|
return nil
|
|
}
|