mirror of
https://github.com/netbirdio/netbird.git
synced 2025-06-01 07:35:35 +02:00
Turn credentials generation (#102)
* abstract peer channel * remove wip code * refactor NewServer with Peer updates channel * feature: add TURN credentials manager * hmac logic * example test function * test: add TimeBasedAuthSecretsManager_GenerateCredentials test * test: make tests for now with hardcoded secret * test: add TimeBasedAuthSecretsManager_SetupRefresh test * test: add TimeBasedAuthSecretsManager_SetupRefresh test * test: add TimeBasedAuthSecretsManager_CancelRefresh test * feature: extract TURNConfig to the management config * feature: return hash based TURN credentials only on initial sync * feature: make TURN time based secret credentials optional Co-authored-by: mlsmaycon <mlsmaycon@gmail.com>
This commit is contained in:
parent
86f3b1e5c8
commit
b17424d630
@ -39,7 +39,8 @@ func startManagement(config *mgmt.Config, t *testing.T) (*grpc.Server, net.Liste
|
|||||||
|
|
||||||
accountManager := mgmt.NewManager(store)
|
accountManager := mgmt.NewManager(store)
|
||||||
peersUpdateManager := mgmt.NewPeersUpdateManager()
|
peersUpdateManager := mgmt.NewPeersUpdateManager()
|
||||||
mgmtServer, err := mgmt.NewServer(config, accountManager, peersUpdateManager)
|
turnManager := mgmt.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig)
|
||||||
|
mgmtServer, err := mgmt.NewServer(config, accountManager, peersUpdateManager, turnManager)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
21
client/testdata/management.json
vendored
21
client/testdata/management.json
vendored
@ -7,14 +7,19 @@
|
|||||||
"Password": null
|
"Password": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"Turns": [
|
"TURNConfig": {
|
||||||
{
|
"Turns": [
|
||||||
"Proto": "udp",
|
{
|
||||||
"URI": "turn:stun.wiretrustee.com:3468",
|
"Proto": "udp",
|
||||||
"Username": "some_user",
|
"URI": "turn:stun.wiretrustee.com:3468",
|
||||||
"Password": "c29tZV9wYXNzd29yZA=="
|
"Username": "some_user",
|
||||||
}
|
"Password": "c29tZV9wYXNzd29yZA=="
|
||||||
],
|
}
|
||||||
|
],
|
||||||
|
"CredentialsTTL": "1h",
|
||||||
|
"Secret": "c29tZV9wYXNzd29yZA==",
|
||||||
|
"TimeBasedCredentials": true
|
||||||
|
},
|
||||||
"Signal": {
|
"Signal": {
|
||||||
"Proto": "http",
|
"Proto": "http",
|
||||||
"URI": "signal.wiretrustee.com:10000",
|
"URI": "signal.wiretrustee.com:10000",
|
||||||
|
@ -62,7 +62,8 @@ func startManagement(config *mgmt.Config, t *testing.T) (*grpc.Server, net.Liste
|
|||||||
|
|
||||||
accountManager := mgmt.NewManager(store)
|
accountManager := mgmt.NewManager(store)
|
||||||
peersUpdateManager := mgmt.NewPeersUpdateManager()
|
peersUpdateManager := mgmt.NewPeersUpdateManager()
|
||||||
mgmtServer, err := mgmt.NewServer(config, accountManager, peersUpdateManager)
|
turnManager := mgmt.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig)
|
||||||
|
mgmtServer, err := mgmt.NewServer(config, accountManager, peersUpdateManager, turnManager)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -79,7 +79,8 @@ var (
|
|||||||
opts = append(opts, grpc.KeepaliveEnforcementPolicy(kaep), grpc.KeepaliveParams(kasp))
|
opts = append(opts, grpc.KeepaliveEnforcementPolicy(kaep), grpc.KeepaliveParams(kasp))
|
||||||
grpcServer := grpc.NewServer(opts...)
|
grpcServer := grpc.NewServer(opts...)
|
||||||
peersUpdateManager := server.NewPeersUpdateManager()
|
peersUpdateManager := server.NewPeersUpdateManager()
|
||||||
server, err := server.NewServer(config, accountManager, peersUpdateManager)
|
turnManager := server.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig)
|
||||||
|
server, err := server.NewServer(config, accountManager, peersUpdateManager, turnManager)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("failed creating new server: %v", err)
|
log.Fatalf("failed creating new server: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/wiretrustee/wiretrustee/util"
|
||||||
|
)
|
||||||
|
|
||||||
type Protocol string
|
type Protocol string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -12,15 +16,23 @@ const (
|
|||||||
|
|
||||||
// Config of the Management service
|
// Config of the Management service
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Stuns []*Host
|
Stuns []*Host
|
||||||
Turns []*Host
|
TURNConfig *TURNConfig
|
||||||
Signal *Host
|
Signal *Host
|
||||||
|
|
||||||
Datadir string
|
Datadir string
|
||||||
|
|
||||||
HttpConfig *HttpServerConfig
|
HttpConfig *HttpServerConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TURNConfig is a config of the TURNCredentialsManager
|
||||||
|
type TURNConfig struct {
|
||||||
|
TimeBasedCredentials bool
|
||||||
|
CredentialsTTL util.Duration
|
||||||
|
Secret []byte
|
||||||
|
Turns []*Host
|
||||||
|
}
|
||||||
|
|
||||||
// HttpServerConfig is a config of the HTTP Management service server
|
// HttpServerConfig is a config of the HTTP Management service server
|
||||||
type HttpServerConfig struct {
|
type HttpServerConfig struct {
|
||||||
LetsEncryptDomain string
|
LetsEncryptDomain string
|
||||||
|
@ -19,15 +19,16 @@ type Server struct {
|
|||||||
accountManager *AccountManager
|
accountManager *AccountManager
|
||||||
wgKey wgtypes.Key
|
wgKey wgtypes.Key
|
||||||
proto.UnimplementedManagementServiceServer
|
proto.UnimplementedManagementServiceServer
|
||||||
peersUpdateManager *PeersUpdateManager
|
peersUpdateManager *PeersUpdateManager
|
||||||
config *Config
|
config *Config
|
||||||
|
turnCredentialsManager TURNCredentialsManager
|
||||||
}
|
}
|
||||||
|
|
||||||
// AllowedIPsFormat generates Wireguard AllowedIPs format (e.g. 100.30.30.1/32)
|
// AllowedIPsFormat generates Wireguard AllowedIPs format (e.g. 100.30.30.1/32)
|
||||||
const AllowedIPsFormat = "%s/32"
|
const AllowedIPsFormat = "%s/32"
|
||||||
|
|
||||||
// NewServer creates a new Management server
|
// NewServer creates a new Management server
|
||||||
func NewServer(config *Config, accountManager *AccountManager, peersUpdateManager *PeersUpdateManager) (*Server, error) {
|
func NewServer(config *Config, accountManager *AccountManager, peersUpdateManager *PeersUpdateManager, turnCredentialsManager TURNCredentialsManager) (*Server, error) {
|
||||||
key, err := wgtypes.GeneratePrivateKey()
|
key, err := wgtypes.GeneratePrivateKey()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -36,9 +37,10 @@ func NewServer(config *Config, accountManager *AccountManager, peersUpdateManage
|
|||||||
return &Server{
|
return &Server{
|
||||||
wgKey: key,
|
wgKey: key,
|
||||||
// peerKey -> event channel
|
// peerKey -> event channel
|
||||||
peersUpdateManager: peersUpdateManager,
|
peersUpdateManager: peersUpdateManager,
|
||||||
accountManager: accountManager,
|
accountManager: accountManager,
|
||||||
config: config,
|
config: config,
|
||||||
|
turnCredentialsManager: turnCredentialsManager,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,7 +91,8 @@ func (s *Server) Sync(req *proto.EncryptedMessage, srv proto.ManagementService_S
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("failed marking peer as connected %s %v", peerKey, err)
|
log.Warnf("failed marking peer as connected %s %v", peerKey, err)
|
||||||
}
|
}
|
||||||
// Todo start turn credentials goroutine
|
|
||||||
|
s.turnCredentialsManager.SetupRefresh(peerKey.String())
|
||||||
// keep a connection to the peer and send updates when available
|
// keep a connection to the peer and send updates when available
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
@ -118,7 +121,8 @@ func (s *Server) Sync(req *proto.EncryptedMessage, srv proto.ManagementService_S
|
|||||||
// happens when connection drops, e.g. client disconnects
|
// happens when connection drops, e.g. client disconnects
|
||||||
log.Debugf("stream of peer %s has been closed", peerKey.String())
|
log.Debugf("stream of peer %s has been closed", peerKey.String())
|
||||||
s.peersUpdateManager.CloseChannel(peerKey.String())
|
s.peersUpdateManager.CloseChannel(peerKey.String())
|
||||||
err := s.accountManager.MarkPeerConnected(peerKey.String(), false)
|
s.turnCredentialsManager.CancelRefresh(peerKey.String())
|
||||||
|
err = s.accountManager.MarkPeerConnected(peerKey.String(), false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("failed marking peer as disconnected %s %v", peerKey, err)
|
log.Warnf("failed marking peer as disconnected %s %v", peerKey, err)
|
||||||
}
|
}
|
||||||
@ -165,7 +169,7 @@ func (s *Server) registerPeer(peerKey wgtypes.Key, req *proto.LoginRequest) (*Pe
|
|||||||
peersToSend = append(peersToSend, p)
|
peersToSend = append(peersToSend, p)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
update := toSyncResponse(s.config, peer, peersToSend)
|
update := toSyncResponse(s.config, peer, peersToSend, nil)
|
||||||
err = s.peersUpdateManager.SendUpdate(remotePeer.Key, &UpdateMessage{Update: update})
|
err = s.peersUpdateManager.SendUpdate(remotePeer.Key, &UpdateMessage{Update: update})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// todo rethink if we should keep this return
|
// todo rethink if we should keep this return
|
||||||
@ -215,11 +219,10 @@ func (s *Server) Login(ctx context.Context, req *proto.EncryptedMessage) (*proto
|
|||||||
return nil, status.Error(codes.Internal, "internal server error")
|
return nil, status.Error(codes.Internal, "internal server error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Todo fill up turn credentials
|
|
||||||
|
|
||||||
// if peer has reached this point then it has logged in
|
// if peer has reached this point then it has logged in
|
||||||
loginResp := &proto.LoginResponse{
|
loginResp := &proto.LoginResponse{
|
||||||
WiretrusteeConfig: toWiretrusteeConfig(s.config),
|
WiretrusteeConfig: toWiretrusteeConfig(s.config, nil),
|
||||||
PeerConfig: toPeerConfig(peer),
|
PeerConfig: toPeerConfig(peer),
|
||||||
}
|
}
|
||||||
encryptedResp, err := encryption.EncryptMessage(peerKey, s.wgKey, loginResp)
|
encryptedResp, err := encryption.EncryptMessage(peerKey, s.wgKey, loginResp)
|
||||||
@ -233,7 +236,7 @@ func (s *Server) Login(ctx context.Context, req *proto.EncryptedMessage) (*proto
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func toResponseProto(configProto Protocol) proto.HostConfig_Protocol {
|
func ToResponseProto(configProto Protocol) proto.HostConfig_Protocol {
|
||||||
switch configProto {
|
switch configProto {
|
||||||
case UDP:
|
case UDP:
|
||||||
return proto.HostConfig_UDP
|
return proto.HostConfig_UDP
|
||||||
@ -251,24 +254,33 @@ func toResponseProto(configProto Protocol) proto.HostConfig_Protocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func toWiretrusteeConfig(config *Config) *proto.WiretrusteeConfig {
|
func toWiretrusteeConfig(config *Config, turnCredentials *TURNCredentials) *proto.WiretrusteeConfig {
|
||||||
|
|
||||||
var stuns []*proto.HostConfig
|
var stuns []*proto.HostConfig
|
||||||
for _, stun := range config.Stuns {
|
for _, stun := range config.Stuns {
|
||||||
stuns = append(stuns, &proto.HostConfig{
|
stuns = append(stuns, &proto.HostConfig{
|
||||||
Uri: stun.URI,
|
Uri: stun.URI,
|
||||||
Protocol: toResponseProto(stun.Proto),
|
Protocol: ToResponseProto(stun.Proto),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
var turns []*proto.ProtectedHostConfig
|
var turns []*proto.ProtectedHostConfig
|
||||||
for _, turn := range config.Turns {
|
for _, turn := range config.TURNConfig.Turns {
|
||||||
|
var username string
|
||||||
|
var password string
|
||||||
|
if turnCredentials != nil {
|
||||||
|
username = turnCredentials.Username
|
||||||
|
password = turnCredentials.Password
|
||||||
|
} else {
|
||||||
|
username = turn.Username
|
||||||
|
password = string(turn.Password)
|
||||||
|
}
|
||||||
turns = append(turns, &proto.ProtectedHostConfig{
|
turns = append(turns, &proto.ProtectedHostConfig{
|
||||||
HostConfig: &proto.HostConfig{
|
HostConfig: &proto.HostConfig{
|
||||||
Uri: turn.URI,
|
Uri: turn.URI,
|
||||||
Protocol: toResponseProto(turn.Proto),
|
Protocol: ToResponseProto(turn.Proto),
|
||||||
},
|
},
|
||||||
User: turn.Username,
|
User: username,
|
||||||
Password: string(turn.Password),
|
Password: password,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -277,7 +289,7 @@ func toWiretrusteeConfig(config *Config) *proto.WiretrusteeConfig {
|
|||||||
Turns: turns,
|
Turns: turns,
|
||||||
Signal: &proto.HostConfig{
|
Signal: &proto.HostConfig{
|
||||||
Uri: config.Signal.URI,
|
Uri: config.Signal.URI,
|
||||||
Protocol: toResponseProto(config.Signal.Proto),
|
Protocol: ToResponseProto(config.Signal.Proto),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -288,9 +300,9 @@ func toPeerConfig(peer *Peer) *proto.PeerConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func toSyncResponse(config *Config, peer *Peer, peers []*Peer) *proto.SyncResponse {
|
func toSyncResponse(config *Config, peer *Peer, peers []*Peer, turnCredentials *TURNCredentials) *proto.SyncResponse {
|
||||||
|
|
||||||
wtConfig := toWiretrusteeConfig(config)
|
wtConfig := toWiretrusteeConfig(config, turnCredentials)
|
||||||
|
|
||||||
pConfig := toPeerConfig(peer)
|
pConfig := toPeerConfig(peer)
|
||||||
|
|
||||||
@ -322,13 +334,22 @@ func (s *Server) sendInitialSync(peerKey wgtypes.Key, peer *Peer, srv proto.Mana
|
|||||||
log.Warnf("error getting a list of peers for a peer %s", peer.Key)
|
log.Warnf("error getting a list of peers for a peer %s", peer.Key)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
plainResp := toSyncResponse(s.config, peer, peers)
|
|
||||||
|
// make secret time based TURN credentials optional
|
||||||
|
var turnCredentials *TURNCredentials
|
||||||
|
if s.config.TURNConfig.TimeBasedCredentials {
|
||||||
|
creds := s.turnCredentialsManager.GenerateCredentials()
|
||||||
|
turnCredentials = &creds
|
||||||
|
} else {
|
||||||
|
turnCredentials = nil
|
||||||
|
}
|
||||||
|
plainResp := toSyncResponse(s.config, peer, peers, turnCredentials)
|
||||||
|
|
||||||
encryptedResp, err := encryption.EncryptMessage(peerKey, s.wgKey, plainResp)
|
encryptedResp, err := encryption.EncryptMessage(peerKey, s.wgKey, plainResp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return status.Errorf(codes.Internal, "error handling request")
|
return status.Errorf(codes.Internal, "error handling request")
|
||||||
}
|
}
|
||||||
// Todo fill up the turn credentials
|
|
||||||
err = srv.Send(&proto.EncryptedMessage{
|
err = srv.Send(&proto.EncryptedMessage{
|
||||||
WgPubKey: s.wgKey.PublicKey().String(),
|
WgPubKey: s.wgKey.PublicKey().String(),
|
||||||
Body: encryptedResp,
|
Body: encryptedResp,
|
||||||
|
@ -119,19 +119,18 @@ var _ = Describe("Management service", func() {
|
|||||||
Uri: "stun:stun.wiretrustee.com:3468",
|
Uri: "stun:stun.wiretrustee.com:3468",
|
||||||
Protocol: mgmtProto.HostConfig_UDP,
|
Protocol: mgmtProto.HostConfig_UDP,
|
||||||
}
|
}
|
||||||
expectedTurnsConfig := &mgmtProto.ProtectedHostConfig{
|
expectedTRUNHost := &mgmtProto.HostConfig{
|
||||||
HostConfig: &mgmtProto.HostConfig{
|
Uri: "turn:stun.wiretrustee.com:3468",
|
||||||
Uri: "turn:stun.wiretrustee.com:3468",
|
Protocol: mgmtProto.HostConfig_UDP,
|
||||||
Protocol: mgmtProto.HostConfig_UDP,
|
|
||||||
},
|
|
||||||
User: "some_user",
|
|
||||||
Password: "some_password",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Expect(resp.WiretrusteeConfig.Signal).To(BeEquivalentTo(expectedSignalConfig))
|
Expect(resp.WiretrusteeConfig.Signal).To(BeEquivalentTo(expectedSignalConfig))
|
||||||
Expect(resp.WiretrusteeConfig.Stuns).To(ConsistOf(expectedStunsConfig))
|
Expect(resp.WiretrusteeConfig.Stuns).To(ConsistOf(expectedStunsConfig))
|
||||||
Expect(resp.WiretrusteeConfig.Turns).To(ConsistOf(expectedTurnsConfig))
|
// TURN validation is special because credentials are dynamically generated
|
||||||
|
Expect(resp.WiretrusteeConfig.Turns).To(HaveLen(1))
|
||||||
|
actualTURN := resp.WiretrusteeConfig.Turns[0]
|
||||||
|
Expect(len(actualTURN.User) > 0).To(BeTrue())
|
||||||
|
Expect(actualTURN.HostConfig).To(BeEquivalentTo(expectedTRUNHost))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -368,7 +367,10 @@ var _ = Describe("Management service", func() {
|
|||||||
resp := &mgmtProto.SyncResponse{}
|
resp := &mgmtProto.SyncResponse{}
|
||||||
err = pb.Unmarshal(decryptedBytes, resp)
|
err = pb.Unmarshal(decryptedBytes, resp)
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
wg.Done()
|
if len(resp.GetRemotePeers()) > 0 {
|
||||||
|
//only consider peer updates
|
||||||
|
wg.Done()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
@ -388,7 +390,6 @@ var _ = Describe("Management service", func() {
|
|||||||
err := syncClient.CloseSend()
|
err := syncClient.CloseSend()
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -486,13 +487,15 @@ func startServer(config *server.Config) (*grpc.Server, net.Listener) {
|
|||||||
lis, err := net.Listen("tcp", ":0")
|
lis, err := net.Listen("tcp", ":0")
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
s := grpc.NewServer()
|
s := grpc.NewServer()
|
||||||
|
|
||||||
store, err := server.NewStore(config.Datadir)
|
store, err := server.NewStore(config.Datadir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("failed creating a store: %s: %v", config.Datadir, err)
|
log.Fatalf("failed creating a store: %s: %v", config.Datadir, err)
|
||||||
}
|
}
|
||||||
accountManager := server.NewManager(store)
|
accountManager := server.NewManager(store)
|
||||||
peersUpdateManager := server.NewPeersUpdateManager()
|
peersUpdateManager := server.NewPeersUpdateManager()
|
||||||
mgmtServer, err := server.NewServer(config, accountManager, peersUpdateManager)
|
turnManager := server.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig)
|
||||||
|
mgmtServer, err := server.NewServer(config, accountManager, peersUpdateManager, turnManager)
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
mgmtProto.RegisterManagementServiceServer(s, mgmtServer)
|
mgmtProto.RegisterManagementServiceServer(s, mgmtServer)
|
||||||
go func() {
|
go func() {
|
||||||
|
21
management/server/testdata/management.json
vendored
21
management/server/testdata/management.json
vendored
@ -7,14 +7,19 @@
|
|||||||
"Password": null
|
"Password": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"Turns": [
|
"TURNConfig": {
|
||||||
{
|
"Turns": [
|
||||||
"Proto": "udp",
|
{
|
||||||
"URI": "turn:stun.wiretrustee.com:3468",
|
"Proto": "udp",
|
||||||
"Username": "some_user",
|
"URI": "turn:stun.wiretrustee.com:3468",
|
||||||
"Password": "c29tZV9wYXNzd29yZA=="
|
"Username": "some_user",
|
||||||
}
|
"Password": "c29tZV9wYXNzd29yZA=="
|
||||||
],
|
}
|
||||||
|
],
|
||||||
|
"CredentialsTTL": "1h",
|
||||||
|
"Secret": "c29tZV9wYXNzd29yZA==",
|
||||||
|
"TimeBasedCredentials": true
|
||||||
|
},
|
||||||
"Signal": {
|
"Signal": {
|
||||||
"Proto": "http",
|
"Proto": "http",
|
||||||
"URI": "signal.wiretrustee.com:10000",
|
"URI": "signal.wiretrustee.com:10000",
|
||||||
|
123
management/server/turncredentials.go
Normal file
123
management/server/turncredentials.go
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha1"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"github.com/wiretrustee/wiretrustee/management/proto"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
//TURNCredentialsManager used to manage TURN credentials
|
||||||
|
type TURNCredentialsManager interface {
|
||||||
|
GenerateCredentials() TURNCredentials
|
||||||
|
SetupRefresh(peerKey string)
|
||||||
|
CancelRefresh(peerKey string)
|
||||||
|
}
|
||||||
|
|
||||||
|
//TimeBasedAuthSecretsManager generates credentials with TTL and using pre-shared secret known to TURN server
|
||||||
|
type TimeBasedAuthSecretsManager struct {
|
||||||
|
mux sync.Mutex
|
||||||
|
config *TURNConfig
|
||||||
|
updateManager *PeersUpdateManager
|
||||||
|
cancelMap map[string]chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type TURNCredentials struct {
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTimeBasedAuthSecretsManager(updateManager *PeersUpdateManager, config *TURNConfig) *TimeBasedAuthSecretsManager {
|
||||||
|
return &TimeBasedAuthSecretsManager{
|
||||||
|
mux: sync.Mutex{},
|
||||||
|
config: config,
|
||||||
|
updateManager: updateManager,
|
||||||
|
cancelMap: make(map[string]chan struct{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//GenerateCredentials generates new time-based secret credentials - basically username is a unix timestamp and password is a HMAC hash of a timestamp with a preshared TURN secret
|
||||||
|
func (m *TimeBasedAuthSecretsManager) GenerateCredentials() TURNCredentials {
|
||||||
|
mac := hmac.New(sha1.New, m.config.Secret)
|
||||||
|
|
||||||
|
timeAuth := time.Now().Add(m.config.CredentialsTTL.Duration).Unix()
|
||||||
|
|
||||||
|
username := fmt.Sprint(timeAuth)
|
||||||
|
|
||||||
|
_, err := mac.Write([]byte(username))
|
||||||
|
if err != nil {
|
||||||
|
log.Errorln("Generating turn password failed with error: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
bytePassword := mac.Sum(nil)
|
||||||
|
password := base64.StdEncoding.EncodeToString(bytePassword)
|
||||||
|
|
||||||
|
return TURNCredentials{
|
||||||
|
Username: username,
|
||||||
|
Password: password,
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *TimeBasedAuthSecretsManager) cancel(peerKey string) {
|
||||||
|
if channel, ok := m.cancelMap[peerKey]; ok {
|
||||||
|
close(channel)
|
||||||
|
delete(m.cancelMap, peerKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//CancelRefresh cancels scheduled peer credentials refresh
|
||||||
|
func (m *TimeBasedAuthSecretsManager) CancelRefresh(peerKey string) {
|
||||||
|
m.mux.Lock()
|
||||||
|
defer m.mux.Unlock()
|
||||||
|
m.cancel(peerKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
//SetupRefresh starts peer credentials refresh. Since credentials are expiring (TTL) it is necessary to always generate them and send to the peer.
|
||||||
|
//A goroutine is created and put into TimeBasedAuthSecretsManager.cancelMap. This routine should be cancelled if peer is gone.
|
||||||
|
func (m *TimeBasedAuthSecretsManager) SetupRefresh(peerKey string) {
|
||||||
|
m.mux.Lock()
|
||||||
|
defer m.mux.Unlock()
|
||||||
|
m.cancel(peerKey)
|
||||||
|
cancel := make(chan struct{}, 1)
|
||||||
|
m.cancelMap[peerKey] = cancel
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-cancel:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
//we don't want to regenerate credentials right on expiration, so we do it slightly before (at 3/4 of TTL)
|
||||||
|
time.Sleep(m.config.CredentialsTTL.Duration / 4 * 3)
|
||||||
|
|
||||||
|
c := m.GenerateCredentials()
|
||||||
|
var turns []*proto.ProtectedHostConfig
|
||||||
|
for _, host := range m.config.Turns {
|
||||||
|
turns = append(turns, &proto.ProtectedHostConfig{
|
||||||
|
HostConfig: &proto.HostConfig{
|
||||||
|
Uri: host.URI,
|
||||||
|
Protocol: ToResponseProto(host.Proto),
|
||||||
|
},
|
||||||
|
User: c.Username,
|
||||||
|
Password: c.Password,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
update := &proto.SyncResponse{
|
||||||
|
WiretrusteeConfig: &proto.WiretrusteeConfig{
|
||||||
|
Turns: turns,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err := m.updateManager.SendUpdate(peerKey, &UpdateMessage{Update: update})
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("error while sending TURN update to peer %s %v", peerKey, err)
|
||||||
|
// todo maybe continue trying?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
133
management/server/turncredentials_test.go
Normal file
133
management/server/turncredentials_test.go
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha1"
|
||||||
|
"encoding/base64"
|
||||||
|
"github.com/wiretrustee/wiretrustee/util"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var TurnTestHost = &Host{
|
||||||
|
Proto: UDP,
|
||||||
|
URI: "turn:turn.wiretrustee.com:77777",
|
||||||
|
Username: "username",
|
||||||
|
Password: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimeBasedAuthSecretsManager_GenerateCredentials(t *testing.T) {
|
||||||
|
ttl := util.Duration{Duration: time.Hour}
|
||||||
|
secret := []byte("some_secret")
|
||||||
|
peersManager := NewPeersUpdateManager()
|
||||||
|
|
||||||
|
tested := NewTimeBasedAuthSecretsManager(peersManager, &TURNConfig{
|
||||||
|
CredentialsTTL: ttl,
|
||||||
|
Secret: secret,
|
||||||
|
Turns: []*Host{TurnTestHost},
|
||||||
|
})
|
||||||
|
|
||||||
|
credentials := tested.GenerateCredentials()
|
||||||
|
|
||||||
|
if credentials.Username == "" {
|
||||||
|
t.Errorf("expected generated TURN username not to be empty, got empty")
|
||||||
|
}
|
||||||
|
if credentials.Password == "" {
|
||||||
|
t.Errorf("expected generated TURN password not to be empty, got empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
validateMAC(credentials.Username, credentials.Password, secret, t)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimeBasedAuthSecretsManager_SetupRefresh(t *testing.T) {
|
||||||
|
ttl := util.Duration{Duration: 2 * time.Second}
|
||||||
|
secret := []byte("some_secret")
|
||||||
|
peersManager := NewPeersUpdateManager()
|
||||||
|
peer := "some_peer"
|
||||||
|
updateChannel := peersManager.CreateChannel(peer)
|
||||||
|
|
||||||
|
tested := NewTimeBasedAuthSecretsManager(peersManager, &TURNConfig{
|
||||||
|
CredentialsTTL: ttl,
|
||||||
|
Secret: secret,
|
||||||
|
Turns: []*Host{TurnTestHost},
|
||||||
|
})
|
||||||
|
|
||||||
|
tested.SetupRefresh(peer)
|
||||||
|
|
||||||
|
if _, ok := tested.cancelMap[peer]; !ok {
|
||||||
|
t.Errorf("expecting peer to be present in a cancel map, got not present")
|
||||||
|
}
|
||||||
|
|
||||||
|
var updates []*UpdateMessage
|
||||||
|
|
||||||
|
loop:
|
||||||
|
for timeout := time.After(5 * time.Second); ; {
|
||||||
|
|
||||||
|
select {
|
||||||
|
case update := <-updateChannel:
|
||||||
|
updates = append(updates, update)
|
||||||
|
case <-timeout:
|
||||||
|
break loop
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(updates) >= 2 {
|
||||||
|
break loop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(updates) < 2 {
|
||||||
|
t.Errorf("expecting 2 peer credentials updates, got %v", len(updates))
|
||||||
|
}
|
||||||
|
|
||||||
|
firstUpdate := updates[0].Update.GetWiretrusteeConfig().Turns[0]
|
||||||
|
secondUpdate := updates[1].Update.GetWiretrusteeConfig().Turns[0]
|
||||||
|
|
||||||
|
if firstUpdate.Password == secondUpdate.Password {
|
||||||
|
t.Errorf("expecting first credential update password %v to be diffeerent from second, got equal", firstUpdate.Password)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimeBasedAuthSecretsManager_CancelRefresh(t *testing.T) {
|
||||||
|
ttl := util.Duration{Duration: time.Hour}
|
||||||
|
secret := []byte("some_secret")
|
||||||
|
peersManager := NewPeersUpdateManager()
|
||||||
|
peer := "some_peer"
|
||||||
|
|
||||||
|
tested := NewTimeBasedAuthSecretsManager(peersManager, &TURNConfig{
|
||||||
|
CredentialsTTL: ttl,
|
||||||
|
Secret: secret,
|
||||||
|
Turns: []*Host{TurnTestHost},
|
||||||
|
})
|
||||||
|
|
||||||
|
tested.SetupRefresh(peer)
|
||||||
|
if _, ok := tested.cancelMap[peer]; !ok {
|
||||||
|
t.Errorf("expecting peer to be present in a cancel map, got not present")
|
||||||
|
}
|
||||||
|
|
||||||
|
tested.CancelRefresh(peer)
|
||||||
|
if _, ok := tested.cancelMap[peer]; ok {
|
||||||
|
t.Errorf("expecting peer to be not present in a cancel map, got present")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateMAC(username string, actualMAC string, key []byte, t *testing.T) {
|
||||||
|
mac := hmac.New(sha1.New, key)
|
||||||
|
|
||||||
|
_, err := mac.Write([]byte(username))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedMAC := mac.Sum(nil)
|
||||||
|
decodedMAC, err := base64.StdEncoding.DecodeString(actualMAC)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
equal := hmac.Equal(decodedMAC, expectedMAC)
|
||||||
|
|
||||||
|
if !equal {
|
||||||
|
t.Errorf("expected password MAC to be %s. got %s", expectedMAC, decodedMAC)
|
||||||
|
}
|
||||||
|
}
|
37
util/duration.go
Normal file
37
util/duration.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
//Duration is used strictly for JSON requests/responses due to duration marshalling issues
|
||||||
|
type Duration struct {
|
||||||
|
time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d Duration) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(d.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Duration) UnmarshalJSON(b []byte) error {
|
||||||
|
var v interface{}
|
||||||
|
if err := json.Unmarshal(b, &v); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch value := v.(type) {
|
||||||
|
case float64:
|
||||||
|
d.Duration = time.Duration(value)
|
||||||
|
return nil
|
||||||
|
case string:
|
||||||
|
var err error
|
||||||
|
d.Duration, err = time.ParseDuration(value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return errors.New("invalid duration")
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user