Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[client] Add experimental support for userspace routing #3134

Open
wants to merge 71 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
4199da4
Add userspace routing
lixmal Dec 26, 2024
b43a8c5
Update wireguard-go ref
lixmal Dec 30, 2024
fad82ee
Add stop methods and improve udp implementation
lixmal Dec 30, 2024
d261654
Add logger
lixmal Dec 30, 2024
6a97d44
Improve udp implementation
lixmal Dec 30, 2024
9feaa8d
Add icmp forwarder
lixmal Dec 30, 2024
fb1a107
Fix lint and test issues
lixmal Dec 31, 2024
509b4e2
Lower udp timeout and add teardown messages
lixmal Dec 31, 2024
ed22d79
Add more control with env vars, also allow to pass traffic to native …
lixmal Jan 2, 2025
a12a9ac
Handle all local IPs
lixmal Jan 2, 2025
7667886
Add more tcp logging
lixmal Jan 2, 2025
d85e57e
Handle other icmp types in forwarder
lixmal Jan 2, 2025
657413b
Move icmp acceptance logic
lixmal Jan 2, 2025
fa27369
Fix linter issues
lixmal Jan 2, 2025
c3c6afa
Merge branch 'main' into userspace-router
lixmal Jan 2, 2025
2b8092d
Close endpoints
lixmal Jan 2, 2025
911f86d
Support local IPs in netstack mode
lixmal Jan 2, 2025
568d064
Drop certain forwarded icmp packets
lixmal Jan 2, 2025
e912f2d
Fix double close in logger
lixmal Jan 2, 2025
f772a21
Fix log level handling
lixmal Jan 2, 2025
0b9854b
Fix tests
lixmal Jan 2, 2025
2930288
Fix test expectation
lixmal Jan 2, 2025
88b420d
Remove linux restriction
lixmal Jan 2, 2025
0c2fa38
Exclude benchmark from CI
lixmal Jan 3, 2025
d711172
Fix benchmarks
lixmal Jan 3, 2025
9490e90
Reduce complexity
lixmal Jan 3, 2025
955b2b9
Complete route ACLs and add tests
lixmal Jan 3, 2025
fc799ef
Set log level from logrus
lixmal Jan 3, 2025
c68be6b
Remove fractions of seconds
lixmal Jan 3, 2025
979fe6b
Reduce complexity and fix linter issues
lixmal Jan 3, 2025
3ce3990
Merge branch 'main' into userspace-router
lixmal Jan 3, 2025
f26b418
Allow to set firewall log level
lixmal Jan 3, 2025
a6ad4dc
Close endpoint when stopping udp forwarder
lixmal Jan 3, 2025
62a20f5
Add local IPs test
lixmal Jan 3, 2025
f69dd6f
Make extra IPs from interfaces optional
lixmal Jan 3, 2025
0b116b3
Use native firewall for nat/firewall operations if available
lixmal Jan 3, 2025
eaadb75
Add env var to force userspace routing if native routing is available
lixmal Jan 3, 2025
7dfe7e4
Always use userspace routing in netstack mode
lixmal Jan 3, 2025
766e0cc
Add packet tracer
lixmal Jan 3, 2025
474fb33
Remove established field from udp and icmp (unused)
lixmal Jan 4, 2025
290e699
Demote close error levels
lixmal Jan 5, 2025
fe7a2aa
Fix test
lixmal Jan 5, 2025
4a189a8
Use MTU for udp max size
lixmal Jan 6, 2025
5ea39df
Adjust limits for iOS
lixmal Jan 6, 2025
2060242
Merge branch 'main' into userspace-router
lixmal Jan 7, 2025
28f5cd5
Merge branch 'main' into userspace-router
lixmal Jan 8, 2025
daf9359
Handle disable-server-routes flag in userspace router
lixmal Jan 9, 2025
6335ef8
Correct comment
lixmal Jan 9, 2025
706f98c
Improve routing decision logic
lixmal Jan 9, 2025
01957a3
Merge branch 'main' into userspace-router
lixmal Jan 10, 2025
af46f25
Block wg net forwarded traffic
lixmal Jan 10, 2025
d31543c
Enable ssh server on freebsd
lixmal Jan 11, 2025
648b22a
Fix listening in netstack mode
lixmal Jan 11, 2025
8430c37
Fix panic if login cmd fails
lixmal Jan 11, 2025
1296ecf
Tidy up go mod
lixmal Jan 11, 2025
1c00870
Merge branch 'allow-ssh-server-freebsd' into userspace-router
lixmal Jan 11, 2025
a625f90
Merge branch 'main' into userspace-router
lixmal Jan 14, 2025
8dce131
Disable local forwarding in netstack mode by default for security rea…
lixmal Jan 14, 2025
ea6c947
Merge branch 'main' into userspace-router
lixmal Jan 15, 2025
22991b3
Process drop rules first (#3167)
lixmal Jan 15, 2025
77afcc8
Merge branch 'main' into userspace-router
lixmal Jan 15, 2025
21a3679
Fix regression
lixmal Jan 15, 2025
9b5c043
Make debug ops a bit safer
lixmal Jan 16, 2025
862d548
Support port ranges
lixmal Jan 23, 2025
b951fb4
Use uppercase field name
lixmal Jan 23, 2025
da43d33
Merge branch 'port-range-acl' into userspace-router
lixmal Jan 23, 2025
e3d4f98
Add test cases for port ranges in peer ACLs
lixmal Jan 23, 2025
0837864
Fix port
lixmal Jan 23, 2025
a0ca3ed
Merge branch 'port-range-acl' into userspace-router
lixmal Jan 23, 2025
4d635e3
Merge branch 'main' into userspace-router
lixmal Jan 29, 2025
48f58d7
Treat the whole localhost range as local IPs
lixmal Jan 29, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions client/Dockerfile-rootless
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ USER netbird:netbird

ENV NB_FOREGROUND_MODE=true
ENV NB_USE_NETSTACK_MODE=true
ENV NB_ENABLE_NETSTACK_LOCAL_FORWARDING=true
ENV NB_CONFIG=config.json
ENV NB_DAEMON_ADDR=unix://netbird.sock
ENV NB_DISABLE_DNS=true
Expand Down
137 changes: 137 additions & 0 deletions client/cmd/trace.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package cmd

import (
"fmt"
"math/rand"
"strings"

"github.com/spf13/cobra"
"google.golang.org/grpc/status"

"github.com/netbirdio/netbird/client/proto"
)

var traceCmd = &cobra.Command{
Use: "trace <direction> <source-ip> <dest-ip>",
Short: "Trace a packet through the firewall",
Example: `
netbird debug trace in 192.168.1.10 10.10.0.2 -p tcp --sport 12345 --dport 443 --syn --ack
netbird debug trace out 10.10.0.1 8.8.8.8 -p udp --dport 53
netbird debug trace in 10.10.0.2 10.10.0.1 -p icmp --type 8 --code 0
netbird debug trace in 100.64.1.1 self -p tcp --dport 80`,
Args: cobra.ExactArgs(3),
RunE: tracePacket,
}

func init() {
debugCmd.AddCommand(traceCmd)

traceCmd.Flags().StringP("protocol", "p", "tcp", "Protocol (tcp/udp/icmp)")
traceCmd.Flags().Uint16("sport", 0, "Source port")
traceCmd.Flags().Uint16("dport", 0, "Destination port")
traceCmd.Flags().Uint8("icmp-type", 0, "ICMP type")
traceCmd.Flags().Uint8("icmp-code", 0, "ICMP code")
traceCmd.Flags().Bool("syn", false, "TCP SYN flag")
traceCmd.Flags().Bool("ack", false, "TCP ACK flag")
traceCmd.Flags().Bool("fin", false, "TCP FIN flag")
traceCmd.Flags().Bool("rst", false, "TCP RST flag")
traceCmd.Flags().Bool("psh", false, "TCP PSH flag")
traceCmd.Flags().Bool("urg", false, "TCP URG flag")
}

func tracePacket(cmd *cobra.Command, args []string) error {
direction := strings.ToLower(args[0])
if direction != "in" && direction != "out" {
return fmt.Errorf("invalid direction: use 'in' or 'out'")
}

protocol := cmd.Flag("protocol").Value.String()
if protocol != "tcp" && protocol != "udp" && protocol != "icmp" {
return fmt.Errorf("invalid protocol: use tcp/udp/icmp")
}

sport, err := cmd.Flags().GetUint16("sport")
if err != nil {
return fmt.Errorf("invalid source port: %v", err)
}
dport, err := cmd.Flags().GetUint16("dport")
if err != nil {
return fmt.Errorf("invalid destination port: %v", err)
}

// For TCP/UDP, generate random ephemeral port (49152-65535) if not specified
if protocol != "icmp" {
if sport == 0 {
sport = uint16(rand.Intn(16383) + 49152)
}
if dport == 0 {
dport = uint16(rand.Intn(16383) + 49152)
}
}

var tcpFlags *proto.TCPFlags
if protocol == "tcp" {
syn, _ := cmd.Flags().GetBool("syn")
ack, _ := cmd.Flags().GetBool("ack")
fin, _ := cmd.Flags().GetBool("fin")
rst, _ := cmd.Flags().GetBool("rst")
psh, _ := cmd.Flags().GetBool("psh")
urg, _ := cmd.Flags().GetBool("urg")

tcpFlags = &proto.TCPFlags{
Syn: syn,
Ack: ack,
Fin: fin,
Rst: rst,
Psh: psh,
Urg: urg,
}
}

icmpType, _ := cmd.Flags().GetUint32("icmp-type")
icmpCode, _ := cmd.Flags().GetUint32("icmp-code")

conn, err := getClient(cmd)
if err != nil {
return err
}
defer conn.Close()

client := proto.NewDaemonServiceClient(conn)
resp, err := client.TracePacket(cmd.Context(), &proto.TracePacketRequest{
SourceIp: args[1],
DestinationIp: args[2],
Protocol: protocol,
SourcePort: uint32(sport),
DestinationPort: uint32(dport),
Direction: direction,
TcpFlags: tcpFlags,
IcmpType: &icmpType,
IcmpCode: &icmpCode,
})
if err != nil {
return fmt.Errorf("trace failed: %v", status.Convert(err).Message())
}

printTrace(cmd, args[1], args[2], protocol, sport, dport, resp)
return nil
}

func printTrace(cmd *cobra.Command, src, dst, proto string, sport, dport uint16, resp *proto.TracePacketResponse) {
cmd.Printf("Packet trace %s:%d -> %s:%d (%s)\n\n", src, sport, dst, dport, strings.ToUpper(proto))

for _, stage := range resp.Stages {
if stage.ForwardingDetails != nil {
cmd.Printf("%s: %s [%s]\n", stage.Name, stage.Message, *stage.ForwardingDetails)
} else {
cmd.Printf("%s: %s\n", stage.Name, stage.Message)
}
}

disposition := map[bool]string{
true: "\033[32mALLOWED\033[0m", // Green
false: "\033[31mDENIED\033[0m", // Red
}[resp.FinalDisposition]

cmd.Printf("\nFinal disposition: %s\n", disposition)
}
4 changes: 2 additions & 2 deletions client/firewall/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ import (
)

// NewFirewall creates a firewall manager instance
func NewFirewall(iface IFaceMapper, _ *statemanager.Manager) (firewall.Manager, error) {
func NewFirewall(iface IFaceMapper, _ *statemanager.Manager, disableServerRoutes bool) (firewall.Manager, error) {
if !iface.IsUserspaceBind() {
return nil, fmt.Errorf("not implemented for this OS: %s", runtime.GOOS)
}

// use userspace packet filtering firewall
fm, err := uspfilter.Create(iface)
fm, err := uspfilter.Create(iface, disableServerRoutes)
if err != nil {
return nil, err
}
Expand Down
14 changes: 7 additions & 7 deletions client/firewall/create_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,12 @@ const SKIP_NFTABLES_ENV = "NB_SKIP_NFTABLES_CHECK"
// FWType is the type for the firewall type
type FWType int

func NewFirewall(iface IFaceMapper, stateManager *statemanager.Manager) (firewall.Manager, error) {
func NewFirewall(iface IFaceMapper, stateManager *statemanager.Manager, disableServerRoutes bool) (firewall.Manager, error) {
// on the linux system we try to user nftables or iptables
// in any case, because we need to allow netbird interface traffic
// so we use AllowNetbird traffic from these firewall managers
// for the userspace packet filtering firewall
fm, err := createNativeFirewall(iface, stateManager)
fm, err := createNativeFirewall(iface, stateManager, disableServerRoutes)

if !iface.IsUserspaceBind() {
return fm, err
Expand All @@ -47,10 +47,10 @@ func NewFirewall(iface IFaceMapper, stateManager *statemanager.Manager) (firewal
if err != nil {
log.Warnf("failed to create native firewall: %v. Proceeding with userspace", err)
}
return createUserspaceFirewall(iface, fm)
return createUserspaceFirewall(iface, fm, disableServerRoutes)
}

func createNativeFirewall(iface IFaceMapper, stateManager *statemanager.Manager) (firewall.Manager, error) {
func createNativeFirewall(iface IFaceMapper, stateManager *statemanager.Manager, routes bool) (firewall.Manager, error) {
fm, err := createFW(iface)
if err != nil {
return nil, fmt.Errorf("create firewall: %s", err)
Expand All @@ -77,12 +77,12 @@ func createFW(iface IFaceMapper) (firewall.Manager, error) {
}
}

func createUserspaceFirewall(iface IFaceMapper, fm firewall.Manager) (firewall.Manager, error) {
func createUserspaceFirewall(iface IFaceMapper, fm firewall.Manager, disableServerRoutes bool) (firewall.Manager, error) {
var errUsp error
if fm != nil {
fm, errUsp = uspfilter.CreateWithNativeFirewall(iface, fm)
fm, errUsp = uspfilter.CreateWithNativeFirewall(iface, fm, disableServerRoutes)
} else {
fm, errUsp = uspfilter.Create(iface)
fm, errUsp = uspfilter.Create(iface, disableServerRoutes)
}

if errUsp != nil {
Expand Down
4 changes: 4 additions & 0 deletions client/firewall/iface.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package firewall

import (
wgdevice "golang.zx2c4.com/wireguard/device"

"github.com/netbirdio/netbird/client/iface/device"
)

Expand All @@ -10,4 +12,6 @@ type IFaceMapper interface {
Address() device.WGAddress
IsUserspaceBind() bool
SetFilter(device.PacketFilter) error
GetDevice() *device.FilteredDevice
GetWGDevice() *wgdevice.Device
}
5 changes: 5 additions & 0 deletions client/firewall/iptables/manager_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,11 @@ func (m *Manager) AllowNetbird() error {
// Flush doesn't need to be implemented for this manager
func (m *Manager) Flush() error { return nil }

// SetLogLevel sets the log level for the firewall manager
func (m *Manager) SetLogLevel(log.Level) {
// not supported
}

func getConntrackEstablished() []string {
return []string{"-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED", "-j", "ACCEPT"}
}
11 changes: 10 additions & 1 deletion client/firewall/iptables/router_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,16 @@ func (r *router) AddRouteFiltering(
}

rule := genRouteFilteringRuleSpec(params)
if err := r.iptablesClient.Append(tableFilter, chainRTFWD, rule...); err != nil {
// Insert DROP rules at the beginning, append ACCEPT rules at the end
var err error
if action == firewall.ActionDrop {
// after the established rule
err = r.iptablesClient.Insert(tableFilter, chainRTFWD, 2, rule...)
} else {
err = r.iptablesClient.Append(tableFilter, chainRTFWD, rule...)
}

if err != nil {
return nil, fmt.Errorf("add route rule: %v", err)
}

Expand Down
2 changes: 2 additions & 0 deletions client/firewall/manager/firewall.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ type Manager interface {

// Flush the changes to firewall controller
Flush() error

SetLogLevel(log.Level)
}

func GenKey(format string, pair RouterPair) string {
Expand Down
5 changes: 5 additions & 0 deletions client/firewall/nftables/manager_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,11 @@ func (m *Manager) cleanupNetbirdTables() error {
return nil
}

// SetLogLevel sets the log level for the firewall manager
func (m *Manager) SetLogLevel(log.Level) {
// not supported
}

// Flush rule/chain/set operations from the buffer
//
// Method also get all rules after flush and refreshes handle values in the rulesets
Expand Down
17 changes: 16 additions & 1 deletion client/firewall/nftables/manager_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ func TestNftablesManager(t *testing.T) {
Kind: expr.VerdictAccept,
},
}
require.ElementsMatch(t, rules[0].Exprs, expectedExprs1, "expected the same expressions")
compareExprsIgnoringCounters(t, rules[0].Exprs, expectedExprs1)

ipToAdd, _ := netip.AddrFromSlice(ip)
add := ipToAdd.Unmap()
Expand Down Expand Up @@ -307,3 +307,18 @@ func TestNftablesManagerCompatibilityWithIptables(t *testing.T) {
stdout, stderr = runIptablesSave(t)
verifyIptablesOutput(t, stdout, stderr)
}

func compareExprsIgnoringCounters(t *testing.T, got, want []expr.Any) {
t.Helper()
require.Equal(t, len(got), len(want), "expression count mismatch")

for i := range got {
if _, isCounter := got[i].(*expr.Counter); isCounter {
_, wantIsCounter := want[i].(*expr.Counter)
require.True(t, wantIsCounter, "expected Counter at index %d", i)
continue
}

require.Equal(t, got[i], want[i], "expression mismatch at index %d", i)
}
}
8 changes: 7 additions & 1 deletion client/firewall/nftables/router_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,13 @@ func (r *router) AddRouteFiltering(
UserData: []byte(ruleKey),
}

rule = r.conn.AddRule(rule)
// Insert DROP rules at the beginning, append ACCEPT rules at the end
if action == firewall.ActionDrop {
// TODO: Insert after the established rule
rule = r.conn.InsertRule(rule)
} else {
rule = r.conn.AddRule(rule)
}

log.Tracef("Adding route rule %s", spew.Sdump(rule))
if err := r.conn.Flush(); err != nil {
Expand Down
23 changes: 20 additions & 3 deletions client/firewall/uspfilter/allow_netbird.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
package uspfilter

import (
"context"
"time"

log "github.com/sirupsen/logrus"

"github.com/netbirdio/netbird/client/firewall/uspfilter/conntrack"
"github.com/netbirdio/netbird/client/internal/statemanager"
)
Expand All @@ -17,17 +22,29 @@ func (m *Manager) Reset(stateManager *statemanager.Manager) error {

if m.udpTracker != nil {
m.udpTracker.Close()
m.udpTracker = conntrack.NewUDPTracker(conntrack.DefaultUDPTimeout)
m.udpTracker = conntrack.NewUDPTracker(conntrack.DefaultUDPTimeout, m.logger)
}

if m.icmpTracker != nil {
m.icmpTracker.Close()
m.icmpTracker = conntrack.NewICMPTracker(conntrack.DefaultICMPTimeout)
m.icmpTracker = conntrack.NewICMPTracker(conntrack.DefaultICMPTimeout, m.logger)
}

if m.tcpTracker != nil {
m.tcpTracker.Close()
m.tcpTracker = conntrack.NewTCPTracker(conntrack.DefaultTCPTimeout)
m.tcpTracker = conntrack.NewTCPTracker(conntrack.DefaultTCPTimeout, m.logger)
}

if m.forwarder != nil {
m.forwarder.Stop()
}

if m.logger != nil {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
if err := m.logger.Stop(ctx); err != nil {
log.Errorf("failed to shutdown logger: %v", err)
}
}

if m.nativeFirewall != nil {
Expand Down
Loading
Loading