Skip to content

Commit

Permalink
Add DNS routes (#1943)
Browse files Browse the repository at this point in the history
  • Loading branch information
lixmal authored Jun 4, 2024
1 parent d403658 commit ee79f6c
Show file tree
Hide file tree
Showing 74 changed files with 2,911 additions and 1,327 deletions.
8 changes: 7 additions & 1 deletion .github/workflows/golang-test-linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,10 @@ jobs:
run: CGO_ENABLED=0 go test -c -o sharedsock-testing.bin ./sharedsock

- name: Generate RouteManager Test bin
run: CGO_ENABLED=1 go test -c -o routemanager-testing.bin -tags netgo -ldflags '-w -extldflags "-static -ldbus-1 -lpcap"' ./client/internal/routemanager/...
run: CGO_ENABLED=0 go test -c -o routemanager-testing.bin ./client/internal/routemanager

- name: Generate SystemOps Test bin
run: CGO_ENABLED=1 go test -c -o systemops-testing.bin -tags netgo -ldflags '-w -extldflags "-static -ldbus-1 -lpcap"' ./client/internal/routemanager/systemops

- name: Generate nftables Manager Test bin
run: CGO_ENABLED=0 go test -c -o nftablesmanager-testing.bin ./client/firewall/nftables/...
Expand All @@ -108,6 +111,9 @@ jobs:
- name: Run RouteManager tests in docker
run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/internal/routemanager --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/routemanager-testing.bin -test.timeout 5m -test.parallel 1

- name: Run SystemOps tests in docker
run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/internal/routemanager/systemops --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/systemops-testing.bin -test.timeout 5m -test.parallel 1

- name: Run nftables Manager tests in docker
run: docker run -t --cap-add=NET_ADMIN --privileged --rm -v $PWD:/ci -w /ci/client/firewall --entrypoint /busybox/sh gcr.io/distroless/base:debug -c /ci/nftablesmanager-testing.bin -test.timeout 5m -test.parallel 1

Expand Down
5 changes: 4 additions & 1 deletion client/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const (
disableAutoConnectFlag = "disable-auto-connect"
serverSSHAllowedFlag = "allow-server-ssh"
extraIFaceBlackListFlag = "extra-iface-blacklist"
dnsRouteIntervalFlag = "dns-router-interval"
)

var (
Expand Down Expand Up @@ -68,7 +69,9 @@ var (
autoConnectDisabled bool
extraIFaceBlackList []string
anonymizeFlag bool
rootCmd = &cobra.Command{
dnsRouteInterval time.Duration

rootCmd = &cobra.Command{
Use: "netbird",
Short: "",
Long: "",
Expand Down
55 changes: 49 additions & 6 deletions client/cmd/route.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cmd

import (
"fmt"
"strings"

"github.com/spf13/cobra"
"google.golang.org/grpc/status"
Expand Down Expand Up @@ -66,16 +67,58 @@ func routesList(cmd *cobra.Command, _ []string) error {
return nil
}

printRoutes(cmd, resp)

return nil
}

func printRoutes(cmd *cobra.Command, resp *proto.ListRoutesResponse) {
cmd.Println("Available Routes:")
for _, route := range resp.Routes {
selectedStatus := "Not Selected"
if route.GetSelected() {
selectedStatus = "Selected"
}
cmd.Printf("\n - ID: %s\n Network: %s\n Status: %s\n", route.GetID(), route.GetNetwork(), selectedStatus)
printRoute(cmd, route)
}
}

return nil
func printRoute(cmd *cobra.Command, route *proto.Route) {
selectedStatus := getSelectedStatus(route)
domains := route.GetDomains()

if len(domains) > 0 {
printDomainRoute(cmd, route, domains, selectedStatus)
} else {
printNetworkRoute(cmd, route, selectedStatus)
}
}

func getSelectedStatus(route *proto.Route) string {
if route.GetSelected() {
return "Selected"
}
return "Not Selected"
}

func printDomainRoute(cmd *cobra.Command, route *proto.Route, domains []string, selectedStatus string) {
cmd.Printf("\n - ID: %s\n Domains: %s\n Status: %s\n", route.GetID(), strings.Join(domains, ", "), selectedStatus)
resolvedIPs := route.GetResolvedIPs()

if len(resolvedIPs) > 0 {
printResolvedIPs(cmd, domains, resolvedIPs)
} else {
cmd.Printf(" Resolved IPs: -\n")
}
}

func printNetworkRoute(cmd *cobra.Command, route *proto.Route, selectedStatus string) {
cmd.Printf("\n - ID: %s\n Network: %s\n Status: %s\n", route.GetID(), route.GetNetwork(), selectedStatus)
}

func printResolvedIPs(cmd *cobra.Command, domains []string, resolvedIPs map[string]*proto.IPList) {
cmd.Printf(" Resolved IPs:\n")
for _, domain := range domains {
if ipList, exists := resolvedIPs[domain]; exists {
cmd.Printf(" [%s]: %s\n", domain, strings.Join(ipList.GetIps(), ", "))
}
}
}

func routesSelect(cmd *cobra.Command, args []string) error {
Expand Down
25 changes: 15 additions & 10 deletions client/cmd/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -807,11 +807,7 @@ func anonymizePeerDetail(a *anonymize.Anonymizer, peer *peerStateDetailOutput) {
}

for i, route := range peer.Routes {
prefix, err := netip.ParsePrefix(route)
if err == nil {
ip := a.AnonymizeIPString(prefix.Addr().String())
peer.Routes[i] = fmt.Sprintf("%s/%d", ip, prefix.Bits())
}
peer.Routes[i] = anonymizeRoute(a, route)
}
}

Expand Down Expand Up @@ -847,12 +843,21 @@ func anonymizeOverview(a *anonymize.Anonymizer, overview *statusOutputOverview)
}

for i, route := range overview.Routes {
prefix, err := netip.ParsePrefix(route)
if err == nil {
ip := a.AnonymizeIPString(prefix.Addr().String())
overview.Routes[i] = fmt.Sprintf("%s/%d", ip, prefix.Bits())
}
overview.Routes[i] = anonymizeRoute(a, route)
}

overview.FQDN = a.AnonymizeDomain(overview.FQDN)
}

func anonymizeRoute(a *anonymize.Anonymizer, route string) string {
prefix, err := netip.ParsePrefix(route)
if err == nil {
ip := a.AnonymizeIPString(prefix.Addr().String())
return fmt.Sprintf("%s/%d", ip, prefix.Bits())
}
domains := strings.Split(route, ", ")
for i, domain := range domains {
domains[i] = a.AnonymizeDomain(domain)
}
return strings.Join(domains, ", ")
}
11 changes: 11 additions & 0 deletions client/cmd/up.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import (
"net/netip"
"runtime"
"strings"
"time"

log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"google.golang.org/grpc/codes"
gstatus "google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/durationpb"

"github.com/netbirdio/netbird/client/internal"
"github.com/netbirdio/netbird/client/internal/peer"
Expand Down Expand Up @@ -42,6 +44,7 @@ func init() {
upCmd.PersistentFlags().Uint16Var(&wireguardPort, wireguardPortFlag, iface.DefaultWgPort, "Wireguard interface listening port")
upCmd.PersistentFlags().BoolVarP(&networkMonitor, networkMonitorFlag, "N", false, "Enable network monitoring")
upCmd.PersistentFlags().StringSliceVar(&extraIFaceBlackList, extraIFaceBlackListFlag, nil, "Extra list of default interfaces to ignore for listening")
upCmd.PersistentFlags().DurationVar(&dnsRouteInterval, dnsRouteIntervalFlag, time.Minute, "DNS route update interval")
}

func upFunc(cmd *cobra.Command, args []string) error {
Expand Down Expand Up @@ -137,6 +140,10 @@ func runInForegroundMode(ctx context.Context, cmd *cobra.Command) error {
}
}

if cmd.Flag(dnsRouteIntervalFlag).Changed {
ic.DNSRouteInterval = &dnsRouteInterval
}

config, err := internal.UpdateOrCreateConfig(ic)
if err != nil {
return fmt.Errorf("get config file: %v", err)
Expand Down Expand Up @@ -237,6 +244,10 @@ func runInDaemonMode(ctx context.Context, cmd *cobra.Command) error {
loginRequest.NetworkMonitor = &networkMonitor
}

if cmd.Flag(dnsRouteIntervalFlag).Changed {
loginRequest.DnsRouteInterval = durationpb.New(dnsRouteInterval)
}

var loginErr error

var loginResp *proto.LoginResponse
Expand Down
30 changes: 30 additions & 0 deletions client/errors/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package errors

import (
"fmt"
"strings"

"github.com/hashicorp/go-multierror"
)

func formatError(es []error) string {
if len(es) == 0 {
return fmt.Sprintf("0 error occurred:\n\t* %s", es[0])
}

points := make([]string, len(es))
for i, err := range es {
points[i] = fmt.Sprintf("* %s", err)
}

return fmt.Sprintf(
"%d errors occurred:\n\t%s",
len(es), strings.Join(points, "\n\t"))
}

func FormatErrorOrNil(err *multierror.Error) error {
if err != nil {
err.ErrorFormat = formatError
}
return err.ErrorOrNil()
}
18 changes: 18 additions & 0 deletions client/internal/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ import (
"os"
"reflect"
"strings"
"time"

log "github.com/sirupsen/logrus"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"

"github.com/netbirdio/netbird/client/internal/routemanager/dynamic"
"github.com/netbirdio/netbird/client/ssh"
"github.com/netbirdio/netbird/iface"
mgm "github.com/netbirdio/netbird/management/client"
Expand Down Expand Up @@ -53,6 +55,7 @@ type ConfigInput struct {
NetworkMonitor *bool
DisableAutoConnect *bool
ExtraIFaceBlackList []string
DNSRouteInterval *time.Duration
}

// Config Configuration type
Expand Down Expand Up @@ -95,6 +98,9 @@ type Config struct {
// DisableAutoConnect determines whether the client should not start with the service
// it's set to false by default due to backwards compatibility
DisableAutoConnect bool

// DNSRouteInterval is the interval in which the DNS routes are updated
DNSRouteInterval time.Duration
}

// ReadConfig read config file and return with Config. If it is not exists create a new with default values
Expand Down Expand Up @@ -357,6 +363,18 @@ func (config *Config) apply(input ConfigInput) (updated bool, err error) {
updated = true
}

if input.DNSRouteInterval != nil && *input.DNSRouteInterval != config.DNSRouteInterval {
log.Infof("updating DNS route interval to %s (old value %s)",
input.DNSRouteInterval.String(), config.DNSRouteInterval.String())
config.DNSRouteInterval = *input.DNSRouteInterval
updated = true
} else if config.DNSRouteInterval == 0 {
config.DNSRouteInterval = dynamic.DefaultInterval
log.Infof("using default DNS route interval %s", config.DNSRouteInterval)
updated = true

}

return updated, nil
}

Expand Down
1 change: 1 addition & 0 deletions client/internal/connect.go
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,7 @@ func createEngineConfig(key wgtypes.Key, config *Config, peerConfig *mgmProto.Pe
RosenpassEnabled: config.RosenpassEnabled,
RosenpassPermissive: config.RosenpassPermissive,
ServerSSHAllowed: util.ReturnBoolWithDefaultTrue(config.ServerSSHAllowed),
DNSRouteInterval: config.DNSRouteInterval,
}

if config.PreSharedKey != "" {
Expand Down
4 changes: 4 additions & 0 deletions client/internal/dns/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ func (w *mocWGIface) Address() iface.WGAddress {
}
}

func (w *mocWGIface) ToInterface() *net.Interface {
panic("implement me")
}

func (w *mocWGIface) GetFilter() iface.PacketFilter {
return w.filter
}
Expand Down
7 changes: 6 additions & 1 deletion client/internal/dns/wgiface.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@

package dns

import "github.com/netbirdio/netbird/iface"
import (
"net"

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

// WGIface defines subset methods of interface required for manager
type WGIface interface {
Name() string
Address() iface.WGAddress
ToInterface() *net.Interface
IsUserspaceBind() bool
GetFilter() iface.PacketFilter
GetDevice() *iface.DeviceWrapper
Expand Down
16 changes: 14 additions & 2 deletions client/internal/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import (
"github.com/netbirdio/netbird/iface"
"github.com/netbirdio/netbird/iface/bind"
mgm "github.com/netbirdio/netbird/management/client"
"github.com/netbirdio/netbird/management/domain"
mgmProto "github.com/netbirdio/netbird/management/proto"
"github.com/netbirdio/netbird/route"
signal "github.com/netbirdio/netbird/signal/client"
Expand Down Expand Up @@ -88,6 +89,8 @@ type EngineConfig struct {
RosenpassPermissive bool

ServerSSHAllowed bool

DNSRouteInterval time.Duration
}

// Engine is a mechanism responsible for reacting on Signal and Management stream events and managing connections to the remote peers.
Expand Down Expand Up @@ -297,7 +300,7 @@ func (e *Engine) Start() error {
}
e.dnsServer = dnsServer

e.routeManager = routemanager.NewManager(e.ctx, e.config.WgPrivateKey.PublicKey().String(), e.wgInterface, e.statusRecorder, initialRoutes)
e.routeManager = routemanager.NewManager(e.ctx, e.config.WgPrivateKey.PublicKey().String(), e.config.DNSRouteInterval, e.wgInterface, e.statusRecorder, initialRoutes)
beforePeerHook, afterPeerHook, err := e.routeManager.Init()
if err != nil {
log.Errorf("Failed to initialize route manager: %s", err)
Expand Down Expand Up @@ -766,15 +769,24 @@ func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error {
func toRoutes(protoRoutes []*mgmProto.Route) []*route.Route {
routes := make([]*route.Route, 0)
for _, protoRoute := range protoRoutes {
_, prefix, _ := route.ParseNetwork(protoRoute.Network)
var prefix netip.Prefix
if len(protoRoute.Domains) == 0 {
var err error
if prefix, err = netip.ParsePrefix(protoRoute.Network); err != nil {
log.Errorf("Failed to parse prefix %s: %v", protoRoute.Network, err)
continue
}
}
convertedRoute := &route.Route{
ID: route.ID(protoRoute.ID),
Network: prefix,
Domains: domain.FromPunycodeList(protoRoute.Domains),
NetID: route.NetID(protoRoute.NetID),
NetworkType: route.NetworkType(protoRoute.NetworkType),
Peer: protoRoute.Peer,
Metric: int(protoRoute.Metric),
Masquerade: protoRoute.Masquerade,
KeepRoute: protoRoute.KeepRoute,
}
routes = append(routes, convertedRoute)
}
Expand Down
2 changes: 1 addition & 1 deletion client/internal/engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ func TestEngine_UpdateNetworkMap(t *testing.T) {
if err != nil {
t.Fatal(err)
}
engine.routeManager = routemanager.NewManager(ctx, key.PublicKey().String(), engine.wgInterface, engine.statusRecorder, nil)
engine.routeManager = routemanager.NewManager(ctx, key.PublicKey().String(), time.Minute, engine.wgInterface, engine.statusRecorder, nil)
engine.dnsServer = &dns.MockServer{
UpdateDNSServerFunc: func(serial uint64, update nbdns.Config) error { return nil },
}
Expand Down
Loading

0 comments on commit ee79f6c

Please sign in to comment.