Files
netbird/client/firewall/iptables/manager_linux.go
2023-03-23 16:54:04 +04:00

242 lines
6.1 KiB
Go

package iptables
import (
"fmt"
"net"
"strconv"
"sync"
"github.com/coreos/go-iptables/iptables"
"github.com/google/uuid"
fw "github.com/netbirdio/netbird/client/firewall"
log "github.com/sirupsen/logrus"
)
const (
// ChainFilterName is the name of the chain that is used for filtering by the Netbird client
ChainFilterName = "NETBIRD-ACL"
)
// jumpNetbirdDefaultRule always added by manager to the input chain for all trafic from the Netbird interface
var jumpNetbirdDefaultRule = []string{
"-j", ChainFilterName, "-m", "comment", "--comment", "Netbird traffic chain jump"}
// pingSupportDefaultRule always added by the manager to the Netbird ACL chain
var pingSupportDefaultRule = []string{
"-p", "icmp", "--icmp-type", "echo-request", "-j",
"ACCEPT", "-m", "comment", "--comment", "Allow pings from the Netbird Devices"}
// Manager of iptables firewall
type Manager struct {
mutex sync.Mutex
ipv4Client *iptables.IPTables
ipv6Client *iptables.IPTables
wgIfaceName string
}
// Create iptables firewall manager
func Create(wgIfaceName string) (*Manager, error) {
m := &Manager{
wgIfaceName: wgIfaceName,
}
// 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")
} 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,
port *fw.Port,
direction fw.Direction,
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 portValue string
if port != nil && port.Values != nil {
// TODO: we support only one port per rule in current implementation of ACLs
portValue = strconv.Itoa(port.Values[0])
}
ruleID := uuid.New().String()
if comment == "" {
comment = ruleID
}
specs := m.filterRuleSpecs(
"filter",
ChainFilterName,
ip,
string(protocol),
portValue,
direction,
action,
comment,
)
ok, err := client.Exists("filter", ChainFilterName, specs...)
if err != nil {
return nil, fmt.Errorf("check is rule already exists: %w", err)
}
if ok {
return nil, fmt.Errorf("rule already exists")
}
if err := client.Insert("filter", ChainFilterName, 1, specs...); err != nil {
return nil, err
}
return &Rule{id: ruleID, specs: specs, 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
}
return client.Delete("filter", ChainFilterName, 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", ChainFilterName); err != nil {
return fmt.Errorf("clean ipv4 firewall ACL chain: %w", err)
}
if m.ipv6Client != nil {
if err := m.reset(m.ipv6Client, "filter", ChainFilterName); err != nil {
return fmt.Errorf("clean ipv6 firewall ACL chain: %w", err)
}
}
return nil
}
// reset firewall chain, clear it and drop it
func (m *Manager) reset(client *iptables.IPTables, table, chain string) error {
ok, err := client.ChainExists(table, chain)
if err != nil {
return fmt.Errorf("failed to check if chain exists: %w", err)
}
if !ok {
return nil
}
specs := append([]string{"-i", m.wgIfaceName}, jumpNetbirdDefaultRule...)
if err := client.Delete("filter", "INPUT", specs...); err != nil {
return fmt.Errorf("failed to delete default rule: %w", err)
}
return client.ClearAndDeleteChain(table, chain)
}
// filterRuleSpecs returns the specs of a filtering rule
func (m *Manager) filterRuleSpecs(
table string, chain string, ip net.IP, protocol string, port string,
direction fw.Direction, action fw.Action, comment string,
) (specs []string) {
switch direction {
case fw.DirectionSrc:
specs = append(specs, "-s", ip.String())
case fw.DirectionDst:
specs = append(specs, "-d", ip.String())
}
specs = append(specs, "-p", protocol)
if port != "" {
specs = append(specs, "--dport", port)
}
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", ChainFilterName)
if err != nil {
return nil, fmt.Errorf("failed to check if chain exists: %w", err)
}
if !ok {
if err := client.NewChain("filter", ChainFilterName); err != nil {
return nil, fmt.Errorf("failed to create chain: %w", err)
}
if err := client.AppendUnique("filter", ChainFilterName, pingSupportDefaultRule...); err != nil {
return nil, fmt.Errorf("failed to create default ping allow rule: %w", err)
}
specs := append([]string{"-i", m.wgIfaceName}, jumpNetbirdDefaultRule...)
if err := client.AppendUnique("filter", "INPUT", specs...); err != nil {
return nil, fmt.Errorf("failed to create chain: %w", err)
}
}
return client, nil
}
func (m *Manager) actionToStr(action fw.Action) string {
if action == fw.ActionAccept {
return "ACCEPT"
}
return "DROP"
}