diff --git a/client/cmd/testutil.go b/client/cmd/testutil.go index 0cb7eb7fd..da76ee73a 100644 --- a/client/cmd/testutil.go +++ b/client/cmd/testutil.go @@ -39,7 +39,8 @@ func startManagement(config *mgmt.Config, t *testing.T) (*grpc.Server, net.Liste accountManager := mgmt.NewManager(store) 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 { t.Fatal(err) } diff --git a/client/testdata/management.json b/client/testdata/management.json index 8282a7756..9224ff2c3 100644 --- a/client/testdata/management.json +++ b/client/testdata/management.json @@ -7,14 +7,19 @@ "Password": null } ], - "Turns": [ - { - "Proto": "udp", - "URI": "turn:stun.wiretrustee.com:3468", - "Username": "some_user", - "Password": "c29tZV9wYXNzd29yZA==" - } - ], + "TURNConfig": { + "Turns": [ + { + "Proto": "udp", + "URI": "turn:stun.wiretrustee.com:3468", + "Username": "some_user", + "Password": "c29tZV9wYXNzd29yZA==" + } + ], + "CredentialsTTL": "1h", + "Secret": "c29tZV9wYXNzd29yZA==", + "TimeBasedCredentials": true + }, "Signal": { "Proto": "http", "URI": "signal.wiretrustee.com:10000", diff --git a/management/client/client_test.go b/management/client/client_test.go index 80bf7967b..dd8897b8f 100644 --- a/management/client/client_test.go +++ b/management/client/client_test.go @@ -62,7 +62,8 @@ func startManagement(config *mgmt.Config, t *testing.T) (*grpc.Server, net.Liste accountManager := mgmt.NewManager(store) 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 { t.Fatal(err) } diff --git a/management/cmd/management.go b/management/cmd/management.go index dfc222d53..a9568d66c 100644 --- a/management/cmd/management.go +++ b/management/cmd/management.go @@ -79,7 +79,8 @@ var ( opts = append(opts, grpc.KeepaliveEnforcementPolicy(kaep), grpc.KeepaliveParams(kasp)) grpcServer := grpc.NewServer(opts...) 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 { log.Fatalf("failed creating new server: %v", err) } diff --git a/management/server/config.go b/management/server/config.go index 07012cca3..3dc003c27 100644 --- a/management/server/config.go +++ b/management/server/config.go @@ -1,5 +1,9 @@ package server +import ( + "github.com/wiretrustee/wiretrustee/util" +) + type Protocol string const ( @@ -12,15 +16,23 @@ const ( // Config of the Management service type Config struct { - Stuns []*Host - Turns []*Host - Signal *Host + Stuns []*Host + TURNConfig *TURNConfig + Signal *Host Datadir string 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 type HttpServerConfig struct { LetsEncryptDomain string diff --git a/management/server/grpcserver.go b/management/server/grpcserver.go index 1c80bc5f3..ac0d41e4a 100644 --- a/management/server/grpcserver.go +++ b/management/server/grpcserver.go @@ -19,15 +19,16 @@ type Server struct { accountManager *AccountManager wgKey wgtypes.Key proto.UnimplementedManagementServiceServer - peersUpdateManager *PeersUpdateManager - config *Config + peersUpdateManager *PeersUpdateManager + config *Config + turnCredentialsManager TURNCredentialsManager } // AllowedIPsFormat generates Wireguard AllowedIPs format (e.g. 100.30.30.1/32) const AllowedIPsFormat = "%s/32" // 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() if err != nil { return nil, err @@ -36,9 +37,10 @@ func NewServer(config *Config, accountManager *AccountManager, peersUpdateManage return &Server{ wgKey: key, // peerKey -> event channel - peersUpdateManager: peersUpdateManager, - accountManager: accountManager, - config: config, + peersUpdateManager: peersUpdateManager, + accountManager: accountManager, + config: config, + turnCredentialsManager: turnCredentialsManager, }, nil } @@ -89,7 +91,8 @@ func (s *Server) Sync(req *proto.EncryptedMessage, srv proto.ManagementService_S if err != nil { 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 for { select { @@ -118,7 +121,8 @@ func (s *Server) Sync(req *proto.EncryptedMessage, srv proto.ManagementService_S // happens when connection drops, e.g. client disconnects log.Debugf("stream of peer %s has been closed", 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 { 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) } } - update := toSyncResponse(s.config, peer, peersToSend) + update := toSyncResponse(s.config, peer, peersToSend, nil) err = s.peersUpdateManager.SendUpdate(remotePeer.Key, &UpdateMessage{Update: update}) if err != nil { // 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") } } - // Todo fill up turn credentials // if peer has reached this point then it has logged in loginResp := &proto.LoginResponse{ - WiretrusteeConfig: toWiretrusteeConfig(s.config), + WiretrusteeConfig: toWiretrusteeConfig(s.config, nil), PeerConfig: toPeerConfig(peer), } encryptedResp, err := encryption.EncryptMessage(peerKey, s.wgKey, loginResp) @@ -233,7 +236,7 @@ func (s *Server) Login(ctx context.Context, req *proto.EncryptedMessage) (*proto }, nil } -func toResponseProto(configProto Protocol) proto.HostConfig_Protocol { +func ToResponseProto(configProto Protocol) proto.HostConfig_Protocol { switch configProto { case 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 for _, stun := range config.Stuns { stuns = append(stuns, &proto.HostConfig{ Uri: stun.URI, - Protocol: toResponseProto(stun.Proto), + Protocol: ToResponseProto(stun.Proto), }) } 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{ HostConfig: &proto.HostConfig{ Uri: turn.URI, - Protocol: toResponseProto(turn.Proto), + Protocol: ToResponseProto(turn.Proto), }, - User: turn.Username, - Password: string(turn.Password), + User: username, + Password: password, }) } @@ -277,7 +289,7 @@ func toWiretrusteeConfig(config *Config) *proto.WiretrusteeConfig { Turns: turns, Signal: &proto.HostConfig{ 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) @@ -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) 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) if err != nil { return status.Errorf(codes.Internal, "error handling request") } - // Todo fill up the turn credentials + err = srv.Send(&proto.EncryptedMessage{ WgPubKey: s.wgKey.PublicKey().String(), Body: encryptedResp, diff --git a/management/server/management_test.go b/management/server/management_test.go index 33a5b506c..8e7fae5c3 100644 --- a/management/server/management_test.go +++ b/management/server/management_test.go @@ -119,19 +119,18 @@ var _ = Describe("Management service", func() { Uri: "stun:stun.wiretrustee.com:3468", Protocol: mgmtProto.HostConfig_UDP, } - expectedTurnsConfig := &mgmtProto.ProtectedHostConfig{ - HostConfig: &mgmtProto.HostConfig{ - Uri: "turn:stun.wiretrustee.com:3468", - Protocol: mgmtProto.HostConfig_UDP, - }, - User: "some_user", - Password: "some_password", + expectedTRUNHost := &mgmtProto.HostConfig{ + Uri: "turn:stun.wiretrustee.com:3468", + Protocol: mgmtProto.HostConfig_UDP, } Expect(resp.WiretrusteeConfig.Signal).To(BeEquivalentTo(expectedSignalConfig)) 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{} err = pb.Unmarshal(decryptedBytes, resp) 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() Expect(err).NotTo(HaveOccurred()) } - }) }) }) @@ -486,13 +487,15 @@ func startServer(config *server.Config) (*grpc.Server, net.Listener) { lis, err := net.Listen("tcp", ":0") Expect(err).NotTo(HaveOccurred()) s := grpc.NewServer() + store, err := server.NewStore(config.Datadir) if err != nil { log.Fatalf("failed creating a store: %s: %v", config.Datadir, err) } accountManager := server.NewManager(store) 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()) mgmtProto.RegisterManagementServiceServer(s, mgmtServer) go func() { diff --git a/management/server/testdata/management.json b/management/server/testdata/management.json index 8282a7756..9224ff2c3 100644 --- a/management/server/testdata/management.json +++ b/management/server/testdata/management.json @@ -7,14 +7,19 @@ "Password": null } ], - "Turns": [ - { - "Proto": "udp", - "URI": "turn:stun.wiretrustee.com:3468", - "Username": "some_user", - "Password": "c29tZV9wYXNzd29yZA==" - } - ], + "TURNConfig": { + "Turns": [ + { + "Proto": "udp", + "URI": "turn:stun.wiretrustee.com:3468", + "Username": "some_user", + "Password": "c29tZV9wYXNzd29yZA==" + } + ], + "CredentialsTTL": "1h", + "Secret": "c29tZV9wYXNzd29yZA==", + "TimeBasedCredentials": true + }, "Signal": { "Proto": "http", "URI": "signal.wiretrustee.com:10000", diff --git a/management/server/turncredentials.go b/management/server/turncredentials.go new file mode 100644 index 000000000..ca97f8625 --- /dev/null +++ b/management/server/turncredentials.go @@ -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? + } + } + } + }() +} diff --git a/management/server/turncredentials_test.go b/management/server/turncredentials_test.go new file mode 100644 index 000000000..b02ed50f3 --- /dev/null +++ b/management/server/turncredentials_test.go @@ -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) + } +} diff --git a/util/duration.go b/util/duration.go new file mode 100644 index 000000000..4757bf17e --- /dev/null +++ b/util/duration.go @@ -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") + } +}