package main
import (
+ "fmt"
"sort"
"github.com/jroimartin/gocui"
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))
Finish = make(chan struct{})
OurStats = &Stats{dead: make(chan struct{})}
Name = flag.String("name", "test", "Username")
+ Room = flag.String("room", "/", "Room name")
Muted bool
)
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)
if err != nil {
log.Fatal(err)
}
+ *Name = strings.ReplaceAll(*Name, " ", "-")
vad := uint64(*vadRaw)
opusEnc := newOpusEnc()
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)
}
"flag"
"os"
"sort"
+ "strings"
"time"
"github.com/jroimartin/gocui"
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 {
"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"
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{}
)
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)
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
}
}
}
if found {
- Peers[peer.sid] = &peer
+ Peers[peer.sid] = peer
go peer.Tx()
}
PeersM.Unlock()
}
}
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)
}
}()
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
}
{
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
}
}
}(&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()
}
peer.stats.last = time.Now()
- for _, p := range Peers {
+ for _, p := range peer.room.peers {
if p.sid == sid {
continue
}
vors "go.stargrave.org/vors/internal"
)
+var (
+ Peers = map[byte]*Peer{}
+ PeersM sync.Mutex
+)
+
type Stats struct {
pktsRx int64
pktsTx int64
bytesTx uint64
bads int64
last time.Time
- alive chan struct{}
}
type Peer struct {
addr *net.UDPAddr
key []byte
stats *Stats
+ room *Room
logger *slog.Logger
conn net.Conn
--- /dev/null
+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
+}
--- /dev/null
+@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
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
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
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