1 // VoRS -- Vo(IP) Really Simple
2 // Copyright (C) 2024 Sergey Matveev <stargrave@stargrave.org>
4 // This program is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU Affero General Public License as
6 // published by the Free Software Foundation, version 3 of the License.
8 // This program is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 // GNU Affero General Public License for more details.
13 // You should have received a copy of the GNU Affero General Public License
14 // along with this program. If not, see <http://www.gnu.org/licenses/>.
35 "github.com/dchest/siphash"
36 "github.com/flynn/noise"
37 "github.com/jroimartin/gocui"
38 "go.cypherpunks.ru/netstring/v2"
39 "go.stargrave.org/opus/v2"
40 vors "go.stargrave.org/vors/v3/internal"
41 "golang.org/x/crypto/blake2s"
42 "golang.org/x/crypto/chacha20"
56 Streams = map[byte]*Stream{}
58 Finish = make(chan struct{})
59 OurStats = &Stats{dead: make(chan struct{})}
60 Name = flag.String("name", "test", "username")
61 Room = flag.String("room", "/", "room name")
63 Ctrl = make(chan []byte)
66 func incr(data []byte) {
67 for i := len(data) - 1; i >= 0; i-- {
76 func muteToggle() (muted bool) {
85 Ctrl <- vors.ArgsEncode([]byte(cmd))
91 srvAddr := flag.String("srv", "vors.home.arpa:"+strconv.Itoa(vors.DefaultPort),
92 "host:TCP/UDP port to connect to")
93 srvPubB64 := flag.String("pub", "", "server's public key, Base64")
94 recCmd := flag.String("rec", "rec "+vors.SoxParams, "rec command")
95 playCmd := flag.String("play", "play "+vors.SoxParams, "play command")
96 vadRaw := flag.Uint("vad", 0, "VAD threshold")
97 passwd := flag.String("passwd", "", "protected room's password")
98 muteTogglePth := flag.String("mute-toggle", "",
99 "path to FIFO to toggle mute")
100 prefer4 := flag.Bool("4", false,
101 "Prefer obsolete legacy IPv4 address during name resolution")
102 version := flag.Bool("version", false, "print version")
103 warranty := flag.Bool("warranty", false, "print warranty information")
104 flag.Usage = func() {
105 fmt.Fprintln(os.Stderr, "Usage: vors-client [opts] -name NAME -pub PUB -srv HOST:PORT")
107 fmt.Fprintln(os.Stderr, `
108 Press Tab to cycle through peers and chat windows. Pressing Enter in a
109 peer window toggles silencing (no audio will be played from it). Chat
110 windows allows you to enter the text and send it to everyone in the room
113 Press F1 to toggle mute -- no sending of microphone audio to server).
116 Each peer contains various statistics: number of packets received from
117 it (or sent, if it is you), traffic amount, number of silence seconds,
118 number of bad packets (malformed or altered, number of lost packets,
119 number of reordered packets.
120 Gree "T" means that recently an audio packet was received.
121 Red "MUTE" means that peer is in muted mode.
122 Magenta "S" means that peer is locally muted.`)
125 log.SetFlags(log.Lmicroseconds)
128 fmt.Println(vors.Warranty)
132 fmt.Println(vors.GetVersion())
138 hsh := blake2s.Sum256([]byte(*passwd))
142 srvPub, err := base64.RawURLEncoding.DecodeString(*srvPubB64)
146 *Name = strings.ReplaceAll(*Name, " ", "-")
149 if *muteTogglePth == "" {
153 fd, err := os.OpenFile(*muteTogglePth, os.O_WRONLY, os.FileMode(0666))
163 fd.WriteString(reply + "\n")
165 time.Sleep(time.Second)
169 vad := uint64(*vadRaw)
170 opusEnc := newOpusEnc()
171 var mic io.ReadCloser
173 cmd := vors.MakeCmd(*recCmd)
174 mic, err = cmd.StdoutPipe()
184 vors.PreferIPv4 = *prefer4
185 ctrlConn, err := net.DialTCP("tcp", nil, vors.MustResolveTCP(*srvAddr))
187 log.Fatalln("dial server:", err)
189 defer ctrlConn.Close()
190 if err = ctrlConn.SetNoDelay(true); err != nil {
191 log.Fatalln("nodelay:", err)
193 ctrl := vors.NewNSConn(ctrlConn)
195 hs, err := noise.NewHandshakeState(noise.Config{
196 CipherSuite: vors.NoiseCipherSuite,
197 Pattern: noise.HandshakeNK,
200 Prologue: []byte(vors.NoisePrologue),
203 log.Fatalln("noise.NewHandshakeState:", err)
205 buf, _, _, err := hs.WriteMessage(nil, vors.ArgsEncode(
206 []byte(*Name), []byte(*Room), passwdHsh,
209 log.Fatalln("handshake encrypt:", err)
213 w.WriteString(vors.NoisePrologue)
214 netstring.NewWriter(&w).WriteChunk(buf)
217 _, err = io.Copy(ctrlConn, bytes.NewReader(buf))
219 log.Fatalln("write handshake:", err)
224 log.Fatalln("read handshake:", ctrl.Err)
226 buf, txCS, rxCS, err := hs.ReadMessage(nil, buf)
228 log.Fatalln("handshake decrypt:", err)
231 rx := make(chan []byte)
233 for buf := range ctrl.Rx {
234 buf, err = rxCS.Decrypt(buf[:0], nil, buf)
236 log.Println("rx decrypt", err)
244 srvAddrUDP := vors.MustResolveUDP(*srvAddr)
245 conn, err := net.DialUDP("udp", nil, srvAddrUDP)
247 log.Fatalln("connect:", err)
251 args, err := vors.ArgsDecode(buf)
253 log.Fatalln("args decode:", err)
256 log.Fatalln("empty args")
258 var cookie vors.Cookie
259 switch cmd := string(args[0]); cmd {
261 log.Fatalln("handshake failed:", string(args[1]))
263 copy(cookie[:], args[1])
265 log.Fatalln("unexpected post-handshake cmd:", cmd)
267 timeout := time.NewTimer(vors.PingTime)
273 ticker := time.NewTicker(time.Second)
274 if _, err = conn.Write(cookie[:]); err != nil {
275 log.Fatalln("write:", err)
277 WaitForCookieAcceptance:
281 log.Fatalln("cookie acceptance timeout")
283 if _, err = conn.Write(cookie[:]); err != nil {
284 log.Fatalln("write:", err)
287 args, err := vors.ArgsDecode(buf)
289 log.Fatalln("args decode:", err)
292 log.Fatalln("empty args")
294 switch cmd := string(args[0]); cmd {
296 log.Fatalln("cookie acceptance failed:", string(args[1]))
300 Streams[sid] = &Stream{name: *Name, stats: OurStats}
303 log.Fatalln("unexpected post-cookie cmd:", cmd)
305 break WaitForCookieAcceptance
313 var keyCiphOur []byte
316 xof, err := blake2s.NewXOF(chacha20.KeySize+16, nil)
320 xof.Write([]byte(vors.NoisePrologue))
321 xof.Write(hs.ChannelBinding())
322 buf := make([]byte, chacha20.KeySize+16)
323 if _, err = io.ReadFull(xof, buf); err != nil {
326 keyCiphOur, keyMACOur = buf[:chacha20.KeySize], buf[chacha20.KeySize:]
331 LoggerReady := make(chan struct{})
332 GUI, err = gocui.NewGui(gocui.OutputNormal)
337 GUI.SelFgColor = gocui.ColorCyan
339 GUI.SetManagerFunc(guiLayout)
340 if err := GUI.SetKeybinding("", gocui.KeyTab, gocui.ModNone, tabHandle); err != nil {
343 if err := GUI.SetKeybinding("", gocui.KeyF1, gocui.ModNone,
344 func(gui *gocui.Gui, v *gocui.View) error {
351 if err := GUI.SetKeybinding("", gocui.KeyF10, gocui.ModNone,
352 func(gui *gocui.Gui, v *gocui.View) error {
362 v, err := GUI.View("logs")
367 log.Println("connected", "sid:", sid,
368 "addr:", conn.LocalAddr().String())
371 time.Sleep(vors.ScreenRefresh)
372 GUI.Update(func(gui *gocui.Gui) error {
381 time.Sleep(100 * time.Millisecond)
386 for buf := range Ctrl {
387 buf, err = txCS.Encrypt(nil, nil, buf)
389 log.Fatalln("tx encrypt:", err)
391 if err = ctrl.Tx(buf); err != nil {
392 log.Fatalln("tx:", err)
399 time.Sleep(vors.PingTime)
400 Ctrl <- vors.ArgsEncode([]byte(vors.CmdPing))
404 go func(seen *time.Time) {
406 for buf := range rx {
407 args, err := vors.ArgsDecode(buf)
409 log.Fatalln("args decode:", err)
412 log.Fatalln("empty args")
414 switch cmd := string(args[0]); cmd {
419 sidRaw, name, key := args[1], args[2], args[3]
421 log.Println("add", string(name), "sid:", sid)
422 keyCiph, keyMAC := key[:chacha20.KeySize], key[chacha20.KeySize:]
425 in: make(chan []byte, 1<<10),
426 stats: &Stats{dead: make(chan struct{})},
429 dec, err := opus.NewDecoder(vors.Rate, 1)
433 if err = dec.SetComplexity(10); err != nil {
437 var player io.WriteCloser
438 playerTx := make(chan []byte, 5)
441 cmd = vors.MakeCmd(*playCmd)
442 player, err = cmd.StdinPipe()
455 for len(playerTx) > vors.MaxLost {
457 stream.stats.reorder++
459 pcmbuf, ok = <-playerTx
466 if _, err = io.Copy(player,
467 bytes.NewReader(pcmbuf)); err != nil {
468 log.Println("play:", err)
475 var ciph *chacha20.Cipher
476 mac := siphash.New(keyMAC)
477 tag := make([]byte, siphash.Size)
479 pcm := make([]int16, vors.FrameLen)
480 nonce := make([]byte, 12)
484 for buf := range stream.in {
485 copy(nonce[len(nonce)-4:], buf)
487 if _, err = mac.Write(
488 buf[:len(buf)-siphash.Size],
493 if subtle.ConstantTimeCompare(
495 buf[len(buf)-siphash.Size:],
500 ciph, err = chacha20.NewUnauthenticatedCipher(
506 pkt = buf[4+3 : len(buf)-siphash.Size]
507 ciph.XORKeyStream(pkt, pkt)
509 ctr = binary.BigEndian.Uint32(nonce[len(nonce)-4:])
511 // ignore the very first packet in the stream
514 lost = int(ctr - (stream.ctr + 1))
517 stream.actr = uint32(buf[4+0])<<16 |
518 uint32(buf[4+1])<<8 | uint32(buf[4+2])
519 stream.stats.lost += int64(lost)
520 if lost > vors.MaxLost {
523 for ; lost > 0; lost-- {
524 lastDur, err = dec.LastPacketDuration()
526 log.Println("PLC:", err)
529 err = dec.DecodePLC(pcm[:lastDur])
531 log.Println("PLC:", err)
534 stream.stats.AddRMS(pcm)
538 pcmbuf := make([]byte, 2*lastDur)
539 pcmConv(pcmbuf, pcm[:lastDur])
542 _, err = dec.Decode(pkt, pcm)
544 log.Println("decode:", err)
547 stream.stats.AddRMS(pcm)
548 stream.stats.last = time.Now()
552 pcmbuf := make([]byte, 2*len(pcm))
560 go statsDrawer(stream)
562 Streams[sid] = stream
568 log.Println("unknown sid:", sid)
571 log.Println("del", s.name, "sid:", sid)
581 log.Println("unknown sid:", sid)
585 case vors.CmdUnmuted:
589 log.Println("unknown sid:", sid)
597 log.Println("unknown sid:", sid)
600 log.Println(s.name, ":", string(args[2]))
602 log.Fatal("unexpected cmd:", cmd)
607 go func(seen *time.Time) {
608 for now := range time.Tick(vors.PingTime) {
609 if seen.Add(2 * vors.PingTime).Before(now) {
610 log.Println("timeout:", seen)
620 var from *net.UDPAddr
625 buf := make([]byte, 2*vors.FrameLen)
626 n, from, err = conn.ReadFromUDP(buf)
628 log.Println("recvfrom:", err)
632 if from.Port != srvAddrUDP.Port || !from.IP.Equal(srvAddrUDP.IP) {
633 log.Println("wrong addr:", from)
636 if n <= 4+siphash.Size {
637 log.Println("too small:", n)
640 stream = Streams[buf[0]]
642 log.Println("unknown stream:", buf[0])
646 stream.stats.bytes += vors.IPHdrLen(from.IP) + 8 + uint64(n)
647 ctr = binary.BigEndian.Uint32(buf)
648 if ctr <= stream.ctr {
649 stream.stats.reorder++
656 go statsDrawer(&Stream{name: *Name, stats: OurStats})
659 for now := range time.NewTicker(time.Second).C {
660 if !OurStats.last.Add(time.Second).Before(now) {
664 OurStats.bytes += vors.IPHdrLen(srvAddrUDP.IP) + 8 + 1
665 if _, err = conn.Write([]byte{sid}); err != nil {
666 log.Println("send:", err)
675 var ciph *chacha20.Cipher
676 mac := siphash.New(keyMACOur)
677 tag := make([]byte, siphash.Size)
678 buf := make([]byte, 2*vors.FrameLen)
679 pcm := make([]int16, vors.FrameLen)
680 actr := make([]byte, 3)
681 nonce := make([]byte, 12)
682 nonce[len(nonce)-4] = sid
686 _, err = io.ReadFull(mic, buf)
688 log.Println("mic:", err)
695 for i = 0; i < vors.FrameLen; i++ {
696 pcm[i] = int16(uint16(buf[i*2+0]) | (uint16(buf[i*2+1]) << 8))
698 if vad != 0 && vors.RMS(pcm) < vad {
701 n, err = opusEnc.Encode(pcm, buf[4+len(actr):])
710 incr(nonce[len(nonce)-3:])
711 copy(buf, nonce[len(nonce)-4:])
713 ciph, err = chacha20.NewUnauthenticatedCipher(keyCiphOur, nonce)
718 buf[4+len(actr):4+len(actr)+n],
719 buf[4+len(actr):4+len(actr)+n],
722 if _, err = mac.Write(buf[:4+len(actr)+n]); err != nil {
726 copy(buf[4+len(actr)+n:], tag)
727 pkt = buf[:4+len(actr)+n+siphash.Size]
730 OurStats.bytes += vors.IPHdrLen(srvAddrUDP.IP) + 8 + uint64(len(pkt))
731 OurStats.last = time.Now()
733 if _, err = conn.Write(pkt); err != nil {
734 log.Println("send:", err)
740 if err != nil && err != gocui.ErrQuit {