From: Sergey Matveev Date: Sat, 13 Apr 2024 19:38:26 +0000 (+0300) Subject: Rooms support X-Git-Tag: v1.0.0~6 X-Git-Url: http://www.git.stargrave.org/?a=commitdiff_plain;h=d4c3e020997d4e0a4ec6a29edd3bea0b47cbc685c37f7556d6224ac38cf00a89;p=vors.git Rooms support --- diff --git a/cmd/client/gui.go b/cmd/client/gui.go index 5e637bc..cb79c76 100644 --- a/cmd/client/gui.go +++ b/cmd/client/gui.go @@ -16,6 +16,7 @@ package main import ( + "fmt" "sort" "github.com/jroimartin/gocui" @@ -48,7 +49,7 @@ func guiLayout(gui *gocui.Gui) error { if err != gocui.ErrUnknownView { return err } - v.Title = "Logs" + v.Title = fmt.Sprintf("Logs name=%s room=%s", *Name, *Room) v.Autoscroll = true } sids := make([]int, 0, len(Streams)) diff --git a/cmd/client/main.go b/cmd/client/main.go index c0fc015..45a5a67 100644 --- a/cmd/client/main.go +++ b/cmd/client/main.go @@ -51,6 +51,7 @@ var ( Finish = make(chan struct{}) OurStats = &Stats{dead: make(chan struct{})} Name = flag.String("name", "test", "Username") + Room = flag.String("room", "/", "Room name") Muted bool ) @@ -92,6 +93,7 @@ func main() { recCmd := flag.String("rec", "rec "+soxParams, "rec command") playCmd := flag.String("play", "play "+soxParams, "play command") vadRaw := flag.Uint("vad", 0, "VAD threshold") + passwd := flag.String("passwd", "", "Protected room's password") flag.Parse() log.SetFlags(log.Lmicroseconds | log.Lshortfile) @@ -99,6 +101,7 @@ func main() { if err != nil { log.Fatal(err) } + *Name = strings.ReplaceAll(*Name, " ", "-") vad := uint64(*vadRaw) opusEnc := newOpusEnc() @@ -138,7 +141,7 @@ func main() { if err != nil { log.Fatalln("noise.NewHandshakeState:", err) } - buf, _, _, err := hs.WriteMessage(nil, []byte(*Name)) + buf, _, _, err := hs.WriteMessage(nil, []byte(*Name+" "+*Room+" "+*passwd)) if err != nil { log.Fatalln("handshake encrypt:", err) } diff --git a/cmd/server/gui.go b/cmd/server/gui.go index 0e488e8..6e16d02 100644 --- a/cmd/server/gui.go +++ b/cmd/server/gui.go @@ -19,6 +19,7 @@ import ( "flag" "os" "sort" + "strings" "time" "github.com/jroimartin/gocui" @@ -51,20 +52,28 @@ func guiLayout(gui *gocui.Gui) error { v.Title = "Logs" v.Autoscroll = true } - sids := make([]int, 0, len(Peers)) - for sid := range Peers { - sids = append(sids, int(sid)) + roomNames := make([]string, 0, len(Rooms)) + for n := range Rooms { + roomNames = append(roomNames, n) } - sort.Ints(sids) - for _, sid := range sids { - peer := Peers[byte(sid)] - v, err := gui.SetView(peer.name, 0, prevY, maxX-1, prevY+2) - prevY += 3 + sort.Strings(roomNames) + var now time.Time + for _, name := range roomNames { + room := Rooms[name] + lines := room.Stats(now) + v, err = gui.SetView(room.name, 0, prevY, maxX-1, prevY+1+len(lines)) + prevY += 2 + len(lines) if err != nil { if err != gocui.ErrUnknownView { return err } - v.Title = peer.name + title := room.name + if room.key != "" { + title += " protected" + } + v.Title = title + v.Clear() + v.Write([]byte(strings.Join(lines, "\n"))) } } if !GUIReady { diff --git a/cmd/server/main.go b/cmd/server/main.go index 86204b8..37efdfb 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -29,10 +29,9 @@ import ( "net/netip" "os" "strconv" - "sync" + "strings" "time" - "github.com/dustin/go-humanize" "github.com/flynn/noise" "github.com/jroimartin/gocui" vors "go.stargrave.org/vors/internal" @@ -46,8 +45,6 @@ var ( MinVersion: tls.VersionTLS13, CurvePreferences: []tls.CurveID{tls.X25519}, } - Peers = map[byte]*Peer{} - PeersM sync.Mutex Prv, Pub []byte Cookies = map[vors.Cookie]chan *net.UDPAddr{} ) @@ -56,10 +53,6 @@ func newPeer(conn *net.TCPConn) { logger := slog.With("remote", conn.RemoteAddr().String()) logger.Info("connected") defer conn.Close() - if len(Peers) == 1<<8 { - logger.Error("too many peers") - return - } err := conn.SetNoDelay(true) if err != nil { log.Fatalln("nodelay:", err) @@ -90,25 +83,78 @@ func newPeer(conn *net.TCPConn) { logger.Error("read handshake", "err", err) return } - peer := Peer{ + peer := &Peer{ logger: logger, conn: conn, - stats: &Stats{alive: make(chan struct{})}, + stats: &Stats{}, rx: make(chan []byte), tx: make(chan []byte, 10), alive: make(chan struct{}), } + var room *Room { - name, _, _, err := hs.ReadMessage(nil, buf) + nameAndRoom, _, _, err := hs.ReadMessage(nil, buf) if err != nil { logger.Error("handshake: decrypt", "err", err) return } - peer.name = string(name) + cols := strings.SplitN(string(nameAndRoom), " ", 3) + roomName := "/" + if len(cols) > 1 { + roomName = cols[1] + } + var key string + if len(cols) > 2 { + key = cols[2] + } + peer.name = string(cols[0]) + logger = logger.With("name", peer.name, "room", roomName) + RoomsM.Lock() + room = Rooms[roomName] + if room == nil { + room = &Room{ + name: roomName, + key: key, + peers: make(map[byte]*Peer), + alive: make(chan struct{}), + } + Rooms[roomName] = room + go func() { + if *NoGUI { + return + } + tick := time.Tick(vors.ScreenRefresh) + var now time.Time + var v *gocui.View + for { + select { + case <-room.alive: + GUI.DeleteView(room.name) + return + case now = <-tick: + v, err = GUI.View(room.name) + if err == nil { + v.Clear() + v.Write([]byte(strings.Join(room.Stats(now), "\n"))) + } + } + } + }() + } + RoomsM.Unlock() + if room.key != key { + logger.Error("wrong password") + buf, _, _, err = hs.WriteMessage(nil, []byte("wrong password")) + if err != nil { + log.Fatal(err) + } + vors.PktWrite(conn, buf) + return + } } - logger = logger.With("name", peer.name) + peer.room = room - for _, p := range Peers { + for _, p := range room.peers { if p.name != peer.name { continue } @@ -134,7 +180,7 @@ func newPeer(conn *net.TCPConn) { } } if found { - Peers[peer.sid] = &peer + Peers[peer.sid] = peer go peer.Tx() } PeersM.Unlock() @@ -148,16 +194,17 @@ func newPeer(conn *net.TCPConn) { } } logger = logger.With("sid", peer.sid) + room.peers[peer.sid] = peer logger.Info("logged in") defer func() { logger.Info("removing") PeersM.Lock() delete(Peers, peer.sid) + delete(room.peers, peer.sid) PeersM.Unlock() - close(peer.stats.alive) s := []byte(fmt.Sprintf("%s %d", vors.CmdDel, peer.sid)) - for _, p := range Peers { + for _, p := range room.peers { go func(tx chan []byte) { tx <- s }(p.tx) } }() @@ -200,7 +247,7 @@ func newPeer(conn *net.TCPConn) { go peer.Rx() peer.tx <- []byte(fmt.Sprintf("SID %d", peer.sid)) - for _, p := range Peers { + for _, p := range room.peers { if p.sid == peer.sid { continue } @@ -220,7 +267,7 @@ func newPeer(conn *net.TCPConn) { { s := []byte(fmt.Sprintf("%s %d %s %s", vors.CmdAdd, peer.sid, peer.name, hex.EncodeToString(peer.key))) - for _, p := range Peers { + for _, p := range room.peers { if p.sid != peer.sid { p.tx <- s } @@ -245,40 +292,6 @@ func newPeer(conn *net.TCPConn) { } }(&seen) - go func(stats *Stats) { - if *NoGUI { - return - } - tick := time.Tick(vors.ScreenRefresh) - var now time.Time - var v *gocui.View - for { - select { - case <-stats.alive: - GUI.DeleteView(peer.name) - return - case now = <-tick: - s := fmt.Sprintf( - "%s | Rx/Tx/Bad: %s / %s / %s | %s / %s", - peer.addr, - humanize.Comma(stats.pktsRx), - humanize.Comma(stats.pktsTx), - humanize.Comma(stats.bads), - humanize.IBytes(stats.bytesRx), - humanize.IBytes(stats.bytesTx), - ) - if stats.last.Add(vors.ScreenRefresh).After(now) { - s += " | " + vors.CGreen + "TALK" + vors.CReset - } - v, err = GUI.View(peer.name) - if err == nil { - v.Clear() - v.Write([]byte(s)) - } - } - } - }(peer.stats) - for buf := range peer.rx { if string(buf) == vors.CmdPing { seen = time.Now() @@ -424,7 +437,7 @@ func main() { } peer.stats.last = time.Now() - for _, p := range Peers { + for _, p := range peer.room.peers { if p.sid == sid { continue } diff --git a/cmd/server/peer.go b/cmd/server/peer.go index 960d5b2..d1e078a 100644 --- a/cmd/server/peer.go +++ b/cmd/server/peer.go @@ -10,6 +10,11 @@ import ( vors "go.stargrave.org/vors/internal" ) +var ( + Peers = map[byte]*Peer{} + PeersM sync.Mutex +) + type Stats struct { pktsRx int64 pktsTx int64 @@ -17,7 +22,6 @@ type Stats struct { bytesTx uint64 bads int64 last time.Time - alive chan struct{} } type Peer struct { @@ -26,6 +30,7 @@ type Peer struct { addr *net.UDPAddr key []byte stats *Stats + room *Room logger *slog.Logger conn net.Conn diff --git a/cmd/server/room.go b/cmd/server/room.go new file mode 100644 index 0000000..8d88406 --- /dev/null +++ b/cmd/server/room.go @@ -0,0 +1,53 @@ +package main + +import ( + "fmt" + "sort" + "sync" + "time" + + "github.com/dustin/go-humanize" + vors "go.stargrave.org/vors/internal" +) + +var ( + Rooms = map[string]*Room{} + RoomsM sync.Mutex +) + +type Room struct { + name string + key string + peers map[byte]*Peer + alive chan struct{} +} + +func (room *Room) Stats(now time.Time) []string { + sids := make([]int, 0, len(room.peers)) + for sid := range room.peers { + sids = append(sids, int(sid)) + } + sort.Ints(sids) + lines := make([]string, 0, len(sids)) + for _, sid := range sids { + peer := room.peers[byte(sid)] + if peer == nil { + continue + } + line := fmt.Sprintf( + "%12s | %s | Rx/Tx/Bad: %s / %s / %s | %s / %s", + peer.name, + peer.addr, + humanize.Comma(peer.stats.pktsRx), + humanize.Comma(peer.stats.pktsTx), + humanize.Comma(peer.stats.bads), + humanize.IBytes(peer.stats.bytesRx), + humanize.IBytes(peer.stats.bytesTx), + ) + if peer.stats.last.Add(vors.ScreenRefresh).After(now) { + line += " | " + vors.CGreen + "TALK" + vors.CReset + } + lines = append(lines, line) + } + return lines +} diff --git a/doc/features.texi b/doc/features.texi new file mode 100644 index 0000000..8987ded --- /dev/null +++ b/doc/features.texi @@ -0,0 +1,22 @@ +@node Features +@unnumbered Features + +@itemize + +@item Client-server architecture. All clients send their output to the +server, while it copies it to other clients. However, as a rule, there +is single client speaking at one time. + +@item 20ms frames with Opus-encoded audio with PLC (Packet Loss +Concealment) and DTX (discontinuous transmission) features enabled. +Optional VAD (voice activity detection). + +@item Noise protocol-based handshake over TCP between client and server +for creating authenticated encrypted channel and authentication based on +server's public key knowledge. + +@item Rooms, optionally password protected. + +@item Fancy TUI client with mute-toggle ability. + +@end itemize diff --git a/doc/index.texi b/doc/index.texi index 29be2ed..06185d3 100644 --- a/doc/index.texi +++ b/doc/index.texi @@ -12,11 +12,6 @@ VoRS -- Vo(IP) Really Simple. Very simple and usable multi-user VoIP solution. Some kind of alternative to @url{https://www.mumble.info/, Mumble}. -@float -@image{screenshots/example,,,Server and two clients,.webp} -@caption{Server (above) and two clients (left and right) in a terminal multiplexer} -@end float - But why? SIP-based solutions are pretty complicated to setup, because they are not made for simple tasks like sudden voice chats between a few people. WebRTC-based solutions are insane bloated incredible @@ -44,15 +39,21 @@ Mumble tends to output no information, sometimes hiding the fact of a problem and that everything stopped working. @item No NAT-traversal possibility. It is the year 2024 year already, -stop trying to use and revive legacy obsolete IPv4. Or use some overlay -network on top of it, Or VPN, whatever. +stop trying to use and revive legacy obsolete IPv4. Either use some +overlay network on top of it, or VPN, whatever. @item Mono-cypher, mono-codec protocol. The @url{https://opus-codec.org/, Opus} audio codec is perfect for VoIP tasks. ChaCha20-Poly1305 is more than appropriate and satisfies as fast and secure encryption solution. +@float +@image{screenshots/example,,,Server and two clients,.webp} +@caption{Server (above) and two clients (left and right) in a terminal multiplexer} +@end float + @end itemize +@include features.texi @include install.texi @include usage.texi @include vad.texi diff --git a/doc/usage.texi b/doc/usage.texi index 396685c..84e5660 100644 --- a/doc/usage.texi +++ b/doc/usage.texi @@ -35,4 +35,8 @@ $ vors-client -srv "[2001:db8::1]:12978" -pub $pub -name NAME Pressing F10 in server/client TUIs means quitting. Pressing Enter in client means "mute" toggling. +@item + @option{-room} allows you to join non-root room. + @option{-passwd} allows you to protect with provided password. + @end itemize