Compare commits

...

9 Commits

Author SHA1 Message Date
Maycon Santos
5f41e2bd13 Merge branch 'main' into debug-google-workspace 2024-02-19 15:26:58 +01:00
Maycon Santos
cb3408a10b Allow adding 3 nameserver addresses (#1588) 2024-02-19 14:29:20 +01:00
Viktor Liu
0afd738509 Make sure the iOS dialer does not get overwritten (#1585)
* Make sure our iOS dialer does not get overwritten

* set dial timeout for both clients on ios

---------

Co-authored-by: Pascal Fischer <pascal@netbird.io>
2024-02-16 14:37:47 +01:00
bcmmbaga
e3d038da8a debug google workspace request 2024-02-16 13:10:37 +03:00
Maycon Santos
cf87f1e702 Fix/prevent returning error from external cache (#1576)
* Prevent returning error from external cache query

* link comment

* fix spell and remove unnecessary return
2024-02-13 13:10:17 +01:00
Maycon Santos
e890fdae54 Return error when peer is not valid (#1573)
Fix count with invalid peers
2024-02-13 10:59:31 +01:00
Maycon Santos
dd14db6478 Properly handle cache error and return userdata (#1571) 2024-02-12 21:54:16 +01:00
Maycon Santos
88747e3e01 Add an extra server reflexive candidate with WG port (#1549)
sends an extra server reflexive candidate to the remote peer with our related port (usually the Wireguard port)
this is useful when a network has an existing port forwarding rule for the Wireguard port and the local peer and avoids creating a 1:1 NAT on the local network.
2024-02-08 16:50:37 +01:00
Yury Gargay
fb30931365 Expose trusted proxy list and counter configuration for realip middleware (#1535) 2024-02-08 14:40:40 +01:00
13 changed files with 142 additions and 35 deletions

View File

@@ -46,24 +46,32 @@ func (u *upstreamResolverIOS) exchange(ctx context.Context, upstream string, r *
if err != nil {
log.Errorf("error while parsing upstream host: %s", err)
}
timeout := upstreamTimeout
if deadline, ok := ctx.Deadline(); ok {
timeout = time.Until(deadline)
}
client.DialTimeout = timeout
upstreamIP := net.ParseIP(upstreamHost)
if u.lNet.Contains(upstreamIP) || net.IP.IsPrivate(upstreamIP) {
log.Debugf("using private client to query upstream: %s", upstream)
client = u.getClientPrivate()
client = u.getClientPrivate(timeout)
}
return client.ExchangeContext(ctx, r, upstream)
// Cannot use client.ExchangeContext because it overwrites our Dialer
return client.Exchange(r, upstream)
}
// getClientPrivate returns a new DNS client bound to the local IP address of the Netbird interface
// This method is needed for iOS
func (u *upstreamResolverIOS) getClientPrivate() *dns.Client {
func (u *upstreamResolverIOS) getClientPrivate(dialTimeout time.Duration) *dns.Client {
dialer := &net.Dialer{
LocalAddr: &net.UDPAddr{
IP: u.lIP,
Port: 0, // Let the OS pick a free port
},
Timeout: upstreamTimeout,
Timeout: dialTimeout,
Control: func(network, address string, c syscall.RawConn) error {
var operr error
fn := func(s uintptr) {

View File

@@ -130,8 +130,9 @@ type Conn struct {
remoteModeCh chan ModeMessage
meta meta
adapter iface.TunAdapter
iFaceDiscover stdnet.ExternalIFaceDiscover
adapter iface.TunAdapter
iFaceDiscover stdnet.ExternalIFaceDiscover
sentExtraSrflx bool
}
// meta holds meta information about a connection
@@ -464,6 +465,8 @@ func (conn *Conn) cleanup() error {
conn.mu.Lock()
defer conn.mu.Unlock()
conn.sentExtraSrflx = false
var err1, err2, err3 error
if conn.agent != nil {
err1 = conn.agent.Close()
@@ -557,6 +560,30 @@ func (conn *Conn) onICECandidate(candidate ice.Candidate) {
if err != nil {
log.Errorf("failed signaling candidate to the remote peer %s %s", conn.config.Key, err)
}
// sends an extra server reflexive candidate to the remote peer with our related port (usually the wireguard port)
// this is useful when network has an existing port forwarding rule for the wireguard port and this peer
if !conn.sentExtraSrflx && candidate.Type() == ice.CandidateTypeServerReflexive && candidate.Port() != candidate.RelatedAddress().Port {
relatedAdd := candidate.RelatedAddress()
extraSrflx, err := ice.NewCandidateServerReflexive(&ice.CandidateServerReflexiveConfig{
Network: candidate.NetworkType().String(),
Address: candidate.Address(),
Port: relatedAdd.Port,
Component: candidate.Component(),
RelAddr: relatedAdd.Address,
RelPort: relatedAdd.Port,
})
if err != nil {
log.Errorf("failed creating extra server reflexive candidate %s", err)
return
}
err = conn.signalCandidate(extraSrflx)
if err != nil {
log.Errorf("failed signaling the extra server reflexive candidate to the remote peer %s: %s", conn.config.Key, err)
return
}
conn.sentExtraSrflx = true
}
}()
}
}

2
go.mod
View File

@@ -171,3 +171,5 @@ replace github.com/getlantern/systray => github.com/netbirdio/systray v0.0.0-202
replace golang.zx2c4.com/wireguard => github.com/netbirdio/wireguard-go v0.0.0-20240105182236-6c340dd55aed
replace github.com/cloudflare/circl => github.com/cunicu/circl v0.0.0-20230801113412-fec58fc7b5f6
replace github.com/grpc-ecosystem/go-grpc-middleware/v2 => github.com/surik/go-grpc-middleware/v2 v2.0.0-20240206110057-98a38fc1f86f

4
go.sum
View File

@@ -286,8 +286,6 @@ github.com/gopherjs/gopherjs v0.0.0-20220410123724-9e86199038b0 h1:fWY+zXdWhvWnd
github.com/gopherjs/gopherjs v0.0.0-20220410123724-9e86199038b0/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.0.2-0.20240202184442-37827591b26c h1:Kvw2BIua5WGDnknpnODn9K74PYWLhhqt8G3l0chyzEI=
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.0.2-0.20240202184442-37827591b26c/go.mod h1:w9Y7gY31krpLmrVU5ZPG9H7l9fZuRu5/3R3S3FMtVQ4=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/go-secure-stdlib/base62 v0.1.2 h1:ET4pqyjiGmY09R5y+rSd70J2w45CtbWDNvGqWp/R3Ng=
github.com/hashicorp/go-secure-stdlib/base62 v0.1.2/go.mod h1:EdWO6czbmthiwZ3/PUsDV+UD1D5IRU4ActiaWGwt0Yw=
@@ -519,6 +517,8 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/surik/go-grpc-middleware/v2 v2.0.0-20240206110057-98a38fc1f86f h1:J+egXEDkpg/vOYYzPO5IwF8OufGb7g+KcwEF1AWIzhQ=
github.com/surik/go-grpc-middleware/v2 v2.0.0-20240206110057-98a38fc1f86f/go.mod h1:w9Y7gY31krpLmrVU5ZPG9H7l9fZuRu5/3R3S3FMtVQ4=
github.com/things-go/go-socks5 v0.0.4 h1:jMQjIc+qhD4z9cITOMnBiwo9dDmpGuXmBlkRFrl/qD0=
github.com/things-go/go-socks5 v0.0.4/go.mod h1:sh4K6WHrmHZpjxLTCHyYtXYH8OUuD+yZun41NomR1IQ=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=

View File

@@ -15,6 +15,7 @@ import (
"net/url"
"os"
"path"
"slices"
"strings"
"time"
@@ -170,21 +171,31 @@ var (
turnManager := server.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig)
trustedPeers := config.TrustedHTTPProxies
if len(trustedPeers) == 0 {
trustedPeers = []netip.Prefix{netip.MustParsePrefix("0.0.0.0/0")}
trustedPeers := config.ReverseProxy.TrustedPeers
defaultTrustedPeers := []netip.Prefix{netip.MustParsePrefix("0.0.0.0/0"), netip.MustParsePrefix("::/0")}
if len(trustedPeers) == 0 || slices.Equal[[]netip.Prefix](trustedPeers, defaultTrustedPeers) {
log.Warn("TrustedPeers are configured to default value '0.0.0.0/0', '::/0'. This allows connection IP spoofing.")
trustedPeers = defaultTrustedPeers
}
trustedHTTPProxies := config.ReverseProxy.TrustedHTTPProxies
trustedProxiesCount := config.ReverseProxy.TrustedHTTPProxiesCount
if len(trustedHTTPProxies) > 0 && trustedProxiesCount > 0 {
log.Warn("TrustedHTTPProxies and TrustedHTTPProxiesCount both are configured. " +
"This is not recommended way to extract X-Forwarded-For. Consider using one of these options.")
}
realipOpts := realip.Opts{
TrustedPeers: trustedPeers,
TrustedProxies: trustedHTTPProxies,
TrustedProxiesCount: trustedProxiesCount,
Headers: []string{realip.XForwardedFor, realip.XRealIp},
}
headers := []string{realip.XForwardedFor, realip.XRealIp}
gRPCOpts := []grpc.ServerOption{
grpc.KeepaliveEnforcementPolicy(kaep),
grpc.KeepaliveParams(kasp),
grpc.ChainUnaryInterceptor(
realip.UnaryServerInterceptor(trustedPeers, headers),
),
grpc.ChainStreamInterceptor(
realip.StreamServerInterceptor(trustedPeers, headers),
),
grpc.ChainUnaryInterceptor(realip.UnaryServerInterceptorOpts(realipOpts)),
grpc.ChainStreamInterceptor(realip.StreamServerInterceptorOpts(realipOpts)),
}
var certManager *autocert.Manager
var tlsConfig *tls.Config
tlsEnabled := false

View File

@@ -1223,6 +1223,8 @@ func (am *DefaultAccountManager) lookupUserInCache(userID string, account *Accou
}
}
// add extra check on external cache manager. We may get to this point when the user is not yet findable in IDP,
// or it didn't have its metadata updated with am.addAccountIDToIDPAppMeta
user, err := account.FindUser(userID)
if err != nil {
log.Errorf("failed finding user %s in account %s", userID, account.Id)
@@ -1231,14 +1233,11 @@ func (am *DefaultAccountManager) lookupUserInCache(userID string, account *Accou
key := user.IntegrationReference.CacheKey(account.Id, userID)
ud, err := am.externalCacheManager.Get(am.ctx, key)
if err == nil {
log.Errorf("failed to get externalCache for key: %s, error: %s", key, err)
return ud, status.Errorf(status.NotFound, "user %s not found in the IdP", userID)
if err != nil {
log.Debugf("failed to get externalCache for key: %s, error: %s", key, err)
}
log.Infof("user %s not found in any cache", userID)
return nil, nil //nolint:nilnil
return ud, nil
}
func (am *DefaultAccountManager) refreshCache(accountID string) ([]*idp.UserData, error) {

View File

@@ -41,8 +41,6 @@ type Config struct {
HttpConfig *HttpServerConfig
TrustedHTTPProxies []netip.Prefix
IdpManagerConfig *idp.Config
DeviceAuthorizationFlow *DeviceAuthorizationFlow
@@ -50,6 +48,8 @@ type Config struct {
PKCEAuthorizationFlow *PKCEAuthorizationFlow
StoreConfig StoreConfig
ReverseProxy ReverseProxy
}
// GetAuthAudiences returns the audience from the http config and device authorization flow config
@@ -146,6 +146,27 @@ type StoreConfig struct {
Engine StoreEngine
}
// ReverseProxy contains reverse proxy configuration in front of management.
type ReverseProxy struct {
// TrustedHTTPProxies represents a list of trusted HTTP proxies by their IP prefixes.
// When extracting the real IP address from request headers, the middleware will verify
// if the peer's address falls within one of these trusted IP prefixes.
TrustedHTTPProxies []netip.Prefix
// TrustedHTTPProxiesCount specifies the count of trusted HTTP proxies between the internet
// and the server. When using the trusted proxy count method to extract the real IP address,
// the middleware will search the X-Forwarded-For IP list from the rightmost by this count
// minus one.
TrustedHTTPProxiesCount uint
// TrustedPeers represents a list of trusted peers by their IP prefixes.
// These peers are considered trustworthy by the gRPC server operator,
// and the middleware will attempt to extract the real IP address from
// request headers if the peer's address falls within one of these
// trusted IP prefixes.
TrustedPeers []netip.Prefix
}
// validateURL validates input http url
func validateURL(httpURL string) bool {
_, err := url.ParseRequestURI(httpURL)

View File

@@ -115,6 +115,13 @@ func (am *DefaultAccountManager) SaveGroup(accountID, userID string, newGroup *G
if err != nil {
return err
}
for _, peerID := range newGroup.Peers {
if account.Peers[peerID] == nil {
return status.Errorf(status.InvalidArgument, "peer with ID \"%s\" not found", peerID)
}
}
oldGroup, exists := account.Groups[newGroup.ID]
account.Groups[newGroup.ID] = newGroup

View File

@@ -904,7 +904,7 @@ components:
nameservers:
description: Nameserver list
minLength: 1
maxLength: 2
maxLength: 3
type: array
items:
$ref: '#/components/schemas/Nameserver'

View File

@@ -240,10 +240,9 @@ func (h *GroupsHandler) GetGroup(w http.ResponseWriter, r *http.Request) {
func toGroupResponse(account *server.Account, group *server.Group) *api.Group {
cache := make(map[string]api.PeerMinimum)
gr := api.Group{
Id: group.ID,
Name: group.Name,
PeersCount: len(group.Peers),
Issued: &group.Issued,
Id: group.ID,
Name: group.Name,
Issued: &group.Issued,
}
for _, pid := range group.Peers {
@@ -261,5 +260,8 @@ func toGroupResponse(account *server.Account, group *server.Group) *api.Group {
gr.Peers = append(gr.Peers, peerResp)
}
}
gr.PeersCount = len(gr.Peers)
return &gr
}

View File

@@ -5,6 +5,8 @@ import (
"encoding/base64"
"fmt"
"net/http"
"os"
"strconv"
"time"
log "github.com/sirupsen/logrus"
@@ -150,19 +152,37 @@ func (gm *GoogleWorkspaceManager) GetAllAccounts() (map[string][]*UserData, erro
// getAllUsers returns all users in a Google Workspace account filtered by customer ID.
func (gm *GoogleWorkspaceManager) getAllUsers() ([]*UserData, error) {
var usersLimit int64 = 500
if maxUsersLimitEnv := os.Getenv("GOOGLE_WORKSPACE_USERS_LIMIT"); maxUsersLimitEnv != "" {
maxUsersLimit, err := strconv.Atoi(maxUsersLimitEnv)
if err == nil {
log.Debugf("GOOGLE_WORKSPACE_USERS_LIMIT env is set using %d as users limit", maxUsersLimit)
usersLimit = int64(maxUsersLimit)
}
} else {
log.Debugf("GOOGLE_WORKSPACE_USERS_LIMIT env is not set using default users limit 500")
}
users := make([]*UserData, 0)
pageToken := ""
for {
call := gm.usersService.List().Customer(gm.CustomerID).MaxResults(500)
call := gm.usersService.List().Customer(gm.CustomerID).MaxResults(usersLimit)
if pageToken != "" {
call.PageToken(pageToken)
}
resp, err := call.Do()
if err != nil {
log.Debugf("failed to retrieve users from workspace error: %s, http status: %d, headers: %v",
err.Error(),
resp.HTTPStatusCode,
resp.Header,
)
return nil, err
}
log.Debugf("fetched %d users from workspace", len(resp.Users))
for _, user := range resp.Users {
users = append(users, parseGoogleWorkspaceUser(user))
}

View File

@@ -255,8 +255,8 @@ func validateNSGroupName(name, nsGroupID string, nsGroupMap map[string]*nbdns.Na
func validateNSList(list []nbdns.NameServer) error {
nsListLenght := len(list)
if nsListLenght == 0 || nsListLenght > 2 {
return status.Errorf(status.InvalidArgument, "the list of nameservers should be 1 or 2, got %d", len(list))
if nsListLenght == 0 || nsListLenght > 3 {
return status.Errorf(status.InvalidArgument, "the list of nameservers should be 1 or 3, got %d", len(list))
}
return nil
}

View File

@@ -216,7 +216,7 @@ func TestCreateNameServerGroup(t *testing.T) {
shouldCreate: false,
},
{
name: "Create A NS Group With More Than 2 Nameservers Should Fail",
name: "Create A NS Group With More Than 3 Nameservers Should Fail",
inputArgs: input{
name: "super",
description: "super",
@@ -238,6 +238,11 @@ func TestCreateNameServerGroup(t *testing.T) {
NSType: nbdns.UDPNameServerType,
Port: nbdns.DefaultDNSPort,
},
{
IP: netip.MustParseAddr("1.1.4.4"),
NSType: nbdns.UDPNameServerType,
Port: nbdns.DefaultDNSPort,
},
},
enabled: true,
},
@@ -457,6 +462,11 @@ func TestSaveNameServerGroup(t *testing.T) {
NSType: nbdns.UDPNameServerType,
Port: nbdns.DefaultDNSPort,
},
{
IP: netip.MustParseAddr("1.1.4.4"),
NSType: nbdns.UDPNameServerType,
Port: nbdns.DefaultDNSPort,
},
}
invalidID := "doesntExist"
validName := "12345678901234567890qw"