mirror of
https://github.com/netbirdio/netbird.git
synced 2024-11-25 17:43:38 +01:00
6dee89379b
* use ipset for iptables * Update unit-tests for iptables * Remove debug code * Update dependencies * Create separate sets for dPort and sPort rules * Fix iptables tests * Fix 0.0.0.0 processing in iptables with ipset
456 lines
12 KiB
Go
456 lines
12 KiB
Go
package iptables
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"strconv"
|
|
"sync"
|
|
|
|
"github.com/coreos/go-iptables/iptables"
|
|
"github.com/google/uuid"
|
|
"github.com/nadoo/ipset"
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
fw "github.com/netbirdio/netbird/client/firewall"
|
|
"github.com/netbirdio/netbird/iface"
|
|
)
|
|
|
|
const (
|
|
// ChainInputFilterName is the name of the chain that is used for filtering incoming packets
|
|
ChainInputFilterName = "NETBIRD-ACL-INPUT"
|
|
|
|
// ChainOutputFilterName is the name of the chain that is used for filtering outgoing packets
|
|
ChainOutputFilterName = "NETBIRD-ACL-OUTPUT"
|
|
)
|
|
|
|
// dropAllDefaultRule in the Netbird chain
|
|
var dropAllDefaultRule = []string{"-j", "DROP"}
|
|
|
|
// Manager of iptables firewall
|
|
type Manager struct {
|
|
mutex sync.Mutex
|
|
|
|
ipv4Client *iptables.IPTables
|
|
ipv6Client *iptables.IPTables
|
|
|
|
inputDefaultRuleSpecs []string
|
|
outputDefaultRuleSpecs []string
|
|
wgIface iFaceMapper
|
|
|
|
rulesets map[string]ruleset
|
|
}
|
|
|
|
// iFaceMapper defines subset methods of interface required for manager
|
|
type iFaceMapper interface {
|
|
Name() string
|
|
Address() iface.WGAddress
|
|
}
|
|
|
|
type ruleset struct {
|
|
rule *Rule
|
|
ips map[string]string
|
|
}
|
|
|
|
// Create iptables firewall manager
|
|
func Create(wgIface iFaceMapper) (*Manager, error) {
|
|
m := &Manager{
|
|
wgIface: wgIface,
|
|
inputDefaultRuleSpecs: []string{
|
|
"-i", wgIface.Name(), "-j", ChainInputFilterName, "-s", wgIface.Address().String()},
|
|
outputDefaultRuleSpecs: []string{
|
|
"-o", wgIface.Name(), "-j", ChainOutputFilterName, "-d", wgIface.Address().String()},
|
|
rulesets: make(map[string]ruleset),
|
|
}
|
|
|
|
if err := ipset.Init(); err != nil {
|
|
return nil, fmt.Errorf("init ipset: %w", err)
|
|
}
|
|
|
|
// init clients for booth ipv4 and ipv6
|
|
ipv4Client, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("iptables is not installed in the system or not supported")
|
|
}
|
|
if isIptablesClientAvailable(ipv4Client) {
|
|
m.ipv4Client = ipv4Client
|
|
}
|
|
|
|
ipv6Client, err := iptables.NewWithProtocol(iptables.ProtocolIPv6)
|
|
if err != nil {
|
|
log.Errorf("ip6tables is not installed in the system or not supported: %v", err)
|
|
} else {
|
|
if isIptablesClientAvailable(ipv6Client) {
|
|
m.ipv6Client = ipv6Client
|
|
}
|
|
}
|
|
|
|
if err := m.Reset(); err != nil {
|
|
return nil, fmt.Errorf("failed to reset firewall: %v", err)
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
func isIptablesClientAvailable(client *iptables.IPTables) bool {
|
|
_, err := client.ListChains("filter")
|
|
return err == nil
|
|
}
|
|
|
|
// AddFiltering rule to the firewall
|
|
//
|
|
// If comment is empty rule ID is used as comment
|
|
func (m *Manager) AddFiltering(
|
|
ip net.IP,
|
|
protocol fw.Protocol,
|
|
sPort *fw.Port,
|
|
dPort *fw.Port,
|
|
direction fw.RuleDirection,
|
|
action fw.Action,
|
|
ipsetName string,
|
|
comment string,
|
|
) (fw.Rule, error) {
|
|
m.mutex.Lock()
|
|
defer m.mutex.Unlock()
|
|
|
|
client, err := m.client(ip)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var dPortVal, sPortVal string
|
|
if dPort != nil && dPort.Values != nil {
|
|
// TODO: we support only one port per rule in current implementation of ACLs
|
|
dPortVal = strconv.Itoa(dPort.Values[0])
|
|
}
|
|
if sPort != nil && sPort.Values != nil {
|
|
sPortVal = strconv.Itoa(sPort.Values[0])
|
|
}
|
|
ipsetName = m.transformIPsetName(ipsetName, sPortVal, dPortVal)
|
|
|
|
ruleID := uuid.New().String()
|
|
if comment == "" {
|
|
comment = ruleID
|
|
}
|
|
|
|
if ipsetName != "" {
|
|
rs, rsExists := m.rulesets[ipsetName]
|
|
if !rsExists {
|
|
if err := ipset.Flush(ipsetName); err != nil {
|
|
log.Errorf("flush ipset %q before use it: %v", ipsetName, err)
|
|
}
|
|
if err := ipset.Create(ipsetName); err != nil {
|
|
return nil, fmt.Errorf("failed to create ipset: %w", err)
|
|
}
|
|
}
|
|
|
|
if err := ipset.Add(ipsetName, ip.String()); err != nil {
|
|
return nil, fmt.Errorf("failed to add IP to ipset: %w", err)
|
|
}
|
|
|
|
if rsExists {
|
|
// if ruleset already exists it means we already have the firewall rule
|
|
// so we need to update IPs in the ruleset and return new fw.Rule object for ACL manager.
|
|
rs.ips[ip.String()] = ruleID
|
|
return &Rule{
|
|
ruleID: ruleID,
|
|
ipsetName: ipsetName,
|
|
ip: ip.String(),
|
|
dst: direction == fw.RuleDirectionOUT,
|
|
v6: ip.To4() == nil,
|
|
}, nil
|
|
}
|
|
// this is new ipset so we need to create firewall rule for it
|
|
}
|
|
|
|
specs := m.filterRuleSpecs("filter", ip, string(protocol), sPortVal, dPortVal,
|
|
direction, action, comment, ipsetName)
|
|
|
|
if direction == fw.RuleDirectionOUT {
|
|
ok, err := client.Exists("filter", ChainOutputFilterName, specs...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("check is output rule already exists: %w", err)
|
|
}
|
|
if ok {
|
|
return nil, fmt.Errorf("input rule already exists")
|
|
}
|
|
|
|
if err := client.Insert("filter", ChainOutputFilterName, 1, specs...); err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
ok, err := client.Exists("filter", ChainInputFilterName, specs...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("check is input rule already exists: %w", err)
|
|
}
|
|
if ok {
|
|
return nil, fmt.Errorf("input rule already exists")
|
|
}
|
|
|
|
if err := client.Insert("filter", ChainInputFilterName, 1, specs...); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
rule := &Rule{
|
|
ruleID: ruleID,
|
|
specs: specs,
|
|
ipsetName: ipsetName,
|
|
ip: ip.String(),
|
|
dst: direction == fw.RuleDirectionOUT,
|
|
v6: ip.To4() == nil,
|
|
}
|
|
if ipsetName != "" {
|
|
// ipset name is defined and it means that this rule was created
|
|
// for it, need to assosiate it with ruleset
|
|
m.rulesets[ipsetName] = ruleset{
|
|
rule: rule,
|
|
ips: map[string]string{rule.ip: ruleID},
|
|
}
|
|
}
|
|
|
|
return rule, nil
|
|
}
|
|
|
|
// DeleteRule from the firewall by rule definition
|
|
func (m *Manager) DeleteRule(rule fw.Rule) error {
|
|
m.mutex.Lock()
|
|
defer m.mutex.Unlock()
|
|
|
|
r, ok := rule.(*Rule)
|
|
if !ok {
|
|
return fmt.Errorf("invalid rule type")
|
|
}
|
|
|
|
client := m.ipv4Client
|
|
if r.v6 {
|
|
if m.ipv6Client == nil {
|
|
return fmt.Errorf("ipv6 is not supported")
|
|
}
|
|
client = m.ipv6Client
|
|
}
|
|
|
|
if rs, ok := m.rulesets[r.ipsetName]; ok {
|
|
// delete IP from ruleset IPs list and ipset
|
|
if _, ok := rs.ips[r.ip]; ok {
|
|
if err := ipset.Del(r.ipsetName, r.ip); err != nil {
|
|
return fmt.Errorf("failed to delete ip from ipset: %w", err)
|
|
}
|
|
delete(rs.ips, r.ip)
|
|
}
|
|
|
|
// if after delete, set still contains other IPs,
|
|
// no need to delete firewall rule and we should exit here
|
|
if len(rs.ips) != 0 {
|
|
return nil
|
|
}
|
|
|
|
// we delete last IP from the set, that means we need to delete
|
|
// set itself and assosiated firewall rule too
|
|
delete(m.rulesets, r.ipsetName)
|
|
|
|
if err := ipset.Destroy(r.ipsetName); err != nil {
|
|
log.Errorf("delete empty ipset: %v", err)
|
|
}
|
|
r = rs.rule
|
|
}
|
|
|
|
if r.dst {
|
|
return client.Delete("filter", ChainOutputFilterName, r.specs...)
|
|
}
|
|
return client.Delete("filter", ChainInputFilterName, r.specs...)
|
|
}
|
|
|
|
// Reset firewall to the default state
|
|
func (m *Manager) Reset() error {
|
|
m.mutex.Lock()
|
|
defer m.mutex.Unlock()
|
|
|
|
if err := m.reset(m.ipv4Client, "filter"); err != nil {
|
|
return fmt.Errorf("clean ipv4 firewall ACL input chain: %w", err)
|
|
}
|
|
if m.ipv6Client != nil {
|
|
if err := m.reset(m.ipv6Client, "filter"); err != nil {
|
|
return fmt.Errorf("clean ipv6 firewall ACL input chain: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Flush doesn't need to be implemented for this manager
|
|
func (m *Manager) Flush() error { return nil }
|
|
|
|
// reset firewall chain, clear it and drop it
|
|
func (m *Manager) reset(client *iptables.IPTables, table string) error {
|
|
ok, err := client.ChainExists(table, ChainInputFilterName)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to check if input chain exists: %w", err)
|
|
}
|
|
if ok {
|
|
if ok, err := client.Exists("filter", "INPUT", m.inputDefaultRuleSpecs...); err != nil {
|
|
return err
|
|
} else if ok {
|
|
if err := client.Delete("filter", "INPUT", m.inputDefaultRuleSpecs...); err != nil {
|
|
log.WithError(err).Errorf("failed to delete default input rule: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
ok, err = client.ChainExists(table, ChainOutputFilterName)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to check if output chain exists: %w", err)
|
|
}
|
|
if ok {
|
|
if ok, err := client.Exists("filter", "OUTPUT", m.outputDefaultRuleSpecs...); err != nil {
|
|
return err
|
|
} else if ok {
|
|
if err := client.Delete("filter", "OUTPUT", m.outputDefaultRuleSpecs...); err != nil {
|
|
log.WithError(err).Errorf("failed to delete default output rule: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
if err := client.ClearAndDeleteChain(table, ChainInputFilterName); err != nil {
|
|
log.Errorf("failed to clear and delete input chain: %v", err)
|
|
return nil
|
|
}
|
|
|
|
if err := client.ClearAndDeleteChain(table, ChainOutputFilterName); err != nil {
|
|
log.Errorf("failed to clear and delete input chain: %v", err)
|
|
return nil
|
|
}
|
|
|
|
for ipsetName := range m.rulesets {
|
|
if err := ipset.Flush(ipsetName); err != nil {
|
|
log.Errorf("flush ipset %q during reset: %v", ipsetName, err)
|
|
}
|
|
if err := ipset.Destroy(ipsetName); err != nil {
|
|
log.Errorf("delete ipset %q during reset: %v", ipsetName, err)
|
|
}
|
|
delete(m.rulesets, ipsetName)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// filterRuleSpecs returns the specs of a filtering rule
|
|
func (m *Manager) filterRuleSpecs(
|
|
table string, ip net.IP, protocol string, sPort, dPort string,
|
|
direction fw.RuleDirection, action fw.Action, comment string,
|
|
ipsetName string,
|
|
) (specs []string) {
|
|
matchByIP := true
|
|
// don't use IP matching if IP is ip 0.0.0.0
|
|
if s := ip.String(); s == "0.0.0.0" || s == "::" {
|
|
matchByIP = false
|
|
}
|
|
switch direction {
|
|
case fw.RuleDirectionIN:
|
|
if matchByIP {
|
|
if ipsetName != "" {
|
|
specs = append(specs, "-m", "set", "--set", ipsetName, "src")
|
|
} else {
|
|
specs = append(specs, "-s", ip.String())
|
|
}
|
|
}
|
|
case fw.RuleDirectionOUT:
|
|
if matchByIP {
|
|
if ipsetName != "" {
|
|
specs = append(specs, "-m", "set", "--set", ipsetName, "dst")
|
|
} else {
|
|
specs = append(specs, "-d", ip.String())
|
|
}
|
|
}
|
|
}
|
|
if protocol != "all" {
|
|
specs = append(specs, "-p", protocol)
|
|
}
|
|
if sPort != "" {
|
|
specs = append(specs, "--sport", sPort)
|
|
}
|
|
if dPort != "" {
|
|
specs = append(specs, "--dport", dPort)
|
|
}
|
|
specs = append(specs, "-j", m.actionToStr(action))
|
|
return append(specs, "-m", "comment", "--comment", comment)
|
|
}
|
|
|
|
// rawClient returns corresponding iptables client for the given ip
|
|
func (m *Manager) rawClient(ip net.IP) (*iptables.IPTables, error) {
|
|
if ip.To4() != nil {
|
|
return m.ipv4Client, nil
|
|
}
|
|
if m.ipv6Client == nil {
|
|
return nil, fmt.Errorf("ipv6 is not supported")
|
|
}
|
|
return m.ipv6Client, nil
|
|
}
|
|
|
|
// client returns client with initialized chain and default rules
|
|
func (m *Manager) client(ip net.IP) (*iptables.IPTables, error) {
|
|
client, err := m.rawClient(ip)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ok, err := client.ChainExists("filter", ChainInputFilterName)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to check if chain exists: %w", err)
|
|
}
|
|
|
|
if !ok {
|
|
if err := client.NewChain("filter", ChainInputFilterName); err != nil {
|
|
return nil, fmt.Errorf("failed to create input chain: %w", err)
|
|
}
|
|
|
|
if err := client.AppendUnique("filter", ChainInputFilterName, dropAllDefaultRule...); err != nil {
|
|
return nil, fmt.Errorf("failed to create default drop all in netbird input chain: %w", err)
|
|
}
|
|
|
|
if err := client.AppendUnique("filter", "INPUT", m.inputDefaultRuleSpecs...); err != nil {
|
|
return nil, fmt.Errorf("failed to create input chain jump rule: %w", err)
|
|
}
|
|
|
|
}
|
|
|
|
ok, err = client.ChainExists("filter", ChainOutputFilterName)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to check if chain exists: %w", err)
|
|
}
|
|
|
|
if !ok {
|
|
if err := client.NewChain("filter", ChainOutputFilterName); err != nil {
|
|
return nil, fmt.Errorf("failed to create output chain: %w", err)
|
|
}
|
|
|
|
if err := client.AppendUnique("filter", ChainOutputFilterName, dropAllDefaultRule...); err != nil {
|
|
return nil, fmt.Errorf("failed to create default drop all in netbird output chain: %w", err)
|
|
}
|
|
|
|
if err := client.AppendUnique("filter", "OUTPUT", m.outputDefaultRuleSpecs...); err != nil {
|
|
return nil, fmt.Errorf("failed to create output chain jump rule: %w", err)
|
|
}
|
|
}
|
|
|
|
return client, nil
|
|
}
|
|
|
|
func (m *Manager) actionToStr(action fw.Action) string {
|
|
if action == fw.ActionAccept {
|
|
return "ACCEPT"
|
|
}
|
|
return "DROP"
|
|
}
|
|
|
|
func (m *Manager) transformIPsetName(ipsetName string, sPort, dPort string) string {
|
|
if ipsetName == "" {
|
|
return ""
|
|
} else if sPort != "" && dPort != "" {
|
|
return ipsetName + "-sport-dport"
|
|
} else if sPort != "" {
|
|
return ipsetName + "-sport"
|
|
} else if dPort != "" {
|
|
return ipsetName + "-dport"
|
|
}
|
|
return ipsetName
|
|
}
|