mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-31 21:19:55 +00:00
Compare commits
3 Commits
worktree-a
...
fix/ios-de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3352c8402 | ||
|
|
557b611b02 | ||
|
|
485fa06c94 |
@@ -1,199 +0,0 @@
|
|||||||
package iptables
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/netip"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
|
|
||||||
fw "github.com/netbirdio/netbird/client/firewall/manager"
|
|
||||||
"github.com/netbirdio/netbird/client/iface"
|
|
||||||
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
|
||||||
)
|
|
||||||
|
|
||||||
func iptRefcountIfaceV4() *iFaceMock {
|
|
||||||
return &iFaceMock{
|
|
||||||
NameFunc: func() string { return "wt-refcount" },
|
|
||||||
AddressFunc: func() wgaddr.Address {
|
|
||||||
return wgaddr.Address{
|
|
||||||
IP: netip.MustParseAddr("10.20.0.1"),
|
|
||||||
Network: netip.MustParsePrefix("10.20.0.0/24"),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func iptRefcountIfaceDual() *iFaceMock {
|
|
||||||
return &iFaceMock{
|
|
||||||
NameFunc: func() string { return "wt-refcount" },
|
|
||||||
AddressFunc: func() wgaddr.Address {
|
|
||||||
return wgaddr.Address{
|
|
||||||
IP: netip.MustParseAddr("10.20.0.1"),
|
|
||||||
Network: netip.MustParsePrefix("10.20.0.0/24"),
|
|
||||||
IPv6: netip.MustParseAddr("fd00::1"),
|
|
||||||
IPv6Net: netip.MustParsePrefix("fd00::/64"),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newIptRefcountManager(t *testing.T, dual bool) *Manager {
|
|
||||||
t.Helper()
|
|
||||||
var ifMock *iFaceMock
|
|
||||||
if dual {
|
|
||||||
ifMock = iptRefcountIfaceDual()
|
|
||||||
} else {
|
|
||||||
ifMock = iptRefcountIfaceV4()
|
|
||||||
}
|
|
||||||
m, err := Create(ifMock, iface.DefaultMTU)
|
|
||||||
require.NoError(t, err, "create manager")
|
|
||||||
require.NoError(t, m.Init(nil), "init manager")
|
|
||||||
t.Cleanup(func() {
|
|
||||||
require.NoError(t, m.Close(nil), "close manager")
|
|
||||||
})
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
|
|
||||||
func iptDnatV4(port uint16) fw.ForwardRule {
|
|
||||||
return fw.ForwardRule{
|
|
||||||
Protocol: fw.ProtocolTCP,
|
|
||||||
DestinationPort: fw.Port{Values: []uint16{port}},
|
|
||||||
TranslatedAddress: netip.MustParseAddr("10.20.0.2"),
|
|
||||||
TranslatedPort: fw.Port{Values: []uint16{80}},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func iptDnatV6(port uint16) fw.ForwardRule {
|
|
||||||
return fw.ForwardRule{
|
|
||||||
Protocol: fw.ProtocolTCP,
|
|
||||||
DestinationPort: fw.Port{Values: []uint16{port}},
|
|
||||||
TranslatedAddress: netip.MustParseAddr("fd00::2"),
|
|
||||||
TranslatedPort: fw.Port{Values: []uint16{80}},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestIptablesDNAT_RefcountBalancedV4 covers a Balanced Add/Delete pair on v4.
|
|
||||||
func TestIptablesDNAT_RefcountBalancedV4(t *testing.T) {
|
|
||||||
m := newIptRefcountManager(t, false)
|
|
||||||
state := m.router.ipFwdState
|
|
||||||
|
|
||||||
r1, err := m.AddDNATRule(iptDnatV4(7081))
|
|
||||||
require.NoError(t, err, "add v4 dnat 1")
|
|
||||||
v4, v6 := state.Counts()
|
|
||||||
require.Equal(t, 1, v4, "v4 refcount after first add")
|
|
||||||
require.Equal(t, 0, v6, "v6 refcount unchanged")
|
|
||||||
|
|
||||||
r2, err := m.AddDNATRule(iptDnatV4(7082))
|
|
||||||
require.NoError(t, err, "add v4 dnat 2")
|
|
||||||
v4, v6 = state.Counts()
|
|
||||||
require.Equal(t, 2, v4, "v4 refcount after second add")
|
|
||||||
require.Equal(t, 0, v6, "v6 refcount unchanged")
|
|
||||||
|
|
||||||
require.NoError(t, m.DeleteDNATRule(r1))
|
|
||||||
v4, v6 = state.Counts()
|
|
||||||
require.Equal(t, 1, v4, "v4 refcount after first delete")
|
|
||||||
require.Equal(t, 0, v6, "v6 refcount unchanged")
|
|
||||||
|
|
||||||
require.NoError(t, m.DeleteDNATRule(r2))
|
|
||||||
v4, v6 = state.Counts()
|
|
||||||
require.Equal(t, 0, v4, "v4 refcount after second delete")
|
|
||||||
require.Equal(t, 0, v6, "v6 refcount unchanged")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestIptablesDNAT_RefcountBalancedV6 checks the v6 path increments v6 only and
|
|
||||||
// decrements back to zero.
|
|
||||||
func TestIptablesDNAT_RefcountBalancedV6(t *testing.T) {
|
|
||||||
m := newIptRefcountManager(t, true)
|
|
||||||
require.NotNil(t, m.router6, "v6 router")
|
|
||||||
require.Same(t, m.router.ipFwdState, m.router6.ipFwdState, "shared state")
|
|
||||||
state := m.router.ipFwdState
|
|
||||||
|
|
||||||
r1, err := m.AddDNATRule(iptDnatV6(9081))
|
|
||||||
require.NoError(t, err, "add v6 dnat 1")
|
|
||||||
v4, v6 := state.Counts()
|
|
||||||
require.Equal(t, 0, v4)
|
|
||||||
require.Equal(t, 1, v6, "v6 refcount after first add")
|
|
||||||
|
|
||||||
r2, err := m.AddDNATRule(iptDnatV6(9082))
|
|
||||||
require.NoError(t, err, "add v6 dnat 2")
|
|
||||||
v4, v6 = state.Counts()
|
|
||||||
require.Equal(t, 0, v4, "v4 refcount unchanged")
|
|
||||||
require.Equal(t, 2, v6, "v6 refcount after second add")
|
|
||||||
|
|
||||||
require.NoError(t, m.DeleteDNATRule(r1))
|
|
||||||
v4, v6 = state.Counts()
|
|
||||||
require.Equal(t, 0, v4, "v4 refcount unchanged")
|
|
||||||
require.Equal(t, 1, v6, "v6 refcount after first delete")
|
|
||||||
|
|
||||||
require.NoError(t, m.DeleteDNATRule(r2))
|
|
||||||
v4, v6 = state.Counts()
|
|
||||||
require.Equal(t, 0, v4)
|
|
||||||
require.Equal(t, 0, v6, "v6 refcount after second delete")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestIptablesDNAT_DuplicateAddNoLeak verifies the duplicate-rule path returns
|
|
||||||
// without bumping the refcount.
|
|
||||||
func TestIptablesDNAT_DuplicateAddNoLeak(t *testing.T) {
|
|
||||||
m := newIptRefcountManager(t, true)
|
|
||||||
state := m.router.ipFwdState
|
|
||||||
|
|
||||||
rule := iptDnatV4(7083)
|
|
||||||
r1, err := m.AddDNATRule(rule)
|
|
||||||
require.NoError(t, err)
|
|
||||||
v4, _ := state.Counts()
|
|
||||||
require.Equal(t, 1, v4)
|
|
||||||
|
|
||||||
_, err = m.AddDNATRule(rule)
|
|
||||||
require.NoError(t, err, "duplicate add")
|
|
||||||
v4, _ = state.Counts()
|
|
||||||
require.Equal(t, 1, v4, "duplicate add must not increment")
|
|
||||||
|
|
||||||
require.NoError(t, m.DeleteDNATRule(r1))
|
|
||||||
v4, _ = state.Counts()
|
|
||||||
require.Equal(t, 0, v4, "single delete must drop to zero")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestIptablesDNAT_DeleteMissingNoUnderflow verifies Delete on an unknown rule
|
|
||||||
// neither errors nor releases the refcount.
|
|
||||||
func TestIptablesDNAT_DeleteMissingNoUnderflow(t *testing.T) {
|
|
||||||
m := newIptRefcountManager(t, true)
|
|
||||||
state := m.router.ipFwdState
|
|
||||||
|
|
||||||
phantom := iptDnatV4(7099)
|
|
||||||
require.NoError(t, m.DeleteDNATRule(&phantom), "delete missing v4")
|
|
||||||
v4, v6 := state.Counts()
|
|
||||||
require.Equal(t, 0, v4)
|
|
||||||
require.Equal(t, 0, v6)
|
|
||||||
|
|
||||||
phantom6 := iptDnatV6(9099)
|
|
||||||
require.NoError(t, m.DeleteDNATRule(&phantom6), "delete missing v6")
|
|
||||||
v4, v6 = state.Counts()
|
|
||||||
require.Equal(t, 0, v4)
|
|
||||||
require.Equal(t, 0, v6)
|
|
||||||
|
|
||||||
r1, err := m.AddDNATRule(iptDnatV4(7100))
|
|
||||||
require.NoError(t, err)
|
|
||||||
v4, _ = state.Counts()
|
|
||||||
require.Equal(t, 1, v4, "real add still increments after phantom delete")
|
|
||||||
require.NoError(t, m.DeleteDNATRule(r1))
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestIptablesDNAT_DoubleDeleteNoUnderflow verifies a second Delete on the same
|
|
||||||
// rule is a no-op.
|
|
||||||
func TestIptablesDNAT_DoubleDeleteNoUnderflow(t *testing.T) {
|
|
||||||
m := newIptRefcountManager(t, true)
|
|
||||||
state := m.router.ipFwdState
|
|
||||||
|
|
||||||
r1, err := m.AddDNATRule(iptDnatV6(9083))
|
|
||||||
require.NoError(t, err)
|
|
||||||
_, v6 := state.Counts()
|
|
||||||
require.Equal(t, 1, v6)
|
|
||||||
|
|
||||||
require.NoError(t, m.DeleteDNATRule(r1), "first delete")
|
|
||||||
_, v6 = state.Counts()
|
|
||||||
require.Equal(t, 0, v6)
|
|
||||||
|
|
||||||
require.NoError(t, m.DeleteDNATRule(r1), "second delete must be no-op")
|
|
||||||
_, v6 = state.Counts()
|
|
||||||
require.Equal(t, 0, v6, "double delete must not underflow")
|
|
||||||
}
|
|
||||||
@@ -89,7 +89,7 @@ func (m *Manager) createIPv6Components(wgIface iFaceMapper, mtu uint16) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Share the same IP forwarding state with the v4 router, since
|
// Share the same IP forwarding state with the v4 router, since
|
||||||
// Forwarding refcounter is per-family but shared between v4 and v6 routers.
|
// EnableIPForwarding controls both v4 and v6 sysctls.
|
||||||
m.router6.ipFwdState = m.router.ipFwdState
|
m.router6.ipFwdState = m.router.ipFwdState
|
||||||
|
|
||||||
m.aclMgr6, err = newAclManager(ip6Client, wgIface)
|
m.aclMgr6, err = newAclManager(ip6Client, wgIface)
|
||||||
@@ -402,33 +402,17 @@ func (m *Manager) SetLogLevel(log.Level) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) EnableRouting() error {
|
func (m *Manager) EnableRouting() error {
|
||||||
if err := m.router.ipFwdState.RequestForwarding(false); err != nil {
|
if err := m.router.ipFwdState.RequestForwarding(); err != nil {
|
||||||
return fmt.Errorf("enable IPv4 forwarding: %w", err)
|
return fmt.Errorf("enable IP forwarding: %w", err)
|
||||||
}
|
|
||||||
// v6 only when the overlay actually has v6.
|
|
||||||
if m.router6 == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if err := m.router.ipFwdState.RequestForwarding(true); err != nil {
|
|
||||||
if rerr := m.router.ipFwdState.ReleaseForwarding(false); rerr != nil {
|
|
||||||
log.Warnf("rollback v4 forwarding: %v", rerr)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("enable IPv6 forwarding: %w", err)
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) DisableRouting() error {
|
func (m *Manager) DisableRouting() error {
|
||||||
var merr *multierror.Error
|
if err := m.router.ipFwdState.ReleaseForwarding(); err != nil {
|
||||||
if err := m.router.ipFwdState.ReleaseForwarding(false); err != nil {
|
return fmt.Errorf("disable IP forwarding: %w", err)
|
||||||
merr = multierror.Append(merr, fmt.Errorf("disable IPv4 forwarding: %w", err))
|
|
||||||
}
|
}
|
||||||
if m.router6 != nil {
|
return nil
|
||||||
if err := m.router.ipFwdState.ReleaseForwarding(true); err != nil {
|
|
||||||
merr = multierror.Append(merr, fmt.Errorf("disable IPv6 forwarding: %w", err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nberrors.FormatErrorOrNil(merr)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddDNATRule adds a DNAT rule
|
// AddDNATRule adds a DNAT rule
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ func newRouter(iptablesClient *iptables.IPTables, wgIface iFaceMapper, mtu uint1
|
|||||||
wgIface: wgIface,
|
wgIface: wgIface,
|
||||||
mtu: mtu,
|
mtu: mtu,
|
||||||
v6: iptablesClient.Proto() == iptables.ProtocolIPv6,
|
v6: iptablesClient.Proto() == iptables.ProtocolIPv6,
|
||||||
ipFwdState: ipfwdstate.NewIPForwardingState(wgIface.Name()),
|
ipFwdState: ipfwdstate.NewIPForwardingState(),
|
||||||
}
|
}
|
||||||
|
|
||||||
r.ipsetCounter = refcounter.New(
|
r.ipsetCounter = refcounter.New(
|
||||||
@@ -763,6 +763,10 @@ func (r *router) updateState() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *router) AddDNATRule(rule firewall.ForwardRule) (firewall.Rule, error) {
|
func (r *router) AddDNATRule(rule firewall.ForwardRule) (firewall.Rule, error) {
|
||||||
|
if err := r.ipFwdState.RequestForwarding(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
ruleKey := rule.ID()
|
ruleKey := rule.ID()
|
||||||
if _, exists := r.rules[ruleKey+dnatSuffix]; exists {
|
if _, exists := r.rules[ruleKey+dnatSuffix]; exists {
|
||||||
return rule, nil
|
return rule, nil
|
||||||
@@ -837,16 +841,6 @@ func (r *router) AddDNATRule(rule firewall.ForwardRule) (firewall.Rule, error) {
|
|||||||
r.rules[key] = ruleInfo.rule
|
r.rules[key] = ruleInfo.rule
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := r.ipFwdState.RequestForwarding(r.v6); err != nil {
|
|
||||||
if rollbackErr := r.rollbackRules(rules); rollbackErr != nil {
|
|
||||||
log.Errorf("rollback failed: %v", rollbackErr)
|
|
||||||
}
|
|
||||||
for key := range rules {
|
|
||||||
delete(r.rules, key)
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("enable forwarding: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
r.updateState()
|
r.updateState()
|
||||||
return rule, nil
|
return rule, nil
|
||||||
}
|
}
|
||||||
@@ -867,15 +861,12 @@ func (r *router) rollbackRules(rules map[string]ruleInfo) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *router) DeleteDNATRule(rule firewall.Rule) error {
|
func (r *router) DeleteDNATRule(rule firewall.Rule) error {
|
||||||
ruleKey := rule.ID()
|
if err := r.ipFwdState.ReleaseForwarding(); err != nil {
|
||||||
|
log.Errorf("%v", err)
|
||||||
_, hadDNAT := r.rules[ruleKey+dnatSuffix]
|
|
||||||
_, hadSNAT := r.rules[ruleKey+snatSuffix]
|
|
||||||
_, hadFWD := r.rules[ruleKey+fwdSuffix]
|
|
||||||
if !hadDNAT && !hadSNAT && !hadFWD {
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ruleKey := rule.ID()
|
||||||
|
|
||||||
var merr *multierror.Error
|
var merr *multierror.Error
|
||||||
if dnatRule, exists := r.rules[ruleKey+dnatSuffix]; exists {
|
if dnatRule, exists := r.rules[ruleKey+dnatSuffix]; exists {
|
||||||
if err := r.iptablesClient.Delete(tableNat, chainRTRDR, dnatRule...); err != nil {
|
if err := r.iptablesClient.Delete(tableNat, chainRTRDR, dnatRule...); err != nil {
|
||||||
@@ -898,10 +889,6 @@ func (r *router) DeleteDNATRule(rule firewall.Rule) error {
|
|||||||
delete(r.rules, ruleKey+fwdSuffix)
|
delete(r.rules, ruleKey+fwdSuffix)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := r.ipFwdState.ReleaseForwarding(r.v6); err != nil {
|
|
||||||
log.Errorf("%v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
r.updateState()
|
r.updateState()
|
||||||
return nberrors.FormatErrorOrNil(merr)
|
return nberrors.FormatErrorOrNil(merr)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,208 +0,0 @@
|
|||||||
package nftables
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/netip"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
|
|
||||||
fw "github.com/netbirdio/netbird/client/firewall/manager"
|
|
||||||
"github.com/netbirdio/netbird/client/iface"
|
|
||||||
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
|
||||||
)
|
|
||||||
|
|
||||||
func nftRefcountIfaceV4() *iFaceMock {
|
|
||||||
return &iFaceMock{
|
|
||||||
NameFunc: func() string { return "wt-refcount" },
|
|
||||||
AddressFunc: func() wgaddr.Address {
|
|
||||||
return wgaddr.Address{
|
|
||||||
IP: netip.MustParseAddr("100.96.0.1"),
|
|
||||||
Network: netip.MustParsePrefix("100.96.0.0/16"),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func nftRefcountIfaceDual() *iFaceMock {
|
|
||||||
return &iFaceMock{
|
|
||||||
NameFunc: func() string { return "wt-refcount" },
|
|
||||||
AddressFunc: func() wgaddr.Address {
|
|
||||||
return wgaddr.Address{
|
|
||||||
IP: netip.MustParseAddr("100.96.0.1"),
|
|
||||||
Network: netip.MustParsePrefix("100.96.0.0/16"),
|
|
||||||
IPv6: netip.MustParseAddr("fd00::1"),
|
|
||||||
IPv6Net: netip.MustParsePrefix("fd00::/64"),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newNftRefcountManager(t *testing.T, dual bool) *Manager {
|
|
||||||
t.Helper()
|
|
||||||
if check() != NFTABLES {
|
|
||||||
t.Skip("nftables not supported on this system")
|
|
||||||
}
|
|
||||||
var ifMock *iFaceMock
|
|
||||||
if dual {
|
|
||||||
ifMock = nftRefcountIfaceDual()
|
|
||||||
} else {
|
|
||||||
ifMock = nftRefcountIfaceV4()
|
|
||||||
}
|
|
||||||
m, err := Create(ifMock, iface.DefaultMTU)
|
|
||||||
require.NoError(t, err, "create manager")
|
|
||||||
require.NoError(t, m.Init(nil), "init manager")
|
|
||||||
t.Cleanup(func() {
|
|
||||||
require.NoError(t, m.Close(nil), "close manager")
|
|
||||||
})
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
|
|
||||||
func dnatV4(port uint16) fw.ForwardRule {
|
|
||||||
return fw.ForwardRule{
|
|
||||||
Protocol: fw.ProtocolTCP,
|
|
||||||
DestinationPort: fw.Port{Values: []uint16{port}},
|
|
||||||
TranslatedAddress: netip.MustParseAddr("100.96.0.2"),
|
|
||||||
TranslatedPort: fw.Port{Values: []uint16{80}},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func dnatV6(port uint16) fw.ForwardRule {
|
|
||||||
return fw.ForwardRule{
|
|
||||||
Protocol: fw.ProtocolTCP,
|
|
||||||
DestinationPort: fw.Port{Values: []uint16{port}},
|
|
||||||
TranslatedAddress: netip.MustParseAddr("fd00::2"),
|
|
||||||
TranslatedPort: fw.Port{Values: []uint16{80}},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestNftablesDNAT_RefcountBalancedV4 verifies that Add/Delete pairs leave the
|
|
||||||
// v4 refcount at zero.
|
|
||||||
func TestNftablesDNAT_RefcountBalancedV4(t *testing.T) {
|
|
||||||
m := newNftRefcountManager(t, false)
|
|
||||||
state := m.router.ipFwdState
|
|
||||||
|
|
||||||
r1, err := m.AddDNATRule(dnatV4(8081))
|
|
||||||
require.NoError(t, err, "add v4 dnat 1")
|
|
||||||
v4, v6 := state.Counts()
|
|
||||||
require.Equal(t, 1, v4, "v4 refcount after first add")
|
|
||||||
require.Equal(t, 0, v6, "v6 refcount unchanged")
|
|
||||||
|
|
||||||
r2, err := m.AddDNATRule(dnatV4(8082))
|
|
||||||
require.NoError(t, err, "add v4 dnat 2")
|
|
||||||
v4, v6 = state.Counts()
|
|
||||||
require.Equal(t, 2, v4, "v4 refcount after second add")
|
|
||||||
require.Equal(t, 0, v6, "v6 refcount unchanged")
|
|
||||||
|
|
||||||
require.NoError(t, m.DeleteDNATRule(r1), "delete v4 dnat 1")
|
|
||||||
v4, v6 = state.Counts()
|
|
||||||
require.Equal(t, 1, v4, "v4 refcount after first delete")
|
|
||||||
require.Equal(t, 0, v6, "v6 refcount unchanged")
|
|
||||||
|
|
||||||
require.NoError(t, m.DeleteDNATRule(r2), "delete v4 dnat 2")
|
|
||||||
v4, v6 = state.Counts()
|
|
||||||
require.Equal(t, 0, v4, "v4 refcount after second delete")
|
|
||||||
require.Equal(t, 0, v6, "v6 refcount unchanged")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestNftablesDNAT_RefcountBalancedV6 verifies the v6 path increments v6 only
|
|
||||||
// and decrements back to zero on Delete.
|
|
||||||
func TestNftablesDNAT_RefcountBalancedV6(t *testing.T) {
|
|
||||||
m := newNftRefcountManager(t, true)
|
|
||||||
require.NotNil(t, m.router6, "v6 router")
|
|
||||||
require.Same(t, m.router.ipFwdState, m.router6.ipFwdState, "shared state")
|
|
||||||
state := m.router.ipFwdState
|
|
||||||
|
|
||||||
r1, err := m.AddDNATRule(dnatV6(9091))
|
|
||||||
require.NoError(t, err, "add v6 dnat 1")
|
|
||||||
v4, v6 := state.Counts()
|
|
||||||
require.Equal(t, 0, v4, "v4 refcount unchanged")
|
|
||||||
require.Equal(t, 1, v6, "v6 refcount after first add")
|
|
||||||
|
|
||||||
r2, err := m.AddDNATRule(dnatV6(9092))
|
|
||||||
require.NoError(t, err, "add v6 dnat 2")
|
|
||||||
v4, v6 = state.Counts()
|
|
||||||
require.Equal(t, 0, v4)
|
|
||||||
require.Equal(t, 2, v6, "v6 refcount after second add")
|
|
||||||
|
|
||||||
require.NoError(t, m.DeleteDNATRule(r1), "delete v6 dnat 1")
|
|
||||||
v4, v6 = state.Counts()
|
|
||||||
require.Equal(t, 0, v4, "v4 refcount unchanged")
|
|
||||||
require.Equal(t, 1, v6, "v6 refcount after first delete")
|
|
||||||
|
|
||||||
require.NoError(t, m.DeleteDNATRule(r2), "delete v6 dnat 2")
|
|
||||||
v4, v6 = state.Counts()
|
|
||||||
require.Equal(t, 0, v4)
|
|
||||||
require.Equal(t, 0, v6, "v6 refcount after second delete")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestNftablesDNAT_DuplicateAddNoLeak verifies that a duplicate Add (same
|
|
||||||
// ForwardRule) does not double-increment the refcount.
|
|
||||||
func TestNftablesDNAT_DuplicateAddNoLeak(t *testing.T) {
|
|
||||||
m := newNftRefcountManager(t, true)
|
|
||||||
state := m.router.ipFwdState
|
|
||||||
|
|
||||||
rule := dnatV4(8083)
|
|
||||||
r1, err := m.AddDNATRule(rule)
|
|
||||||
require.NoError(t, err, "add v4 dnat")
|
|
||||||
v4, _ := state.Counts()
|
|
||||||
require.Equal(t, 1, v4)
|
|
||||||
|
|
||||||
// duplicate add: same rule ID, must be a no-op for the refcount.
|
|
||||||
_, err = m.AddDNATRule(rule)
|
|
||||||
require.NoError(t, err, "duplicate add")
|
|
||||||
v4, _ = state.Counts()
|
|
||||||
require.Equal(t, 1, v4, "duplicate add must not increment")
|
|
||||||
|
|
||||||
require.NoError(t, m.DeleteDNATRule(r1), "delete v4 dnat")
|
|
||||||
v4, _ = state.Counts()
|
|
||||||
require.Equal(t, 0, v4, "single delete must drop to zero")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestNftablesDNAT_DeleteMissingNoUnderflow verifies deleting a rule that was
|
|
||||||
// never added does not underflow the refcount.
|
|
||||||
func TestNftablesDNAT_DeleteMissingNoUnderflow(t *testing.T) {
|
|
||||||
m := newNftRefcountManager(t, true)
|
|
||||||
state := m.router.ipFwdState
|
|
||||||
|
|
||||||
// Construct a Rule reference for something never added. The router stores
|
|
||||||
// rules by ID(), and DeleteDNATRule looks them up in r.rules; a missing
|
|
||||||
// entry must be a no-op rather than calling Release.
|
|
||||||
phantom := dnatV4(8099)
|
|
||||||
require.NoError(t, m.DeleteDNATRule(&phantom), "delete missing v4 dnat")
|
|
||||||
v4, v6 := state.Counts()
|
|
||||||
require.Equal(t, 0, v4, "v4 refcount unaffected by missing delete")
|
|
||||||
require.Equal(t, 0, v6, "v6 refcount unaffected")
|
|
||||||
|
|
||||||
phantom6 := dnatV6(9099)
|
|
||||||
require.NoError(t, m.DeleteDNATRule(&phantom6), "delete missing v6 dnat")
|
|
||||||
v4, v6 = state.Counts()
|
|
||||||
require.Equal(t, 0, v4)
|
|
||||||
require.Equal(t, 0, v6, "v6 refcount unaffected by missing delete")
|
|
||||||
|
|
||||||
// And after a phantom delete, a real add still results in count=1.
|
|
||||||
r1, err := m.AddDNATRule(dnatV4(8100))
|
|
||||||
require.NoError(t, err, "add v4 dnat after phantom delete")
|
|
||||||
v4, _ = state.Counts()
|
|
||||||
require.Equal(t, 1, v4, "real add still increments after phantom delete")
|
|
||||||
require.NoError(t, m.DeleteDNATRule(r1))
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestNftablesDNAT_DoubleDeleteNoUnderflow verifies that deleting the same rule
|
|
||||||
// twice does not underflow the refcount (the second delete is a no-op).
|
|
||||||
func TestNftablesDNAT_DoubleDeleteNoUnderflow(t *testing.T) {
|
|
||||||
m := newNftRefcountManager(t, true)
|
|
||||||
state := m.router.ipFwdState
|
|
||||||
|
|
||||||
r1, err := m.AddDNATRule(dnatV6(9093))
|
|
||||||
require.NoError(t, err)
|
|
||||||
_, v6 := state.Counts()
|
|
||||||
require.Equal(t, 1, v6)
|
|
||||||
|
|
||||||
require.NoError(t, m.DeleteDNATRule(r1), "first delete")
|
|
||||||
_, v6 = state.Counts()
|
|
||||||
require.Equal(t, 0, v6)
|
|
||||||
|
|
||||||
require.NoError(t, m.DeleteDNATRule(r1), "second delete must be no-op")
|
|
||||||
_, v6 = state.Counts()
|
|
||||||
require.Equal(t, 0, v6, "double delete must not underflow")
|
|
||||||
}
|
|
||||||
@@ -105,8 +105,8 @@ func (m *Manager) createIPv6Components(tableName string, wgIface iFaceMapper, mt
|
|||||||
return fmt.Errorf("create v6 router: %w", err)
|
return fmt.Errorf("create v6 router: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Share the per-family forwarding refcounter with the v4 router so a v4
|
// Share the same IP forwarding state with the v4 router, since
|
||||||
// rule and a v6 rule against the same state machine cooperate cleanly.
|
// EnableIPForwarding controls both v4 and v6 sysctls.
|
||||||
m.router6.ipFwdState = m.router.ipFwdState
|
m.router6.ipFwdState = m.router.ipFwdState
|
||||||
|
|
||||||
m.aclManager6, err = newAclManager(workTable6, wgIface, chainNameRoutingFw)
|
m.aclManager6, err = newAclManager(workTable6, wgIface, chainNameRoutingFw)
|
||||||
@@ -530,33 +530,17 @@ func (m *Manager) SetLogLevel(log.Level) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) EnableRouting() error {
|
func (m *Manager) EnableRouting() error {
|
||||||
if err := m.router.ipFwdState.RequestForwarding(false); err != nil {
|
if err := m.router.ipFwdState.RequestForwarding(); err != nil {
|
||||||
return fmt.Errorf("enable IPv4 forwarding: %w", err)
|
return fmt.Errorf("enable IP forwarding: %w", err)
|
||||||
}
|
|
||||||
// v6 only when the overlay actually has v6.
|
|
||||||
if m.router6 == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if err := m.router.ipFwdState.RequestForwarding(true); err != nil {
|
|
||||||
if rerr := m.router.ipFwdState.ReleaseForwarding(false); rerr != nil {
|
|
||||||
log.Warnf("rollback v4 forwarding: %v", rerr)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("enable IPv6 forwarding: %w", err)
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) DisableRouting() error {
|
func (m *Manager) DisableRouting() error {
|
||||||
var merr *multierror.Error
|
if err := m.router.ipFwdState.ReleaseForwarding(); err != nil {
|
||||||
if err := m.router.ipFwdState.ReleaseForwarding(false); err != nil {
|
return fmt.Errorf("disable IP forwarding: %w", err)
|
||||||
merr = multierror.Append(merr, fmt.Errorf("disable IPv4 forwarding: %w", err))
|
|
||||||
}
|
}
|
||||||
if m.router6 != nil {
|
return nil
|
||||||
if err := m.router.ipFwdState.ReleaseForwarding(true); err != nil {
|
|
||||||
merr = multierror.Append(merr, fmt.Errorf("disable IPv6 forwarding: %w", err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nberrors.FormatErrorOrNil(merr)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flush rule/chain/set operations from the buffer
|
// Flush rule/chain/set operations from the buffer
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ func newRouter(workTable *nftables.Table, wgIface iFaceMapper, mtu uint16) (*rou
|
|||||||
rules: make(map[string]*nftables.Rule),
|
rules: make(map[string]*nftables.Rule),
|
||||||
af: familyForAddr(workTable.Family == nftables.TableFamilyIPv4),
|
af: familyForAddr(workTable.Family == nftables.TableFamilyIPv4),
|
||||||
wgIface: wgIface,
|
wgIface: wgIface,
|
||||||
ipFwdState: ipfwdstate.NewIPForwardingState(wgIface.Name()),
|
ipFwdState: ipfwdstate.NewIPForwardingState(),
|
||||||
mtu: mtu,
|
mtu: mtu,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1550,6 +1550,10 @@ func (r *router) refreshRulesMap() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *router) AddDNATRule(rule firewall.ForwardRule) (firewall.Rule, error) {
|
func (r *router) AddDNATRule(rule firewall.ForwardRule) (firewall.Rule, error) {
|
||||||
|
if err := r.ipFwdState.RequestForwarding(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
ruleKey := rule.ID()
|
ruleKey := rule.ID()
|
||||||
if _, exists := r.rules[ruleKey+dnatSuffix]; exists {
|
if _, exists := r.rules[ruleKey+dnatSuffix]; exists {
|
||||||
return rule, nil
|
return rule, nil
|
||||||
@@ -1560,18 +1564,7 @@ func (r *router) AddDNATRule(rule firewall.ForwardRule) (firewall.Rule, error) {
|
|||||||
return nil, fmt.Errorf("convert protocol to number: %w", err)
|
return nil, fmt.Errorf("convert protocol to number: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request forwarding before queueing rules: addDnatRedirect/addDnatMasq
|
|
||||||
// buffer netlink messages on r.conn that the next caller's Flush would
|
|
||||||
// commit if we returned without flushing them ourselves.
|
|
||||||
v6 := r.af.tableFamily == nftables.TableFamilyIPv6
|
|
||||||
if err := r.ipFwdState.RequestForwarding(v6); err != nil {
|
|
||||||
return nil, fmt.Errorf("enable forwarding: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := r.addDnatRedirect(rule, protoNum, ruleKey); err != nil {
|
if err := r.addDnatRedirect(rule, protoNum, ruleKey); err != nil {
|
||||||
if rerr := r.ipFwdState.ReleaseForwarding(v6); rerr != nil {
|
|
||||||
log.Warnf("rollback forwarding refcount: %v", rerr)
|
|
||||||
}
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1583,11 +1576,6 @@ func (r *router) AddDNATRule(rule firewall.ForwardRule) (firewall.Rule, error) {
|
|||||||
// TODO: find chains with drop policies and add rules there
|
// TODO: find chains with drop policies and add rules there
|
||||||
|
|
||||||
if err := r.conn.Flush(); err != nil {
|
if err := r.conn.Flush(); err != nil {
|
||||||
if rerr := r.ipFwdState.ReleaseForwarding(v6); rerr != nil {
|
|
||||||
log.Warnf("rollback forwarding refcount: %v", rerr)
|
|
||||||
}
|
|
||||||
delete(r.rules, ruleKey+dnatSuffix)
|
|
||||||
delete(r.rules, ruleKey+snatSuffix)
|
|
||||||
return nil, fmt.Errorf("flush rules: %w", err)
|
return nil, fmt.Errorf("flush rules: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1790,18 +1778,16 @@ func (r *router) addDnatMasq(rule firewall.ForwardRule, protoNum uint8, ruleKey
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *router) DeleteDNATRule(rule firewall.Rule) error {
|
func (r *router) DeleteDNATRule(rule firewall.Rule) error {
|
||||||
|
if err := r.ipFwdState.ReleaseForwarding(); err != nil {
|
||||||
|
log.Errorf("%v", err)
|
||||||
|
}
|
||||||
|
|
||||||
ruleKey := rule.ID()
|
ruleKey := rule.ID()
|
||||||
|
|
||||||
if err := r.refreshRulesMap(); err != nil {
|
if err := r.refreshRulesMap(); err != nil {
|
||||||
return fmt.Errorf(refreshRulesMapError, err)
|
return fmt.Errorf(refreshRulesMapError, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, hadDNAT := r.rules[ruleKey+dnatSuffix]
|
|
||||||
_, hadSNAT := r.rules[ruleKey+snatSuffix]
|
|
||||||
if !hadDNAT && !hadSNAT {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var merr *multierror.Error
|
var merr *multierror.Error
|
||||||
var needsFlush bool
|
var needsFlush bool
|
||||||
|
|
||||||
@@ -1838,10 +1824,6 @@ func (r *router) DeleteDNATRule(rule firewall.Rule) error {
|
|||||||
delete(r.rules, ruleKey+snatSuffix)
|
delete(r.rules, ruleKey+snatSuffix)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := r.ipFwdState.ReleaseForwarding(r.af.tableFamily == nftables.TableFamilyIPv6); err != nil {
|
|
||||||
log.Errorf("%v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nberrors.FormatErrorOrNil(merr)
|
return nberrors.FormatErrorOrNil(merr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ func (c *ConnectClient) RunOniOS(
|
|||||||
networkChangeListener listener.NetworkChangeListener,
|
networkChangeListener listener.NetworkChangeListener,
|
||||||
dnsManager dns.IosDnsManager,
|
dnsManager dns.IosDnsManager,
|
||||||
stateFilePath string,
|
stateFilePath string,
|
||||||
|
cacheDir string,
|
||||||
) error {
|
) error {
|
||||||
// Set GC percent to 5% to reduce memory usage as iOS only allows 50MB of memory for the extension.
|
// Set GC percent to 5% to reduce memory usage as iOS only allows 50MB of memory for the extension.
|
||||||
debug.SetGCPercent(5)
|
debug.SetGCPercent(5)
|
||||||
@@ -126,6 +127,7 @@ func (c *ConnectClient) RunOniOS(
|
|||||||
NetworkChangeListener: networkChangeListener,
|
NetworkChangeListener: networkChangeListener,
|
||||||
DnsManager: dnsManager,
|
DnsManager: dnsManager,
|
||||||
StateFilePath: stateFilePath,
|
StateFilePath: stateFilePath,
|
||||||
|
TempDir: cacheDir,
|
||||||
}
|
}
|
||||||
return c.run(mobileDependency, nil, "")
|
return c.run(mobileDependency, nil, "")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -250,6 +250,7 @@ type BundleGenerator struct {
|
|||||||
syncResponse *mgmProto.SyncResponse
|
syncResponse *mgmProto.SyncResponse
|
||||||
logPath string
|
logPath string
|
||||||
tempDir string
|
tempDir string
|
||||||
|
statePath string
|
||||||
cpuProfile []byte
|
cpuProfile []byte
|
||||||
capturePath string
|
capturePath string
|
||||||
refreshStatus func() // Optional callback to refresh status before bundle generation
|
refreshStatus func() // Optional callback to refresh status before bundle generation
|
||||||
@@ -274,6 +275,7 @@ type GeneratorDependencies struct {
|
|||||||
SyncResponse *mgmProto.SyncResponse
|
SyncResponse *mgmProto.SyncResponse
|
||||||
LogPath string
|
LogPath string
|
||||||
TempDir string // Directory for temporary bundle zip files. If empty, os.TempDir() is used.
|
TempDir string // Directory for temporary bundle zip files. If empty, os.TempDir() is used.
|
||||||
|
StatePath string // Path to the state file. If empty, the ServiceManager default path is used.
|
||||||
CPUProfile []byte
|
CPUProfile []byte
|
||||||
CapturePath string
|
CapturePath string
|
||||||
RefreshStatus func()
|
RefreshStatus func()
|
||||||
@@ -295,6 +297,7 @@ func NewBundleGenerator(deps GeneratorDependencies, cfg BundleConfig) *BundleGen
|
|||||||
syncResponse: deps.SyncResponse,
|
syncResponse: deps.SyncResponse,
|
||||||
logPath: deps.LogPath,
|
logPath: deps.LogPath,
|
||||||
tempDir: deps.TempDir,
|
tempDir: deps.TempDir,
|
||||||
|
statePath: deps.StatePath,
|
||||||
cpuProfile: deps.CPUProfile,
|
cpuProfile: deps.CPUProfile,
|
||||||
capturePath: deps.CapturePath,
|
capturePath: deps.CapturePath,
|
||||||
refreshStatus: deps.RefreshStatus,
|
refreshStatus: deps.RefreshStatus,
|
||||||
@@ -811,8 +814,11 @@ func (g *BundleGenerator) addSyncResponse() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (g *BundleGenerator) addStateFile() error {
|
func (g *BundleGenerator) addStateFile() error {
|
||||||
|
path := g.statePath
|
||||||
|
if path == "" {
|
||||||
sm := profilemanager.NewServiceManager("")
|
sm := profilemanager.NewServiceManager("")
|
||||||
path := sm.GetStatePath()
|
path = sm.GetStatePath()
|
||||||
|
}
|
||||||
if path == "" {
|
if path == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
36
client/internal/debug/debug_ios.go
Normal file
36
client/internal/debug/debug_ios.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
//go:build ios
|
||||||
|
|
||||||
|
package debug
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// swiftLogFile is the Swift app log written by the iOS app into the same log
|
||||||
|
// directory as the Go client log, so it can be collected into the bundle.
|
||||||
|
const swiftLogFile = "swift-log.log"
|
||||||
|
|
||||||
|
// addPlatformLog collects logs for the iOS debug bundle. iOS has no logcat or
|
||||||
|
// systemd journal, so we rely on file-based logs. addLogfile handles the Go
|
||||||
|
// client log (logPath) with rotation, the stderr/stdout companions and
|
||||||
|
// anonymization. The iOS app writes its own Swift log into the same directory,
|
||||||
|
// so we add it alongside the Go log.
|
||||||
|
func (g *BundleGenerator) addPlatformLog() error {
|
||||||
|
if err := g.addLogfile(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.logPath == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
swiftLogPath := filepath.Join(filepath.Dir(g.logPath), swiftLogFile)
|
||||||
|
if err := g.addSingleLogfile(swiftLogPath, swiftLogFile); err != nil {
|
||||||
|
// The Swift log is best-effort: the app may not have written it yet.
|
||||||
|
log.Warnf("failed to add %s to debug bundle: %v", swiftLogFile, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -844,10 +844,6 @@ func collectSysctls() string {
|
|||||||
[]string{"net.ipv4.conf.all.src_valid_mark", "net.ipv4.conf.default.src_valid_mark"},
|
[]string{"net.ipv4.conf.all.src_valid_mark", "net.ipv4.conf.default.src_valid_mark"},
|
||||||
listInterfaceSysctls("ipv4", "src_valid_mark")...,
|
listInterfaceSysctls("ipv4", "src_valid_mark")...,
|
||||||
))
|
))
|
||||||
writeSysctlGroup(&builder, "accept_ra", append(
|
|
||||||
[]string{"net.ipv6.conf.all.accept_ra", "net.ipv6.conf.default.accept_ra"},
|
|
||||||
listInterfaceSysctls("ipv6", "accept_ra")...,
|
|
||||||
))
|
|
||||||
writeSysctlGroup(&builder, "conntrack", []string{
|
writeSysctlGroup(&builder, "conntrack", []string{
|
||||||
"net.netfilter.nf_conntrack_acct",
|
"net.netfilter.nf_conntrack_acct",
|
||||||
"net.netfilter.nf_conntrack_tcp_loose",
|
"net.netfilter.nf_conntrack_tcp_loose",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build !android
|
//go:build !android && !ios
|
||||||
|
|
||||||
package debug
|
package debug
|
||||||
|
|
||||||
|
|||||||
@@ -2,109 +2,54 @@ package ipfwdstate
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/internal/routemanager/systemops"
|
"github.com/netbirdio/netbird/client/internal/routemanager/systemops"
|
||||||
)
|
)
|
||||||
|
|
||||||
// IPForwardingState tracks v4 and v6 IP-forwarding sysctl enables with
|
// IPForwardingState is a struct that keeps track of the IP forwarding state.
|
||||||
// independent refcounts so a v4-only routing setup doesn't flip v6 sysctls.
|
// todo: read initial state of the IP forwarding from the system and reset the state based on it.
|
||||||
|
// todo: separate v4/v6 forwarding state, since the sysctls are independent
|
||||||
|
// (net.ipv4.ip_forward vs net.ipv6.conf.all.forwarding). Currently the nftables
|
||||||
|
// manager shares one instance between both routers, which works only because
|
||||||
|
// EnableIPForwarding enables both sysctls in a single call.
|
||||||
type IPForwardingState struct {
|
type IPForwardingState struct {
|
||||||
mu sync.Mutex
|
enabledCounter int
|
||||||
|
|
||||||
v4Count int
|
|
||||||
v6Count int
|
|
||||||
|
|
||||||
wgIfaceName string
|
|
||||||
v6Saved map[string]int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIPForwardingState(wgIfaceName string) *IPForwardingState {
|
func NewIPForwardingState() *IPForwardingState {
|
||||||
return &IPForwardingState{wgIfaceName: wgIfaceName}
|
return &IPForwardingState{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Counts returns the current v4 and v6 refcounts. Intended for diagnostics
|
func (f *IPForwardingState) RequestForwarding() error {
|
||||||
// and tests.
|
if f.enabledCounter != 0 {
|
||||||
func (f *IPForwardingState) Counts() (v4, v6 int) {
|
f.enabledCounter++
|
||||||
f.mu.Lock()
|
|
||||||
defer f.mu.Unlock()
|
|
||||||
return f.v4Count, f.v6Count
|
|
||||||
}
|
|
||||||
|
|
||||||
// RequestForwarding enables the family's forwarding sysctl on first request.
|
|
||||||
func (f *IPForwardingState) RequestForwarding(v6 bool) error {
|
|
||||||
f.mu.Lock()
|
|
||||||
defer f.mu.Unlock()
|
|
||||||
|
|
||||||
if v6 {
|
|
||||||
return f.requestV6()
|
|
||||||
}
|
|
||||||
return f.requestV4()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReleaseForwarding decrements the family counter. The last v6 release restores
|
|
||||||
// what enable captured. v4 stays on: net.ipv4.ip_forward is co-owned by other
|
|
||||||
// tooling (docker, k8s, libvirt).
|
|
||||||
func (f *IPForwardingState) ReleaseForwarding(v6 bool) error {
|
|
||||||
f.mu.Lock()
|
|
||||||
defer f.mu.Unlock()
|
|
||||||
|
|
||||||
if v6 {
|
|
||||||
return f.releaseV6()
|
|
||||||
}
|
|
||||||
f.releaseV4()
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *IPForwardingState) requestV4() error {
|
if err := systemops.EnableIPForwarding(); err != nil {
|
||||||
if f.v4Count == 0 {
|
return fmt.Errorf("failed to enable IP forwarding with sysctl: %w", err)
|
||||||
if err := systemops.EnableV4IPForwarding(); err != nil {
|
|
||||||
return fmt.Errorf("enable IPv4 forwarding: %w", err)
|
|
||||||
}
|
}
|
||||||
log.Info("IPv4 forwarding enabled")
|
f.enabledCounter = 1
|
||||||
}
|
log.Info("IP forwarding enabled")
|
||||||
f.v4Count++
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *IPForwardingState) releaseV4() {
|
func (f *IPForwardingState) ReleaseForwarding() error {
|
||||||
if f.v4Count > 0 {
|
if f.enabledCounter == 0 {
|
||||||
f.v4Count--
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *IPForwardingState) requestV6() error {
|
|
||||||
if f.v6Count == 0 {
|
|
||||||
saved, err := systemops.EnableV6IPForwarding(f.wgIfaceName)
|
|
||||||
if err != nil {
|
|
||||||
if rerr := systemops.DisableV6IPForwarding(saved); rerr != nil {
|
|
||||||
log.Warnf("rollback partial v6 sysctls: %v", rerr)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("enable IPv6 forwarding: %w", err)
|
|
||||||
}
|
|
||||||
f.v6Saved = saved
|
|
||||||
log.Info("IPv6 forwarding enabled")
|
|
||||||
}
|
|
||||||
f.v6Count++
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *IPForwardingState) releaseV6() error {
|
if f.enabledCounter > 1 {
|
||||||
if f.v6Count == 0 {
|
f.enabledCounter--
|
||||||
return nil
|
|
||||||
}
|
|
||||||
f.v6Count--
|
|
||||||
if f.v6Count > 0 {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
saved := f.v6Saved
|
// if failed to disable IP forwarding we anyway decrement the counter
|
||||||
f.v6Saved = nil
|
f.enabledCounter = 0
|
||||||
if err := systemops.DisableV6IPForwarding(saved); err != nil {
|
|
||||||
return fmt.Errorf("disable IPv6 forwarding: %w", err)
|
// todo call systemops.DisableIPForwarding()
|
||||||
}
|
|
||||||
log.Info("IPv6 forwarding disabled")
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,17 +32,8 @@ func (r *SysOps) removeFromRouteTable(netip.Prefix, Nexthop) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func EnableV4IPForwarding() error {
|
func EnableIPForwarding() error {
|
||||||
log.Infof("Enable IPv4 forwarding is not implemented on %s", runtime.GOOS)
|
log.Infof("Enable IP forwarding is not implemented on %s", runtime.GOOS)
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func EnableV6IPForwarding(string) (map[string]int, error) {
|
|
||||||
log.Infof("Enable IPv6 forwarding is not implemented on %s", runtime.GOOS)
|
|
||||||
return map[string]int{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func DisableV6IPForwarding(map[string]int) error {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,17 +58,8 @@ func (r *SysOps) removeFromRouteTable(netip.Prefix, Nexthop) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func EnableV4IPForwarding() error {
|
func EnableIPForwarding() error {
|
||||||
log.Infof("Enable IPv4 forwarding is not implemented on %s", runtime.GOOS)
|
log.Infof("Enable IP forwarding is not implemented on %s", runtime.GOOS)
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func EnableV6IPForwarding(string) (map[string]int, error) {
|
|
||||||
log.Infof("Enable IPv6 forwarding is not implemented on %s", runtime.GOOS)
|
|
||||||
return map[string]int{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func DisableV6IPForwarding(map[string]int) error {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -763,10 +763,13 @@ func flushRoutes(tableID, family int) error {
|
|||||||
return nberrors.FormatErrorOrNil(result)
|
return nberrors.FormatErrorOrNil(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
func EnableV4IPForwarding() error {
|
func EnableIPForwarding() error {
|
||||||
if _, err := sysctl.Set(ipv4ForwardingPath, 1, false); err != nil {
|
if _, err := sysctl.Set(ipv4ForwardingPath, 1, false); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if _, err := sysctl.Set(ipv6ForwardingPath, 1, false); err != nil {
|
||||||
|
log.Warnf("failed to enable IPv6 forwarding: %v", err)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,17 +43,8 @@ func (r *SysOps) RemoveVPNRoute(prefix netip.Prefix, intf *net.Interface) error
|
|||||||
return r.genericRemoveVPNRoute(prefix, intf)
|
return r.genericRemoveVPNRoute(prefix, intf)
|
||||||
}
|
}
|
||||||
|
|
||||||
func EnableV4IPForwarding() error {
|
func EnableIPForwarding() error {
|
||||||
log.Infof("Enable IPv4 forwarding is not implemented on %s", runtime.GOOS)
|
log.Infof("Enable IP forwarding is not implemented on %s", runtime.GOOS)
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func EnableV6IPForwarding(string) (map[string]int, error) {
|
|
||||||
log.Infof("Enable IPv6 forwarding is not implemented on %s", runtime.GOOS)
|
|
||||||
return map[string]int{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func DisableV6IPForwarding(map[string]int) error {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,82 +0,0 @@
|
|||||||
//go:build !android
|
|
||||||
|
|
||||||
package systemops
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/hashicorp/go-multierror"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
nberrors "github.com/netbirdio/netbird/client/errors"
|
|
||||||
"github.com/netbirdio/netbird/client/internal/routemanager/sysctl"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// 1 (default) accepts RAs only while forwarding is off; 2 keeps RA
|
|
||||||
// acceptance on regardless, so RA-installed host defaults survive our
|
|
||||||
// v6 forwarding flip.
|
|
||||||
acceptRAInterfacePath = "net.ipv6.conf.%s.accept_ra"
|
|
||||||
acceptRAProcPathFormat = "/proc/sys/net/ipv6/conf/%s/accept_ra"
|
|
||||||
)
|
|
||||||
|
|
||||||
// EnableV6IPForwarding bumps accept_ra=2 on host v6 interfaces before flipping
|
|
||||||
// forwarding=1, so RA-installed host defaults survive. Returns the prior values
|
|
||||||
// of sysctls we actually changed; entries already at the target are omitted.
|
|
||||||
func EnableV6IPForwarding(wgIfaceName string) (map[string]int, error) {
|
|
||||||
saved := map[string]int{}
|
|
||||||
bumpAcceptRA(saved, wgIfaceName)
|
|
||||||
|
|
||||||
oldVal, err := sysctl.Set(ipv6ForwardingPath, 1, false)
|
|
||||||
if err != nil {
|
|
||||||
return saved, err
|
|
||||||
}
|
|
||||||
if oldVal != 1 {
|
|
||||||
saved[ipv6ForwardingPath] = oldVal
|
|
||||||
}
|
|
||||||
return saved, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DisableV6IPForwarding restores what EnableV6IPForwarding captured.
|
|
||||||
func DisableV6IPForwarding(saved map[string]int) error {
|
|
||||||
var result *multierror.Error
|
|
||||||
for key, value := range saved {
|
|
||||||
if _, err := sysctl.Set(key, value, false); err != nil {
|
|
||||||
result = multierror.Append(result, fmt.Errorf("restore %s: %w", key, err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nberrors.FormatErrorOrNil(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
func bumpAcceptRA(saved map[string]int, wgIfaceName string) {
|
|
||||||
interfaces, err := net.Interfaces()
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("list interfaces for accept_ra: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for _, intf := range interfaces {
|
|
||||||
if intf.Name == "lo" || intf.Name == wgIfaceName {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
bumpAcceptRAForInterface(saved, intf.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func bumpAcceptRAForInterface(saved map[string]int, name string) {
|
|
||||||
key := fmt.Sprintf(acceptRAInterfacePath, name)
|
|
||||||
// Build procfs path from name, not the dotted key: VLAN names like eth0.100.
|
|
||||||
if _, err := os.Stat(fmt.Sprintf(acceptRAProcPathFormat, name)); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// onlyIfOne=true: leave admin overrides (0, 2) alone.
|
|
||||||
oldVal, err := sysctl.Set(key, 2, true)
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("bump %s: %v", key, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if oldVal != 2 {
|
|
||||||
saved[key] = oldVal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -17,6 +17,7 @@ import (
|
|||||||
|
|
||||||
"github.com/netbirdio/netbird/client/internal"
|
"github.com/netbirdio/netbird/client/internal"
|
||||||
"github.com/netbirdio/netbird/client/internal/auth"
|
"github.com/netbirdio/netbird/client/internal/auth"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/debug"
|
||||||
"github.com/netbirdio/netbird/client/internal/dns"
|
"github.com/netbirdio/netbird/client/internal/dns"
|
||||||
"github.com/netbirdio/netbird/client/internal/listener"
|
"github.com/netbirdio/netbird/client/internal/listener"
|
||||||
"github.com/netbirdio/netbird/client/internal/peer"
|
"github.com/netbirdio/netbird/client/internal/peer"
|
||||||
@@ -25,6 +26,7 @@ import (
|
|||||||
"github.com/netbirdio/netbird/formatter"
|
"github.com/netbirdio/netbird/formatter"
|
||||||
"github.com/netbirdio/netbird/route"
|
"github.com/netbirdio/netbird/route"
|
||||||
"github.com/netbirdio/netbird/shared/management/domain"
|
"github.com/netbirdio/netbird/shared/management/domain"
|
||||||
|
types "github.com/netbirdio/netbird/upload-server/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ConnectionListener export internal Listener for mobile
|
// ConnectionListener export internal Listener for mobile
|
||||||
@@ -65,6 +67,7 @@ func init() {
|
|||||||
type Client struct {
|
type Client struct {
|
||||||
cfgFile string
|
cfgFile string
|
||||||
stateFile string
|
stateFile string
|
||||||
|
cacheDir string
|
||||||
recorder *peer.Status
|
recorder *peer.Status
|
||||||
ctxCancel context.CancelFunc
|
ctxCancel context.CancelFunc
|
||||||
ctxCancelLock *sync.Mutex
|
ctxCancelLock *sync.Mutex
|
||||||
@@ -75,16 +78,20 @@ type Client struct {
|
|||||||
onHostDnsFn func([]string)
|
onHostDnsFn func([]string)
|
||||||
dnsManager dns.IosDnsManager
|
dnsManager dns.IosDnsManager
|
||||||
loginComplete bool
|
loginComplete bool
|
||||||
connectClient *internal.ConnectClient
|
|
||||||
// preloadedConfig holds config loaded from JSON (used on tvOS where file writes are blocked)
|
// preloadedConfig holds config loaded from JSON (used on tvOS where file writes are blocked)
|
||||||
preloadedConfig *profilemanager.Config
|
preloadedConfig *profilemanager.Config
|
||||||
|
|
||||||
|
stateMu sync.RWMutex
|
||||||
|
connectClient *internal.ConnectClient
|
||||||
|
config *profilemanager.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClient instantiate a new Client
|
// NewClient instantiate a new Client
|
||||||
func NewClient(cfgFile, stateFile, deviceName string, osVersion string, osName string, networkChangeListener NetworkChangeListener, dnsManager DnsManager) *Client {
|
func NewClient(cfgFile, stateFile, cacheDir, deviceName string, osVersion string, osName string, networkChangeListener NetworkChangeListener, dnsManager DnsManager) *Client {
|
||||||
return &Client{
|
return &Client{
|
||||||
cfgFile: cfgFile,
|
cfgFile: cfgFile,
|
||||||
stateFile: stateFile,
|
stateFile: stateFile,
|
||||||
|
cacheDir: cacheDir,
|
||||||
deviceName: deviceName,
|
deviceName: deviceName,
|
||||||
osName: osName,
|
osName: osName,
|
||||||
osVersion: osVersion,
|
osVersion: osVersion,
|
||||||
@@ -161,8 +168,9 @@ func (c *Client) Run(fd int32, interfaceName string, envList *EnvList) error {
|
|||||||
c.onHostDnsFn = func([]string) {}
|
c.onHostDnsFn = func([]string) {}
|
||||||
cfg.WgIface = interfaceName
|
cfg.WgIface = interfaceName
|
||||||
|
|
||||||
c.connectClient = internal.NewConnectClient(ctx, cfg, c.recorder)
|
connectClient := internal.NewConnectClient(ctx, cfg, c.recorder)
|
||||||
return c.connectClient.RunOniOS(fd, c.networkChangeListener, c.dnsManager, c.stateFile)
|
c.setState(cfg, connectClient)
|
||||||
|
return connectClient.RunOniOS(fd, c.networkChangeListener, c.dnsManager, c.stateFile, c.cacheDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop the internal client and free the resources
|
// Stop the internal client and free the resources
|
||||||
@@ -174,6 +182,83 @@ func (c *Client) Stop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
c.ctxCancel()
|
c.ctxCancel()
|
||||||
|
c.setState(nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DebugBundle generates a debug bundle, uploads it and returns the upload key.
|
||||||
|
// It works with or without a running engine: when the engine is up it reuses
|
||||||
|
// the live config, sync response and client metrics; otherwise it loads the
|
||||||
|
// config from disk (or the preloaded tvOS config).
|
||||||
|
func (c *Client) DebugBundle(anonymize bool) (string, error) {
|
||||||
|
cfg, cc := c.stateSnapshot()
|
||||||
|
|
||||||
|
// If the engine hasn't been started, load config so we can reach management.
|
||||||
|
if cfg == nil {
|
||||||
|
if c.preloadedConfig != nil {
|
||||||
|
cfg = c.preloadedConfig
|
||||||
|
} else {
|
||||||
|
var err error
|
||||||
|
// Use DirectUpdateOrCreateConfig to avoid atomic file operations
|
||||||
|
// (temp file + rename) blocked by the tvOS sandbox.
|
||||||
|
cfg, err = profilemanager.DirectUpdateOrCreateConfig(profilemanager.ConfigInput{
|
||||||
|
ConfigPath: c.cfgFile,
|
||||||
|
StateFilePath: c.stateFile,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("load config: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deps := debug.GeneratorDependencies{
|
||||||
|
InternalConfig: cfg,
|
||||||
|
StatusRecorder: c.recorder,
|
||||||
|
TempDir: c.cacheDir,
|
||||||
|
StatePath: c.stateFile,
|
||||||
|
}
|
||||||
|
|
||||||
|
if cc != nil {
|
||||||
|
resp, err := cc.GetLatestSyncResponse()
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("get latest sync response: %v", err)
|
||||||
|
}
|
||||||
|
deps.SyncResponse = resp
|
||||||
|
|
||||||
|
if e := cc.Engine(); e != nil {
|
||||||
|
if cm := e.GetClientMetrics(); cm != nil {
|
||||||
|
deps.ClientMetrics = cm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bundleGenerator := debug.NewBundleGenerator(
|
||||||
|
deps,
|
||||||
|
debug.BundleConfig{
|
||||||
|
Anonymize: anonymize,
|
||||||
|
IncludeSystemInfo: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
path, err := bundleGenerator.Generate()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("generate debug bundle: %w", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := os.Remove(path); err != nil {
|
||||||
|
log.Errorf("failed to remove debug bundle file: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
uploadCtx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
key, err := debug.UploadDebugBundle(uploadCtx, types.DefaultBundleURL, cfg.ManagementURL.String(), path)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("upload debug bundle: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("debug bundle uploaded with key %s", key)
|
||||||
|
return key, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetTraceLogLevel configure the logger to trace level
|
// SetTraceLogLevel configure the logger to trace level
|
||||||
@@ -354,11 +439,12 @@ func (c *Client) ClearLoginComplete() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetRoutesSelectionDetails() (*RoutesSelectionDetails, error) {
|
func (c *Client) GetRoutesSelectionDetails() (*RoutesSelectionDetails, error) {
|
||||||
if c.connectClient == nil {
|
_, connectClient := c.stateSnapshot()
|
||||||
|
if connectClient == nil {
|
||||||
return nil, fmt.Errorf("not connected")
|
return nil, fmt.Errorf("not connected")
|
||||||
}
|
}
|
||||||
|
|
||||||
engine := c.connectClient.Engine()
|
engine := connectClient.Engine()
|
||||||
if engine == nil {
|
if engine == nil {
|
||||||
return nil, fmt.Errorf("not connected")
|
return nil, fmt.Errorf("not connected")
|
||||||
}
|
}
|
||||||
@@ -470,11 +556,12 @@ func prepareRouteSelectionDetails(routes []*selectRoute, resolvedDomains map[dom
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) SelectRoute(id string) error {
|
func (c *Client) SelectRoute(id string) error {
|
||||||
if c.connectClient == nil {
|
_, connectClient := c.stateSnapshot()
|
||||||
|
if connectClient == nil {
|
||||||
return fmt.Errorf("not connected")
|
return fmt.Errorf("not connected")
|
||||||
}
|
}
|
||||||
|
|
||||||
engine := c.connectClient.Engine()
|
engine := connectClient.Engine()
|
||||||
if engine == nil {
|
if engine == nil {
|
||||||
return fmt.Errorf("not connected")
|
return fmt.Errorf("not connected")
|
||||||
}
|
}
|
||||||
@@ -500,10 +587,11 @@ func (c *Client) SelectRoute(id string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) DeselectRoute(id string) error {
|
func (c *Client) DeselectRoute(id string) error {
|
||||||
if c.connectClient == nil {
|
_, connectClient := c.stateSnapshot()
|
||||||
|
if connectClient == nil {
|
||||||
return fmt.Errorf("not connected")
|
return fmt.Errorf("not connected")
|
||||||
}
|
}
|
||||||
engine := c.connectClient.Engine()
|
engine := connectClient.Engine()
|
||||||
if engine == nil {
|
if engine == nil {
|
||||||
return fmt.Errorf("not connected")
|
return fmt.Errorf("not connected")
|
||||||
}
|
}
|
||||||
@@ -527,6 +615,22 @@ func (c *Client) DeselectRoute(id string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// setState stores the running engine state so DebugBundle can reuse the live
|
||||||
|
// config and ConnectClient. It is cleared on Stop.
|
||||||
|
func (c *Client) setState(cfg *profilemanager.Config, cc *internal.ConnectClient) {
|
||||||
|
c.stateMu.Lock()
|
||||||
|
defer c.stateMu.Unlock()
|
||||||
|
c.config = cfg
|
||||||
|
c.connectClient = cc
|
||||||
|
}
|
||||||
|
|
||||||
|
// stateSnapshot returns the current config and ConnectClient under the lock.
|
||||||
|
func (c *Client) stateSnapshot() (*profilemanager.Config, *internal.ConnectClient) {
|
||||||
|
c.stateMu.RLock()
|
||||||
|
defer c.stateMu.RUnlock()
|
||||||
|
return c.config, c.connectClient
|
||||||
|
}
|
||||||
|
|
||||||
func formatDuration(d time.Duration) string {
|
func formatDuration(d time.Duration) string {
|
||||||
ds := d.String()
|
ds := d.String()
|
||||||
dotIndex := strings.Index(ds, ".")
|
dotIndex := strings.Index(ds, ".")
|
||||||
|
|||||||
Reference in New Issue
Block a user