mirror of
https://github.com/netbirdio/netbird.git
synced 2026-06-21 06:42:52 -04:00
Compare commits
8 Commits
v0.24.4
...
poc/netsta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c4c2a8cb2 | ||
|
|
9c6c944e46 | ||
|
|
cec4232860 | ||
|
|
2d1dfa3ae7 | ||
|
|
5961c8330e | ||
|
|
d275d411aa | ||
|
|
5ecafef5d2 | ||
|
|
d073a250cc |
1
.github/workflows/sync-tag.yml
vendored
1
.github/workflows/sync-tag.yml
vendored
@@ -17,6 +17,7 @@ jobs:
|
||||
uses: benc-uk/workflow-dispatch@v1
|
||||
with:
|
||||
workflow: sync-tag.yml
|
||||
ref: main
|
||||
repo: ${{ secrets.UPSTREAM_REPO }}
|
||||
token: ${{ secrets.NC_GITHUB_TOKEN }}
|
||||
inputs: '{ "tag": "${{ github.ref_name }}" }'
|
||||
3
go.mod
3
go.mod
@@ -30,6 +30,7 @@ require (
|
||||
require (
|
||||
fyne.io/fyne/v2 v2.1.4
|
||||
github.com/TheJumpCloud/jcapi-go v3.0.0+incompatible
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
|
||||
github.com/c-robinson/iplib v1.0.3
|
||||
github.com/cilium/ebpf v0.10.0
|
||||
github.com/coreos/go-iptables v0.7.0
|
||||
@@ -109,6 +110,7 @@ require (
|
||||
github.com/go-stack/stack v1.8.0 // indirect
|
||||
github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/google/btree v1.0.1 // indirect
|
||||
github.com/google/s2a-go v0.1.4 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.10.0 // indirect
|
||||
@@ -154,6 +156,7 @@ require (
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
||||
gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gvisor.dev/gvisor v0.0.0-20221203005347-703fd9b7fbc0 // indirect
|
||||
honnef.co/go/tools v0.2.2 // indirect
|
||||
k8s.io/apimachinery v0.23.5 // indirect
|
||||
)
|
||||
|
||||
2
go.sum
2
go.sum
@@ -76,6 +76,8 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFI
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
|
||||
github.com/bazelbuild/rules_go v0.30.0/go.mod h1:MC23Dc/wkXEyk3Wpq6lCqz0ZAYOZDw2DR5y3N1q2i7M=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
|
||||
@@ -20,10 +20,10 @@ func NewWGIFace(iFaceName string, address string, mtu int, tunAdapter TunAdapter
|
||||
return wgIFace, err
|
||||
}
|
||||
|
||||
wgIFace.tun = newTunDevice(iFaceName, wgAddress, mtu, transportNet)
|
||||
|
||||
wgIFace.configurer = newWGConfigurer(iFaceName)
|
||||
wgIFace.userspaceBind = !WireGuardModuleIsLoaded()
|
||||
tun := newTunDevice(iFaceName, wgAddress, mtu, transportNet)
|
||||
wgIFace.tun = tun
|
||||
wgIFace.configurer = newWGConfigurer(tun)
|
||||
wgIFace.userspaceBind = true
|
||||
return wgIFace, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
package iface
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
@@ -11,14 +10,6 @@ import (
|
||||
)
|
||||
|
||||
func (c *tunDevice) Create() error {
|
||||
if WireGuardModuleIsLoaded() {
|
||||
log.Infof("create tun interface with kernel WireGuard support: %s", c.DeviceName())
|
||||
return c.createWithKernel()
|
||||
}
|
||||
|
||||
if !tunModuleIsLoaded() {
|
||||
return fmt.Errorf("couldn't check or load tun module")
|
||||
}
|
||||
log.Infof("create tun interface with userspace WireGuard support: %s", c.DeviceName())
|
||||
var err error
|
||||
c.netInterface, err = c.createWithUserspace()
|
||||
@@ -27,7 +18,6 @@ func (c *tunDevice) Create() error {
|
||||
}
|
||||
|
||||
return c.assignAddr()
|
||||
|
||||
}
|
||||
|
||||
// createWithKernel Creates a new WireGuard interface using kernel WireGuard module.
|
||||
@@ -90,34 +80,7 @@ func (c *tunDevice) createWithKernel() error {
|
||||
|
||||
// assignAddr Adds IP address to the tunnel interface
|
||||
func (c *tunDevice) assignAddr() error {
|
||||
link := newWGLink(c.name)
|
||||
|
||||
//delete existing addresses
|
||||
list, err := netlink.AddrList(link, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(list) > 0 {
|
||||
for _, a := range list {
|
||||
addr := a
|
||||
err = netlink.AddrDel(link, &addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Debugf("adding address %s to interface: %s", c.address.String(), c.name)
|
||||
addr, _ := netlink.ParseAddr(c.address.String())
|
||||
err = netlink.AddrAdd(link, addr)
|
||||
if os.IsExist(err) {
|
||||
log.Infof("interface %s already has the address: %s", c.name, c.address.String())
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
// On linux, the link must be brought up
|
||||
err = netlink.LinkSetUp(link)
|
||||
return err
|
||||
return nil
|
||||
}
|
||||
|
||||
type wgLink struct {
|
||||
|
||||
@@ -10,10 +10,9 @@ import (
|
||||
"golang.zx2c4.com/wireguard/ipc"
|
||||
|
||||
"github.com/netbirdio/netbird/iface/bind"
|
||||
"github.com/netbirdio/netbird/iface/uspproxy"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.zx2c4.com/wireguard/device"
|
||||
"golang.zx2c4.com/wireguard/tun"
|
||||
)
|
||||
|
||||
type tunDevice struct {
|
||||
@@ -25,6 +24,7 @@ type tunDevice struct {
|
||||
uapi net.Listener
|
||||
wrapper *DeviceWrapper
|
||||
close chan struct{}
|
||||
tunDevice *device.Device
|
||||
}
|
||||
|
||||
func newTunDevice(name string, address WGAddress, mtu int, transportNet transport.Net) *tunDevice {
|
||||
@@ -46,6 +46,11 @@ func (c *tunDevice) WgAddress() WGAddress {
|
||||
return c.address
|
||||
}
|
||||
|
||||
func (t *tunDevice) Device() *device.Device {
|
||||
// todo: potential nil pointer exception
|
||||
return t.tunDevice
|
||||
}
|
||||
|
||||
func (c *tunDevice) DeviceName() string {
|
||||
return c.name
|
||||
}
|
||||
@@ -85,15 +90,14 @@ func (c *tunDevice) Close() error {
|
||||
return err3
|
||||
}
|
||||
|
||||
// createWithUserspace Creates a new Wireguard interface, using wireguard-go userspace implementation
|
||||
func (c *tunDevice) createWithUserspace() (NetInterface, error) {
|
||||
tunIface, err := tun.CreateTUN(c.name, c.mtu)
|
||||
nsTun := uspproxy.NewNetStackTun(c.address.IP.String())
|
||||
tunIface, err := nsTun.Create()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.wrapper = newDeviceWrapper(tunIface)
|
||||
|
||||
// We need to create a wireguard-go device and listen to configuration requests
|
||||
tunDev := device.NewDevice(
|
||||
c.wrapper,
|
||||
c.iceBind,
|
||||
@@ -105,33 +109,7 @@ func (c *tunDevice) createWithUserspace() (NetInterface, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.uapi, err = c.getUAPI(c.name)
|
||||
if err != nil {
|
||||
_ = tunIface.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-c.close:
|
||||
log.Debugf("exit uapi.Accept()")
|
||||
return
|
||||
default:
|
||||
}
|
||||
uapiConn, uapiErr := c.uapi.Accept()
|
||||
if uapiErr != nil {
|
||||
log.Traceln("uapi Accept failed with error: ", uapiErr)
|
||||
continue
|
||||
}
|
||||
go func() {
|
||||
tunDev.IpcHandle(uapiConn)
|
||||
log.Debugf("exit tunDevice.IpcHandle")
|
||||
}()
|
||||
}
|
||||
}()
|
||||
|
||||
log.Debugln("UAPI listener started")
|
||||
c.tunDevice = tunDev
|
||||
return tunIface, nil
|
||||
}
|
||||
|
||||
|
||||
43
iface/uspproxy/proxy.go
Normal file
43
iface/uspproxy/proxy.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package uspproxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
"github.com/armon/go-socks5"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Dialer interface {
|
||||
Dial(ctx context.Context, network, addr string) (net.Conn, error)
|
||||
}
|
||||
|
||||
// Proxy todo close server
|
||||
type Proxy struct {
|
||||
server *socks5.Server
|
||||
}
|
||||
|
||||
func NewSocks5(dialer Dialer) (*Proxy, error) {
|
||||
conf := &socks5.Config{
|
||||
Dial: dialer.Dial,
|
||||
}
|
||||
server, err := socks5.New(conf)
|
||||
if err != nil {
|
||||
log.Debugf("failed to init socks5 proxy: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Proxy{
|
||||
server: server,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Proxy) ListenAndServe(addr string) error {
|
||||
go func() {
|
||||
err := s.server.ListenAndServe("tcp", addr)
|
||||
if err != nil {
|
||||
log.Debugf("failed to start socks5 proxy: %s", err)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
55
iface/uspproxy/tun.go
Normal file
55
iface/uspproxy/tun.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package uspproxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/netip"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.zx2c4.com/wireguard/tun"
|
||||
"golang.zx2c4.com/wireguard/tun/netstack"
|
||||
)
|
||||
|
||||
type NSDialer struct {
|
||||
net *netstack.Net
|
||||
}
|
||||
|
||||
func (d *NSDialer) Dial(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
conn, err := d.net.Dial(network, addr)
|
||||
if err != nil {
|
||||
log.Debugf("failed to deal connection: %s", err)
|
||||
}
|
||||
return conn, err
|
||||
}
|
||||
|
||||
type NetStackTun struct {
|
||||
address string
|
||||
|
||||
proxy *Proxy
|
||||
}
|
||||
|
||||
func NewNetStackTun(address string) *NetStackTun {
|
||||
return &NetStackTun{
|
||||
address: address,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *NetStackTun) Create() (tun.Device, error) {
|
||||
nsTunDev, tunNet, err := netstack.CreateNetTUN(
|
||||
[]netip.Addr{netip.MustParseAddr(t.address)},
|
||||
[]netip.Addr{netip.MustParseAddr("8.8.8.8")},
|
||||
1420)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dialer := &NSDialer{tunNet}
|
||||
t.proxy, err = NewSocks5(dialer)
|
||||
if err != nil {
|
||||
// close nsTunDev
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = t.proxy.ListenAndServe("0.0.0.0:1234")
|
||||
return nsTunDev, nil
|
||||
}
|
||||
@@ -1,205 +0,0 @@
|
||||
//go:build !android
|
||||
|
||||
package iface
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.zx2c4.com/wireguard/wgctrl"
|
||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||
)
|
||||
|
||||
type wGConfigurer struct {
|
||||
deviceName string
|
||||
}
|
||||
|
||||
func newWGConfigurer(deviceName string) wGConfigurer {
|
||||
return wGConfigurer{
|
||||
deviceName: deviceName,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *wGConfigurer) configureInterface(privateKey string, port int) error {
|
||||
log.Debugf("adding Wireguard private key")
|
||||
key, err := wgtypes.ParseKey(privateKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fwmark := 0
|
||||
config := wgtypes.Config{
|
||||
PrivateKey: &key,
|
||||
ReplacePeers: true,
|
||||
FirewallMark: &fwmark,
|
||||
ListenPort: &port,
|
||||
}
|
||||
|
||||
err = c.configure(config)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`received error "%w" while configuring interface %s with port %d`, err, c.deviceName, port)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *wGConfigurer) updatePeer(peerKey string, allowedIps string, keepAlive time.Duration, endpoint *net.UDPAddr, preSharedKey *wgtypes.Key) error {
|
||||
//parse allowed ips
|
||||
_, ipNet, err := net.ParseCIDR(allowedIps)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
peerKeyParsed, err := wgtypes.ParseKey(peerKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
peer := wgtypes.PeerConfig{
|
||||
PublicKey: peerKeyParsed,
|
||||
ReplaceAllowedIPs: true,
|
||||
AllowedIPs: []net.IPNet{*ipNet},
|
||||
PersistentKeepaliveInterval: &keepAlive,
|
||||
PresharedKey: preSharedKey,
|
||||
Endpoint: endpoint,
|
||||
}
|
||||
|
||||
config := wgtypes.Config{
|
||||
Peers: []wgtypes.PeerConfig{peer},
|
||||
}
|
||||
err = c.configure(config)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`received error "%w" while updating peer on interface %s with settings: allowed ips %s, endpoint %s`, err, c.deviceName, allowedIps, endpoint.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *wGConfigurer) removePeer(peerKey string) error {
|
||||
peerKeyParsed, err := wgtypes.ParseKey(peerKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
peer := wgtypes.PeerConfig{
|
||||
PublicKey: peerKeyParsed,
|
||||
Remove: true,
|
||||
}
|
||||
|
||||
config := wgtypes.Config{
|
||||
Peers: []wgtypes.PeerConfig{peer},
|
||||
}
|
||||
err = c.configure(config)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`received error "%w" while removing peer %s from interface %s`, err, peerKey, c.deviceName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *wGConfigurer) addAllowedIP(peerKey string, allowedIP string) error {
|
||||
_, ipNet, err := net.ParseCIDR(allowedIP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
peerKeyParsed, err := wgtypes.ParseKey(peerKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
peer := wgtypes.PeerConfig{
|
||||
PublicKey: peerKeyParsed,
|
||||
UpdateOnly: true,
|
||||
ReplaceAllowedIPs: false,
|
||||
AllowedIPs: []net.IPNet{*ipNet},
|
||||
}
|
||||
|
||||
config := wgtypes.Config{
|
||||
Peers: []wgtypes.PeerConfig{peer},
|
||||
}
|
||||
err = c.configure(config)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`received error "%w" while adding allowed Ip to peer on interface %s with settings: allowed ips %s`, err, c.deviceName, allowedIP)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *wGConfigurer) removeAllowedIP(peerKey string, allowedIP string) error {
|
||||
_, ipNet, err := net.ParseCIDR(allowedIP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
peerKeyParsed, err := wgtypes.ParseKey(peerKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
existingPeer, err := c.getPeer(c.deviceName, peerKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newAllowedIPs := existingPeer.AllowedIPs
|
||||
|
||||
for i, existingAllowedIP := range existingPeer.AllowedIPs {
|
||||
if existingAllowedIP.String() == ipNet.String() {
|
||||
newAllowedIPs = append(existingPeer.AllowedIPs[:i], existingPeer.AllowedIPs[i+1:]...) //nolint:gocritic
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
peer := wgtypes.PeerConfig{
|
||||
PublicKey: peerKeyParsed,
|
||||
UpdateOnly: true,
|
||||
ReplaceAllowedIPs: true,
|
||||
AllowedIPs: newAllowedIPs,
|
||||
}
|
||||
|
||||
config := wgtypes.Config{
|
||||
Peers: []wgtypes.PeerConfig{peer},
|
||||
}
|
||||
err = c.configure(config)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`received error "%w" while removing allowed IP from peer on interface %s with settings: allowed ips %s`, err, c.deviceName, allowedIP)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *wGConfigurer) getPeer(ifaceName, peerPubKey string) (wgtypes.Peer, error) {
|
||||
wg, err := wgctrl.New()
|
||||
if err != nil {
|
||||
return wgtypes.Peer{}, err
|
||||
}
|
||||
defer func() {
|
||||
err = wg.Close()
|
||||
if err != nil {
|
||||
log.Errorf("got error while closing wgctl: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
wgDevice, err := wg.Device(ifaceName)
|
||||
if err != nil {
|
||||
return wgtypes.Peer{}, err
|
||||
}
|
||||
for _, peer := range wgDevice.Peers {
|
||||
if peer.PublicKey.String() == peerPubKey {
|
||||
return peer, nil
|
||||
}
|
||||
}
|
||||
return wgtypes.Peer{}, fmt.Errorf("peer not found")
|
||||
}
|
||||
|
||||
func (c *wGConfigurer) configure(config wgtypes.Config) error {
|
||||
wg, err := wgctrl.New()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer wg.Close()
|
||||
|
||||
// validate if device with name exists
|
||||
_, err = wg.Device(c.deviceName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Tracef("got Wireguard device %s", c.deviceName)
|
||||
|
||||
return wg.ConfigureDevice(c.deviceName, config)
|
||||
}
|
||||
@@ -83,7 +83,10 @@ var (
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed reading provided config file: %s: %v", mgmtConfig, err)
|
||||
}
|
||||
config.HttpConfig.IdpSignKeyRefreshEnabled = idpSignKeyRefreshEnabled
|
||||
|
||||
if cmd.Flag(idpSignKeyRefreshEnabledFlagName).Changed {
|
||||
config.HttpConfig.IdpSignKeyRefreshEnabled = idpSignKeyRefreshEnabled
|
||||
}
|
||||
|
||||
tlsEnabled := false
|
||||
if mgmtLetsencryptDomain != "" || (config.HttpConfig.CertFile != "" && config.HttpConfig.CertKey != "") {
|
||||
|
||||
@@ -12,7 +12,8 @@ import (
|
||||
|
||||
const (
|
||||
// ExitSetupFailed defines exit code
|
||||
ExitSetupFailed = 1
|
||||
ExitSetupFailed = 1
|
||||
idpSignKeyRefreshEnabledFlagName = "idp-sign-key-refresh-enabled"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -62,7 +63,7 @@ func init() {
|
||||
mgmtCmd.Flags().StringVar(&certKey, "cert-key", "", "Location of your SSL certificate private key. Can be used when you have an existing certificate and don't want a new certificate be generated automatically. If letsencrypt-domain is specified this property has no effect")
|
||||
mgmtCmd.Flags().BoolVar(&disableMetrics, "disable-anonymous-metrics", false, "disables push of anonymous usage metrics to NetBird")
|
||||
mgmtCmd.Flags().StringVar(&dnsDomain, "dns-domain", defaultSingleAccModeDomain, fmt.Sprintf("Domain used for peer resolution. This is appended to the peer's name, e.g. pi-server. %s. Max length is 192 characters to allow appending to a peer name with up to 63 characters.", defaultSingleAccModeDomain))
|
||||
mgmtCmd.Flags().BoolVar(&idpSignKeyRefreshEnabled, "idp-sign-key-refresh-enabled", false, "Enable cache headers evaluation to determine signing key rotation period. This will refresh the signing key upon expiry.")
|
||||
mgmtCmd.Flags().BoolVar(&idpSignKeyRefreshEnabled, idpSignKeyRefreshEnabledFlagName, false, "Enable cache headers evaluation to determine signing key rotation period. This will refresh the signing key upon expiry.")
|
||||
mgmtCmd.Flags().BoolVar(&userDeleteFromIDPEnabled, "user-delete-from-idp", false, "Allows to delete user from IDP when user is deleted from account")
|
||||
rootCmd.MarkFlagRequired("config") //nolint
|
||||
|
||||
|
||||
@@ -164,6 +164,9 @@ type Settings struct {
|
||||
// JWTGroupsClaimName from which we extract groups name to add it to account groups
|
||||
JWTGroupsClaimName string
|
||||
|
||||
// JWTAllowGroups list of groups to which users are allowed access
|
||||
JWTAllowGroups []string `gorm:"serializer:json"`
|
||||
|
||||
// Extra is a dictionary of Account settings
|
||||
Extra *account.ExtraSettings `gorm:"embedded;embeddedPrefix:extra_"`
|
||||
}
|
||||
@@ -176,6 +179,7 @@ func (s *Settings) Copy() *Settings {
|
||||
JWTGroupsEnabled: s.JWTGroupsEnabled,
|
||||
JWTGroupsClaimName: s.JWTGroupsClaimName,
|
||||
GroupsPropagationEnabled: s.GroupsPropagationEnabled,
|
||||
JWTAllowGroups: s.JWTAllowGroups,
|
||||
}
|
||||
if s.Extra != nil {
|
||||
settings.Extra = s.Extra.Copy()
|
||||
|
||||
@@ -91,6 +91,9 @@ func (h *AccountsHandler) UpdateAccount(w http.ResponseWriter, r *http.Request)
|
||||
if req.Settings.JwtGroupsClaimName != nil {
|
||||
settings.JWTGroupsClaimName = *req.Settings.JwtGroupsClaimName
|
||||
}
|
||||
if req.Settings.JwtAllowGroups != nil {
|
||||
settings.JWTAllowGroups = *req.Settings.JwtAllowGroups
|
||||
}
|
||||
|
||||
updatedAccount, err := h.accountManager.UpdateAccountSettings(accountID, user.Id, settings)
|
||||
if err != nil {
|
||||
@@ -128,12 +131,18 @@ func (h *AccountsHandler) DeleteAccount(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
func toAccountResponse(account *server.Account) *api.Account {
|
||||
jwtAllowGroups := account.Settings.JWTAllowGroups
|
||||
if jwtAllowGroups == nil {
|
||||
jwtAllowGroups = []string{}
|
||||
}
|
||||
|
||||
settings := api.AccountSettings{
|
||||
PeerLoginExpiration: int(account.Settings.PeerLoginExpiration.Seconds()),
|
||||
PeerLoginExpirationEnabled: account.Settings.PeerLoginExpirationEnabled,
|
||||
GroupsPropagationEnabled: &account.Settings.GroupsPropagationEnabled,
|
||||
JwtGroupsEnabled: &account.Settings.JWTGroupsEnabled,
|
||||
JwtGroupsClaimName: &account.Settings.JWTGroupsClaimName,
|
||||
JwtAllowGroups: &jwtAllowGroups,
|
||||
}
|
||||
|
||||
if account.Settings.Extra != nil {
|
||||
|
||||
@@ -95,6 +95,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
||||
GroupsPropagationEnabled: br(false),
|
||||
JwtGroupsClaimName: sr(""),
|
||||
JwtGroupsEnabled: br(false),
|
||||
JwtAllowGroups: &[]string{},
|
||||
},
|
||||
expectedArray: true,
|
||||
expectedID: accountID,
|
||||
@@ -112,6 +113,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
||||
GroupsPropagationEnabled: br(false),
|
||||
JwtGroupsClaimName: sr(""),
|
||||
JwtGroupsEnabled: br(false),
|
||||
JwtAllowGroups: &[]string{},
|
||||
},
|
||||
expectedArray: false,
|
||||
expectedID: accountID,
|
||||
@@ -121,7 +123,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
||||
expectedBody: true,
|
||||
requestType: http.MethodPut,
|
||||
requestPath: "/api/accounts/" + accountID,
|
||||
requestBody: bytes.NewBufferString("{\"settings\": {\"peer_login_expiration\": 15552000,\"peer_login_expiration_enabled\": false,\"jwt_groups_enabled\":true,\"jwt_groups_claim_name\":\"roles\"}}"),
|
||||
requestBody: bytes.NewBufferString("{\"settings\": {\"peer_login_expiration\": 15552000,\"peer_login_expiration_enabled\": false,\"jwt_groups_enabled\":true,\"jwt_groups_claim_name\":\"roles\",\"jwt_allow_groups\":[\"test\"]}}"),
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedSettings: api.AccountSettings{
|
||||
PeerLoginExpiration: 15552000,
|
||||
@@ -129,6 +131,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
||||
GroupsPropagationEnabled: br(false),
|
||||
JwtGroupsClaimName: sr("roles"),
|
||||
JwtGroupsEnabled: br(true),
|
||||
JwtAllowGroups: &[]string{"test"},
|
||||
},
|
||||
expectedArray: false,
|
||||
expectedID: accountID,
|
||||
@@ -146,6 +149,7 @@ func TestAccounts_AccountsHandler(t *testing.T) {
|
||||
GroupsPropagationEnabled: br(true),
|
||||
JwtGroupsClaimName: sr("groups"),
|
||||
JwtGroupsEnabled: br(true),
|
||||
JwtAllowGroups: &[]string{},
|
||||
},
|
||||
expectedArray: false,
|
||||
expectedID: accountID,
|
||||
|
||||
@@ -66,6 +66,12 @@ components:
|
||||
description: Name of the claim from which we extract groups names to add it to account groups.
|
||||
type: string
|
||||
example: "roles"
|
||||
jwt_allow_groups:
|
||||
description: List of groups to which users are allowed access
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
example: Administrators
|
||||
extra:
|
||||
$ref: '#/components/schemas/AccountExtraSettings'
|
||||
required:
|
||||
|
||||
@@ -160,6 +160,9 @@ type AccountSettings struct {
|
||||
// GroupsPropagationEnabled Allows propagate the new user auto groups to peers that belongs to the user
|
||||
GroupsPropagationEnabled *bool `json:"groups_propagation_enabled,omitempty"`
|
||||
|
||||
// JwtAllowGroups List of groups to which users are allowed access
|
||||
JwtAllowGroups *[]string `json:"jwt_allow_groups,omitempty"`
|
||||
|
||||
// JwtGroupsClaimName Name of the claim from which we extract groups names to add it to account groups.
|
||||
JwtGroupsClaimName *string `json:"jwt_groups_claim_name,omitempty"`
|
||||
|
||||
|
||||
@@ -34,12 +34,20 @@ type emptyObject struct {
|
||||
|
||||
// APIHandler creates the Management service HTTP API handler registering all the available endpoints.
|
||||
func APIHandler(accountManager s.AccountManager, jwtValidator jwtclaims.JWTValidator, appMetrics telemetry.AppMetrics, authCfg AuthCfg) (http.Handler, error) {
|
||||
claimsExtractor := jwtclaims.NewClaimsExtractor(
|
||||
jwtclaims.WithAudience(authCfg.Audience),
|
||||
jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
|
||||
)
|
||||
|
||||
authMiddleware := middleware.NewAuthMiddleware(
|
||||
accountManager.GetAccountFromPAT,
|
||||
jwtValidator.ValidateAndParse,
|
||||
accountManager.MarkPATUsed,
|
||||
accountManager.GetAccountFromToken,
|
||||
claimsExtractor,
|
||||
authCfg.Audience,
|
||||
authCfg.UserIDClaim)
|
||||
authCfg.UserIDClaim,
|
||||
)
|
||||
|
||||
corsMiddleware := cors.AllowAll()
|
||||
|
||||
@@ -60,11 +68,6 @@ func APIHandler(accountManager s.AccountManager, jwtValidator jwtclaims.JWTValid
|
||||
AuthCfg: authCfg,
|
||||
}
|
||||
|
||||
claimsExtractor := jwtclaims.NewClaimsExtractor(
|
||||
jwtclaims.WithAudience(authCfg.Audience),
|
||||
jwtclaims.WithUserIDClaim(authCfg.UserIDClaim),
|
||||
)
|
||||
|
||||
integrations.RegisterHandlers(api.Router, accountManager, claimsExtractor)
|
||||
api.addAccountsEndpoint()
|
||||
api.addPeersEndpoint()
|
||||
|
||||
@@ -26,11 +26,16 @@ type ValidateAndParseTokenFunc func(token string) (*jwt.Token, error)
|
||||
// MarkPATUsedFunc function
|
||||
type MarkPATUsedFunc func(token string) error
|
||||
|
||||
// GetAccountFromTokenFunc function
|
||||
type GetAccountFromTokenFunc func(claims jwtclaims.AuthorizationClaims) (*server.Account, *server.User, error)
|
||||
|
||||
// AuthMiddleware middleware to verify personal access tokens (PAT) and JWT tokens
|
||||
type AuthMiddleware struct {
|
||||
getAccountFromPAT GetAccountFromPATFunc
|
||||
validateAndParseToken ValidateAndParseTokenFunc
|
||||
markPATUsed MarkPATUsedFunc
|
||||
getAccountFromToken GetAccountFromTokenFunc
|
||||
claimsExtractor *jwtclaims.ClaimsExtractor
|
||||
audience string
|
||||
userIDClaim string
|
||||
}
|
||||
@@ -40,14 +45,19 @@ const (
|
||||
)
|
||||
|
||||
// NewAuthMiddleware instance constructor
|
||||
func NewAuthMiddleware(getAccountFromPAT GetAccountFromPATFunc, validateAndParseToken ValidateAndParseTokenFunc, markPATUsed MarkPATUsedFunc, audience string, userIdClaim string) *AuthMiddleware {
|
||||
func NewAuthMiddleware(getAccountFromPAT GetAccountFromPATFunc, validateAndParseToken ValidateAndParseTokenFunc,
|
||||
markPATUsed MarkPATUsedFunc, getAccountFromToken GetAccountFromTokenFunc, claimsExtractor *jwtclaims.ClaimsExtractor,
|
||||
audience string, userIdClaim string) *AuthMiddleware {
|
||||
if userIdClaim == "" {
|
||||
userIdClaim = jwtclaims.UserIDClaim
|
||||
}
|
||||
|
||||
return &AuthMiddleware{
|
||||
getAccountFromPAT: getAccountFromPAT,
|
||||
validateAndParseToken: validateAndParseToken,
|
||||
markPATUsed: markPATUsed,
|
||||
getAccountFromToken: getAccountFromToken,
|
||||
claimsExtractor: claimsExtractor,
|
||||
audience: audience,
|
||||
userIDClaim: userIdClaim,
|
||||
}
|
||||
@@ -107,6 +117,10 @@ func (m *AuthMiddleware) checkJWTFromRequest(w http.ResponseWriter, r *http.Requ
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := m.verifyUserAccess(validatedToken); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If we get here, everything worked and we can set the
|
||||
// user property in context.
|
||||
newRequest := r.WithContext(context.WithValue(r.Context(), userProperty, validatedToken)) //nolint
|
||||
@@ -115,6 +129,41 @@ func (m *AuthMiddleware) checkJWTFromRequest(w http.ResponseWriter, r *http.Requ
|
||||
return nil
|
||||
}
|
||||
|
||||
// verifyUserAccess checks if a user, based on a validated JWT token,
|
||||
// is allowed access, particularly in cases where the admin enabled JWT
|
||||
// group propagation and designated certain groups with access permissions.
|
||||
func (m *AuthMiddleware) verifyUserAccess(validatedToken *jwt.Token) error {
|
||||
authClaims := m.claimsExtractor.FromToken(validatedToken)
|
||||
account, _, err := m.getAccountFromToken(authClaims)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get the account from token: %w", err)
|
||||
}
|
||||
|
||||
// Ensures JWT group synchronization to the management is enabled before,
|
||||
// filtering access based on the allowed groups.
|
||||
if account.Settings != nil && account.Settings.JWTGroupsEnabled {
|
||||
if allowedGroups := account.Settings.JWTAllowGroups; len(allowedGroups) > 0 {
|
||||
userJWTGroups := make([]string, 0)
|
||||
|
||||
if claim, ok := authClaims.Raw[account.Settings.JWTGroupsClaimName]; ok {
|
||||
if claimGroups, ok := claim.([]interface{}); ok {
|
||||
for _, g := range claimGroups {
|
||||
if group, ok := g.(string); ok {
|
||||
userJWTGroups = append(userJWTGroups, group)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !userHasAllowedGroup(allowedGroups, userJWTGroups) {
|
||||
return fmt.Errorf("user does not belong to any of the allowed JWT groups")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckPATFromRequest checks if the PAT is valid
|
||||
func (m *AuthMiddleware) checkPATFromRequest(w http.ResponseWriter, r *http.Request, auth []string) error {
|
||||
token, err := getTokenFromPATRequest(auth)
|
||||
@@ -168,3 +217,15 @@ func getTokenFromPATRequest(authHeaderParts []string) (string, error) {
|
||||
|
||||
return authHeaderParts[1], nil
|
||||
}
|
||||
|
||||
// userHasAllowedGroup checks if a user belongs to any of the allowed groups.
|
||||
func userHasAllowedGroup(allowedGroups []string, userGroups []string) bool {
|
||||
for _, userGroup := range userGroups {
|
||||
for _, allowedGroup := range allowedGroups {
|
||||
if userGroup == allowedGroup {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/golang-jwt/jwt"
|
||||
|
||||
"github.com/netbirdio/netbird/management/server"
|
||||
"github.com/netbirdio/netbird/management/server/jwtclaims"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -54,7 +55,13 @@ func mockGetAccountFromPAT(token string) (*server.Account, *server.User, *server
|
||||
|
||||
func mockValidateAndParseToken(token string) (*jwt.Token, error) {
|
||||
if token == JWT {
|
||||
return &jwt.Token{}, nil
|
||||
return &jwt.Token{
|
||||
Claims: jwt.MapClaims{
|
||||
userIDClaim: userID,
|
||||
audience + jwtclaims.AccountIDSuffix: accountID,
|
||||
},
|
||||
Valid: true,
|
||||
}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("JWT invalid")
|
||||
}
|
||||
@@ -66,6 +73,19 @@ func mockMarkPATUsed(token string) error {
|
||||
return fmt.Errorf("Should never get reached")
|
||||
}
|
||||
|
||||
func mockGetAccountFromToken(claims jwtclaims.AuthorizationClaims) (*server.Account, *server.User, error) {
|
||||
if testAccount.Id != claims.AccountId {
|
||||
return nil, nil, fmt.Errorf("account with id %s does not exist", claims.AccountId)
|
||||
}
|
||||
|
||||
user, ok := testAccount.Users[claims.UserId]
|
||||
if !ok {
|
||||
return nil, nil, fmt.Errorf("user with id %s does not exist", claims.UserId)
|
||||
}
|
||||
|
||||
return testAccount, user, nil
|
||||
}
|
||||
|
||||
func TestAuthMiddleware_Handler(t *testing.T) {
|
||||
tt := []struct {
|
||||
name string
|
||||
@@ -108,7 +128,20 @@ func TestAuthMiddleware_Handler(t *testing.T) {
|
||||
// do nothing
|
||||
})
|
||||
|
||||
authMiddleware := NewAuthMiddleware(mockGetAccountFromPAT, mockValidateAndParseToken, mockMarkPATUsed, audience, userIDClaim)
|
||||
claimsExtractor := jwtclaims.NewClaimsExtractor(
|
||||
jwtclaims.WithAudience(audience),
|
||||
jwtclaims.WithUserIDClaim(userIDClaim),
|
||||
)
|
||||
|
||||
authMiddleware := NewAuthMiddleware(
|
||||
mockGetAccountFromPAT,
|
||||
mockValidateAndParseToken,
|
||||
mockMarkPATUsed,
|
||||
mockGetAccountFromToken,
|
||||
claimsExtractor,
|
||||
audience,
|
||||
userIDClaim,
|
||||
)
|
||||
|
||||
handlerToTest := authMiddleware.Handler(nextHandler)
|
||||
|
||||
|
||||
@@ -108,6 +108,8 @@ func NewJWTValidator(issuer string, audienceList []string, keysLocation string,
|
||||
refreshedKeys = keys
|
||||
}
|
||||
|
||||
log.Debugf("keys refreshed, new UTC expiration time: %s", refreshedKeys.expiresInTime.UTC())
|
||||
|
||||
keys = refreshedKeys
|
||||
}
|
||||
}
|
||||
@@ -179,7 +181,7 @@ func (m *JWTValidator) ValidateAndParse(token string) (*jwt.Token, error) {
|
||||
|
||||
// stillValid returns true if the JSONWebKey still valid and have enough time to be used
|
||||
func (jwks *Jwks) stillValid() bool {
|
||||
return jwks.expiresInTime.IsZero() && time.Now().Add(5*time.Second).Before(jwks.expiresInTime)
|
||||
return !jwks.expiresInTime.IsZero() && time.Now().Add(5*time.Second).Before(jwks.expiresInTime)
|
||||
}
|
||||
|
||||
func getPemKeys(keysLocation string) (*Jwks, error) {
|
||||
|
||||
@@ -366,7 +366,7 @@ func (am *MockAccountManager) GetUser(claims jwtclaims.AuthorizationClaims) (*se
|
||||
|
||||
func (am *MockAccountManager) ListUsers(accountID string) ([]*server.User, error) {
|
||||
if am.ListUsersFunc != nil {
|
||||
return am.ListUsers(accountID)
|
||||
return am.ListUsersFunc(accountID)
|
||||
}
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListUsers is not implemented")
|
||||
}
|
||||
@@ -464,7 +464,7 @@ func (am *MockAccountManager) SaveUser(accountID, userID string, user *server.Us
|
||||
|
||||
// SaveOrAddUser mocks SaveOrAddUser of the AccountManager interface
|
||||
func (am *MockAccountManager) SaveOrAddUser(accountID, userID string, user *server.User, addIfNotExists bool) (*server.UserInfo, error) {
|
||||
if am.SaveUserFunc != nil {
|
||||
if am.SaveOrAddUserFunc != nil {
|
||||
return am.SaveOrAddUserFunc(accountID, userID, user, addIfNotExists)
|
||||
}
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SaveOrAddUser is not implemented")
|
||||
@@ -545,7 +545,7 @@ func (am *MockAccountManager) GetAccountFromToken(claims jwtclaims.Authorization
|
||||
|
||||
// GetPeers mocks GetPeers of the AccountManager interface
|
||||
func (am *MockAccountManager) GetPeers(accountID, userID string) ([]*nbpeer.Peer, error) {
|
||||
if am.GetAccountFromTokenFunc != nil {
|
||||
if am.GetPeersFunc != nil {
|
||||
return am.GetPeersFunc(accountID, userID)
|
||||
}
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetPeers is not implemented")
|
||||
|
||||
Reference in New Issue
Block a user