mirror of
https://github.com/netbirdio/netbird.git
synced 2024-12-02 04:53:51 +01:00
ef59001459
Modify rules in iptables and nftables to accept all traffic not from netbird network but routed through it.
338 lines
8.8 KiB
Go
338 lines
8.8 KiB
Go
package iptables
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"strconv"
|
|
"sync"
|
|
|
|
"github.com/coreos/go-iptables/iptables"
|
|
"github.com/google/uuid"
|
|
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
|
|
}
|
|
|
|
// iFaceMapper defines subset methods of interface required for manager
|
|
type iFaceMapper interface {
|
|
Name() string
|
|
Address() iface.WGAddress
|
|
}
|
|
|
|
// 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()},
|
|
}
|
|
|
|
// 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")
|
|
}
|
|
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 {
|
|
m.ipv6Client = ipv6Client
|
|
}
|
|
|
|
if err := m.Reset(); err != nil {
|
|
return nil, fmt.Errorf("failed to reset firewall: %v", err)
|
|
}
|
|
return m, 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,
|
|
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])
|
|
}
|
|
|
|
ruleID := uuid.New().String()
|
|
if comment == "" {
|
|
comment = ruleID
|
|
}
|
|
|
|
specs := m.filterRuleSpecs(
|
|
"filter",
|
|
ip,
|
|
string(protocol),
|
|
sPortVal,
|
|
dPortVal,
|
|
direction,
|
|
action,
|
|
comment,
|
|
)
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
return &Rule{
|
|
id: ruleID,
|
|
specs: specs,
|
|
dst: direction == fw.RuleDirectionOUT,
|
|
v6: ip.To4() == nil,
|
|
}, 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 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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
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,
|
|
) (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 {
|
|
specs = append(specs, "-s", ip.String())
|
|
}
|
|
case fw.RuleDirectionOUT:
|
|
if matchByIP {
|
|
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"
|
|
}
|