mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-30 10:39:08 +02:00
Add support for IPv6 networks (on Linux clients) (#1459)
* Feat add basic support for IPv6 networks Newly generated networks automatically generate an IPv6 prefix of size 64 within the ULA address range, devices obtain a randomly generated address within this prefix. Currently, this is Linux only and does not yet support all features (routes currently cause an error). * Fix firewall configuration for IPv6 networks * Fix routing configuration for IPv6 networks * Feat provide info on IPv6 support for specific client to mgmt server * Feat allow configuration of IPv6 support through API, improve stability * Feat add IPv6 support to new firewall implementation * Fix peer list item response not containing IPv6 address * Fix nftables breaking on IPv6 address change * Fix build issues for non-linux systems * Fix intermittent disconnections when IPv6 is enabled * Fix test issues and make some minor revisions * Fix some more testing issues * Fix more CI issues due to IPv6 * Fix more testing issues * Add inheritance of IPv6 enablement status from groups * Fix IPv6 events not having associated messages * Address first review comments regarding IPv6 support * Fix IPv6 table being created even when IPv6 is disabled Also improved stability of IPv6 route and firewall handling on client side * Fix IPv6 routes not being removed * Fix DNS IPv6 issues, limit IPv6 nameservers to IPv6 peers * Improve code for IPv6 DNS server selection, add AAAA custom records * Ensure IPv6 routes can only exist for IPv6 routing peers * Fix IPv6 network generation randomness * Fix a bunch of compilation issues and test failures * Replace method calls that are unavailable in Go 1.21 * Fix nil dereference in cleanUpDefaultForwardRules6 * Fix nil pointer dereference when persisting IPv6 network in sqlite * Clean up of client-side code changes for IPv6 * Fix nil dereference in rule mangling and compilation issues * Add a bunch of client-side test cases for IPv6 * Fix IPv6 tests running on unsupported environments * Fix import cycle in tests * Add missing method SupportsIPv6() for windows * Require IPv6 default route for IPv6 tests * Fix panics in routemanager tests on non-linux * Fix some more route manager tests concerning IPv6 * Add some final client-side tests * Add IPv6 tests for management code, small fixes * Fix linting issues * Fix small test suite issues * Fix linter issues and builds on macOS and Windows again * fix builds for iOS because of IPv6 breakage
This commit is contained in:
@@ -26,7 +26,8 @@ const (
|
||||
|
||||
// some presets for building nftable rules
|
||||
var (
|
||||
zeroXor = binaryutil.NativeEndian.PutUint32(0)
|
||||
zeroXor = binaryutil.NativeEndian.PutUint32(0)
|
||||
zeroXor6 = []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}
|
||||
|
||||
exprCounterAccept = []expr.Any{
|
||||
&expr.Counter{},
|
||||
@@ -39,48 +40,69 @@ var (
|
||||
)
|
||||
|
||||
type router struct {
|
||||
ctx context.Context
|
||||
stop context.CancelFunc
|
||||
conn *nftables.Conn
|
||||
workTable *nftables.Table
|
||||
filterTable *nftables.Table
|
||||
chains map[string]*nftables.Chain
|
||||
ctx context.Context
|
||||
stop context.CancelFunc
|
||||
conn *nftables.Conn
|
||||
workTable *nftables.Table
|
||||
workTable6 *nftables.Table
|
||||
filterTable *nftables.Table
|
||||
filterTable6 *nftables.Table
|
||||
chains map[string]*nftables.Chain
|
||||
chains6 map[string]*nftables.Chain
|
||||
// rules is useful to avoid duplicates and to get missing attributes that we don't have when adding new rules
|
||||
rules map[string]*nftables.Rule
|
||||
isDefaultFwdRulesEnabled bool
|
||||
rules map[string]*nftables.Rule
|
||||
rules6 map[string]*nftables.Rule
|
||||
isDefaultFwdRulesEnabled bool
|
||||
isDefaultFwdRulesEnabled6 bool
|
||||
}
|
||||
|
||||
func newRouter(parentCtx context.Context, workTable *nftables.Table) (*router, error) {
|
||||
func newRouter(parentCtx context.Context, workTable *nftables.Table, workTable6 *nftables.Table) (*router, error) {
|
||||
ctx, cancel := context.WithCancel(parentCtx)
|
||||
|
||||
r := &router{
|
||||
ctx: ctx,
|
||||
stop: cancel,
|
||||
conn: &nftables.Conn{},
|
||||
workTable: workTable,
|
||||
chains: make(map[string]*nftables.Chain),
|
||||
rules: make(map[string]*nftables.Rule),
|
||||
ctx: ctx,
|
||||
stop: cancel,
|
||||
conn: &nftables.Conn{},
|
||||
workTable: workTable,
|
||||
workTable6: workTable6,
|
||||
chains: make(map[string]*nftables.Chain),
|
||||
chains6: make(map[string]*nftables.Chain),
|
||||
rules: make(map[string]*nftables.Rule),
|
||||
rules6: make(map[string]*nftables.Rule),
|
||||
}
|
||||
|
||||
var err error
|
||||
r.filterTable, err = r.loadFilterTable()
|
||||
r.filterTable, r.filterTable6, err = r.loadFilterTables()
|
||||
if err != nil {
|
||||
if errors.Is(err, errFilterTableNotFound) {
|
||||
log.Warnf("table 'filter' not found for forward rules")
|
||||
log.Warnf("table 'filter' not found for forward rules for one of the supported address families-")
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
err = r.cleanUpDefaultForwardRules()
|
||||
err = r.cleanUpDefaultForwardRules(false)
|
||||
if err != nil {
|
||||
log.Errorf("failed to clean up rules from FORWARD chain: %s", err)
|
||||
}
|
||||
|
||||
err = r.createContainers()
|
||||
err = r.cleanUpDefaultForwardRules(true)
|
||||
if err != nil {
|
||||
log.Errorf("failed to clean up rules from IPv6 FORWARD chain: %s", err)
|
||||
}
|
||||
|
||||
err = r.createContainers(false)
|
||||
if err != nil {
|
||||
log.Errorf("failed to create containers for route: %s", err)
|
||||
}
|
||||
|
||||
if r.workTable6 != nil {
|
||||
err = r.createContainers(true)
|
||||
if err != nil {
|
||||
log.Errorf("failed to create v6 containers for route: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
return r, err
|
||||
}
|
||||
|
||||
@@ -90,43 +112,99 @@ func (r *router) RouteingFwChainName() string {
|
||||
|
||||
// ResetForwardRules cleans existing nftables default forward rules from the system
|
||||
func (r *router) ResetForwardRules() {
|
||||
err := r.cleanUpDefaultForwardRules()
|
||||
err := r.cleanUpDefaultForwardRules(false)
|
||||
if err != nil {
|
||||
log.Errorf("failed to reset forward rules: %s", err)
|
||||
}
|
||||
err = r.cleanUpDefaultForwardRules(true)
|
||||
if err != nil {
|
||||
log.Errorf("failed to reset forward rules: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *router) loadFilterTable() (*nftables.Table, error) {
|
||||
func (r *router) RestoreAfterV6Reset(newWorktable6 *nftables.Table) error {
|
||||
r.workTable6 = newWorktable6
|
||||
if newWorktable6 != nil {
|
||||
|
||||
err := r.cleanUpDefaultForwardRules(true)
|
||||
if err != nil {
|
||||
log.Errorf("failed to clean up rules from IPv6 FORWARD chain: %s", err)
|
||||
}
|
||||
|
||||
err = r.createContainers(true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for name, rule := range r.rules6 {
|
||||
rule = &nftables.Rule{
|
||||
Table: r.workTable6,
|
||||
Chain: r.chains6[rule.Chain.Name],
|
||||
Exprs: rule.Exprs,
|
||||
UserData: rule.UserData,
|
||||
}
|
||||
r.rules6[name] = r.conn.AddRule(rule)
|
||||
}
|
||||
}
|
||||
return r.conn.Flush()
|
||||
}
|
||||
|
||||
func (r *router) loadFilterTables() (*nftables.Table, *nftables.Table, error) {
|
||||
tables, err := r.conn.ListTablesOfFamily(nftables.TableFamilyIPv4)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("nftables: unable to list tables: %v", err)
|
||||
return nil, nil, fmt.Errorf("nftables: unable to list tables: %v", err)
|
||||
}
|
||||
|
||||
var table4 *nftables.Table = nil
|
||||
for _, table := range tables {
|
||||
if table.Name == "filter" {
|
||||
return table, nil
|
||||
table4 = table
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errFilterTableNotFound
|
||||
var table6 *nftables.Table = nil
|
||||
tables, err = r.conn.ListTablesOfFamily(nftables.TableFamilyIPv6)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("nftables: unable to list tables: %v", err)
|
||||
}
|
||||
for _, table := range tables {
|
||||
if table.Name == "filter" {
|
||||
table6 = table
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
err = nil
|
||||
if table4 == nil || table6 == nil {
|
||||
err = errFilterTableNotFound
|
||||
}
|
||||
|
||||
return table4, table6, err
|
||||
}
|
||||
|
||||
func (r *router) createContainers() error {
|
||||
func (r *router) createContainers(forV6 bool) error {
|
||||
workTable := r.workTable
|
||||
chainStorage := r.chains
|
||||
if forV6 {
|
||||
workTable = r.workTable6
|
||||
chainStorage = r.chains6
|
||||
}
|
||||
|
||||
r.chains[chainNameRouteingFw] = r.conn.AddChain(&nftables.Chain{
|
||||
chainStorage[chainNameRouteingFw] = r.conn.AddChain(&nftables.Chain{
|
||||
Name: chainNameRouteingFw,
|
||||
Table: r.workTable,
|
||||
Table: workTable,
|
||||
})
|
||||
|
||||
r.chains[chainNameRoutingNat] = r.conn.AddChain(&nftables.Chain{
|
||||
chainStorage[chainNameRoutingNat] = r.conn.AddChain(&nftables.Chain{
|
||||
Name: chainNameRoutingNat,
|
||||
Table: r.workTable,
|
||||
Table: workTable,
|
||||
Hooknum: nftables.ChainHookPostrouting,
|
||||
Priority: nftables.ChainPriorityNATSource - 1,
|
||||
Type: nftables.ChainTypeNAT,
|
||||
})
|
||||
|
||||
err := r.refreshRulesMap()
|
||||
err := r.refreshRulesMap(forV6)
|
||||
if err != nil {
|
||||
log.Errorf("failed to clean up rules from FORWARD chain: %s", err)
|
||||
}
|
||||
@@ -140,7 +218,13 @@ func (r *router) createContainers() error {
|
||||
|
||||
// InsertRoutingRules inserts a nftable rule pair to the forwarding chain and if enabled, to the nat chain
|
||||
func (r *router) InsertRoutingRules(pair manager.RouterPair) error {
|
||||
err := r.refreshRulesMap()
|
||||
parsedIp, _, _ := net.ParseCIDR(pair.Source)
|
||||
|
||||
if parsedIp.To4() == nil && r.workTable6 == nil {
|
||||
return fmt.Errorf("nftables: attempted to add IPv6 routing rule even though IPv6 is not enabled for this host")
|
||||
}
|
||||
|
||||
err := r.refreshRulesMap(parsedIp.To4() == nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -165,7 +249,11 @@ func (r *router) InsertRoutingRules(pair manager.RouterPair) error {
|
||||
}
|
||||
}
|
||||
|
||||
if r.filterTable != nil && !r.isDefaultFwdRulesEnabled {
|
||||
filterTable := r.filterTable
|
||||
if parsedIp.To4() == nil {
|
||||
filterTable = r.filterTable6
|
||||
}
|
||||
if filterTable != nil && !r.isDefaultFwdRulesEnabled {
|
||||
log.Debugf("add default accept forward rule")
|
||||
r.acceptForwardRule(pair.Source)
|
||||
}
|
||||
@@ -191,7 +279,13 @@ func (r *router) insertRoutingRule(format, chainName string, pair manager.Router
|
||||
|
||||
ruleKey := manager.GenKey(format, pair.ID)
|
||||
|
||||
_, exists := r.rules[ruleKey]
|
||||
parsedIp, _, _ := net.ParseCIDR(pair.Source)
|
||||
rules := r.rules
|
||||
if parsedIp.To4() == nil {
|
||||
rules = r.rules6
|
||||
}
|
||||
|
||||
_, exists := rules[ruleKey]
|
||||
if exists {
|
||||
err := r.removeRoutingRule(format, pair)
|
||||
if err != nil {
|
||||
@@ -199,18 +293,35 @@ func (r *router) insertRoutingRule(format, chainName string, pair manager.Router
|
||||
}
|
||||
}
|
||||
|
||||
r.rules[ruleKey] = r.conn.InsertRule(&nftables.Rule{
|
||||
Table: r.workTable,
|
||||
Chain: r.chains[chainName],
|
||||
table, chain := r.workTable, r.chains[chainName]
|
||||
if parsedIp.To4() == nil {
|
||||
table, chain = r.workTable6, r.chains6[chainName]
|
||||
}
|
||||
|
||||
newRule := r.conn.InsertRule(&nftables.Rule{
|
||||
Table: table,
|
||||
Chain: chain,
|
||||
Exprs: expression,
|
||||
UserData: []byte(ruleKey),
|
||||
})
|
||||
|
||||
if parsedIp.To4() == nil {
|
||||
r.rules[ruleKey] = newRule
|
||||
} else {
|
||||
r.rules6[ruleKey] = newRule
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *router) acceptForwardRule(sourceNetwork string) {
|
||||
src := generateCIDRMatcherExpressions(true, sourceNetwork)
|
||||
dst := generateCIDRMatcherExpressions(false, "0.0.0.0/0")
|
||||
table := r.filterTable
|
||||
parsedIp, _, _ := net.ParseCIDR(sourceNetwork)
|
||||
if parsedIp.To4() == nil {
|
||||
dst = generateCIDRMatcherExpressions(false, "::/0")
|
||||
table = r.filterTable6
|
||||
}
|
||||
|
||||
var exprs []expr.Any
|
||||
exprs = append(src, append(dst, &expr.Verdict{ // nolint:gocritic
|
||||
@@ -218,10 +329,10 @@ func (r *router) acceptForwardRule(sourceNetwork string) {
|
||||
})...)
|
||||
|
||||
rule := &nftables.Rule{
|
||||
Table: r.filterTable,
|
||||
Table: table,
|
||||
Chain: &nftables.Chain{
|
||||
Name: "FORWARD",
|
||||
Table: r.filterTable,
|
||||
Table: table,
|
||||
Type: nftables.ChainTypeFilter,
|
||||
Hooknum: nftables.ChainHookForward,
|
||||
Priority: nftables.ChainPriorityFilter,
|
||||
@@ -233,6 +344,9 @@ func (r *router) acceptForwardRule(sourceNetwork string) {
|
||||
r.conn.AddRule(rule)
|
||||
|
||||
src = generateCIDRMatcherExpressions(true, "0.0.0.0/0")
|
||||
if parsedIp.To4() == nil {
|
||||
src = generateCIDRMatcherExpressions(true, "::/0")
|
||||
}
|
||||
dst = generateCIDRMatcherExpressions(false, sourceNetwork)
|
||||
|
||||
exprs = append(src, append(dst, &expr.Verdict{ //nolint:gocritic
|
||||
@@ -240,10 +354,10 @@ func (r *router) acceptForwardRule(sourceNetwork string) {
|
||||
})...)
|
||||
|
||||
rule = &nftables.Rule{
|
||||
Table: r.filterTable,
|
||||
Table: table,
|
||||
Chain: &nftables.Chain{
|
||||
Name: "FORWARD",
|
||||
Table: r.filterTable,
|
||||
Table: table,
|
||||
Type: nftables.ChainTypeFilter,
|
||||
Hooknum: nftables.ChainHookForward,
|
||||
Priority: nftables.ChainPriorityFilter,
|
||||
@@ -252,12 +366,21 @@ func (r *router) acceptForwardRule(sourceNetwork string) {
|
||||
UserData: []byte(userDataAcceptForwardRuleDst),
|
||||
}
|
||||
r.conn.AddRule(rule)
|
||||
r.isDefaultFwdRulesEnabled = true
|
||||
if parsedIp.To4() == nil {
|
||||
r.isDefaultFwdRulesEnabled6 = true
|
||||
} else {
|
||||
r.isDefaultFwdRulesEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveRoutingRules removes a nftable rule pair from forwarding and nat chains
|
||||
func (r *router) RemoveRoutingRules(pair manager.RouterPair) error {
|
||||
err := r.refreshRulesMap()
|
||||
parsedIp, _, _ := net.ParseCIDR(pair.Source)
|
||||
if parsedIp.To4() == nil && r.workTable6 == nil {
|
||||
return fmt.Errorf("nftables: attempted to remove IPv6 routing rule even though IPv6 is not enabled for this host")
|
||||
}
|
||||
|
||||
err := r.refreshRulesMap(parsedIp.To4() == nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -282,8 +405,12 @@ func (r *router) RemoveRoutingRules(pair manager.RouterPair) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(r.rules) == 0 {
|
||||
err := r.cleanUpDefaultForwardRules()
|
||||
rulesList := r.rules
|
||||
if parsedIp.To4() == nil {
|
||||
rulesList = r.rules6
|
||||
}
|
||||
if len(rulesList) == 0 {
|
||||
err := r.cleanUpDefaultForwardRules(parsedIp.To4() == nil)
|
||||
if err != nil {
|
||||
log.Errorf("failed to clean up rules from FORWARD chain: %s", err)
|
||||
}
|
||||
@@ -301,7 +428,13 @@ func (r *router) RemoveRoutingRules(pair manager.RouterPair) error {
|
||||
func (r *router) removeRoutingRule(format string, pair manager.RouterPair) error {
|
||||
ruleKey := manager.GenKey(format, pair.ID)
|
||||
|
||||
rule, found := r.rules[ruleKey]
|
||||
parsedIp, _, _ := net.ParseCIDR(pair.Source)
|
||||
rules := r.rules
|
||||
if parsedIp.To4() == nil {
|
||||
rules = r.rules6
|
||||
}
|
||||
|
||||
rule, found := rules[ruleKey]
|
||||
if found {
|
||||
ruleType := "forwarding"
|
||||
if rule.Chain.Type == nftables.ChainTypeNAT {
|
||||
@@ -315,49 +448,68 @@ func (r *router) removeRoutingRule(format string, pair manager.RouterPair) error
|
||||
|
||||
log.Debugf("nftables: removing %s rule for %s", ruleType, pair.Destination)
|
||||
|
||||
delete(r.rules, ruleKey)
|
||||
delete(rules, ruleKey)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// refreshRulesMap refreshes the rule map with the latest rules. this is useful to avoid
|
||||
// duplicates and to get missing attributes that we don't have when adding new rules
|
||||
func (r *router) refreshRulesMap() error {
|
||||
for _, chain := range r.chains {
|
||||
func (r *router) refreshRulesMap(forV6 bool) error {
|
||||
chainList := r.chains
|
||||
if forV6 {
|
||||
chainList = r.chains6
|
||||
}
|
||||
for _, chain := range chainList {
|
||||
rules, err := r.conn.GetRules(chain.Table, chain)
|
||||
if err != nil {
|
||||
return fmt.Errorf("nftables: unable to list rules: %v", err)
|
||||
}
|
||||
for _, rule := range rules {
|
||||
if len(rule.UserData) > 0 {
|
||||
r.rules[string(rule.UserData)] = rule
|
||||
if forV6 {
|
||||
r.rules6[string(rule.UserData)] = rule
|
||||
} else {
|
||||
r.rules[string(rule.UserData)] = rule
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *router) cleanUpDefaultForwardRules() error {
|
||||
if r.filterTable == nil {
|
||||
r.isDefaultFwdRulesEnabled = false
|
||||
func (r *router) cleanUpDefaultForwardRules(forV6 bool) error {
|
||||
tableFamily := nftables.TableFamilyIPv4
|
||||
filterTable := r.filterTable
|
||||
if forV6 {
|
||||
tableFamily = nftables.TableFamilyIPv6
|
||||
filterTable = r.filterTable6
|
||||
}
|
||||
|
||||
if filterTable == nil {
|
||||
if forV6 {
|
||||
r.isDefaultFwdRulesEnabled6 = false
|
||||
} else {
|
||||
r.isDefaultFwdRulesEnabled = false
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
chains, err := r.conn.ListChainsOfTableFamily(nftables.TableFamilyIPv4)
|
||||
chains, err := r.conn.ListChainsOfTableFamily(tableFamily)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var rules []*nftables.Rule
|
||||
for _, chain := range chains {
|
||||
if chain.Table.Name != r.filterTable.Name {
|
||||
if chain.Table.Name != filterTable.Name {
|
||||
continue
|
||||
}
|
||||
if chain.Name != "FORWARD" {
|
||||
continue
|
||||
}
|
||||
|
||||
rules, err = r.conn.GetRules(r.filterTable, chain)
|
||||
rules, err = r.conn.GetRules(filterTable, chain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -371,7 +523,12 @@ func (r *router) cleanUpDefaultForwardRules() error {
|
||||
}
|
||||
}
|
||||
}
|
||||
r.isDefaultFwdRulesEnabled = false
|
||||
|
||||
if forV6 {
|
||||
r.isDefaultFwdRulesEnabled6 = false
|
||||
} else {
|
||||
r.isDefaultFwdRulesEnabled = false
|
||||
}
|
||||
return r.conn.Flush()
|
||||
}
|
||||
|
||||
@@ -387,6 +544,18 @@ func generateCIDRMatcherExpressions(source bool, cidr string) []expr.Any {
|
||||
} else {
|
||||
offSet = 16 // dst offset
|
||||
}
|
||||
addrLen := uint32(4)
|
||||
zeroXor := zeroXor
|
||||
|
||||
if ip.To4() == nil {
|
||||
if source {
|
||||
offSet = 8 // src offset
|
||||
} else {
|
||||
offSet = 24 // dst offset
|
||||
}
|
||||
addrLen = 16
|
||||
zeroXor = zeroXor6
|
||||
}
|
||||
|
||||
return []expr.Any{
|
||||
// fetch src add
|
||||
@@ -394,13 +563,13 @@ func generateCIDRMatcherExpressions(source bool, cidr string) []expr.Any {
|
||||
DestRegister: 1,
|
||||
Base: expr.PayloadBaseNetworkHeader,
|
||||
Offset: offSet,
|
||||
Len: 4,
|
||||
Len: addrLen,
|
||||
},
|
||||
// net mask
|
||||
&expr.Bitwise{
|
||||
DestRegister: 1,
|
||||
SourceRegister: 1,
|
||||
Len: 4,
|
||||
Len: addrLen,
|
||||
Mask: network.Mask,
|
||||
Xor: zeroXor,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user