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. "TALK" means that recently an audio packet
120 was received. "MUTE" means that peer is in muted mode. "SILENT" means
121 that peer is locally muted.`)
124 log.SetFlags(log.Lmicroseconds)
127 fmt.Println(vors.Warranty)
131 fmt.Println(vors.GetVersion())
137 hsh := blake2s.Sum256([]byte(*passwd))
141 srvPub, err := base64.RawURLEncoding.DecodeString(*srvPubB64)
145 *Name = strings.ReplaceAll(*Name, " ", "-")
148 if *muteTogglePth == "" {
152 fd, err := os.OpenFile(*muteTogglePth, os.O_WRONLY, os.FileMode(0666))
162 fd.WriteString(reply + "\n")
164 time.Sleep(time.Second)
168 vad := uint64(*vadRaw)
169 opusEnc := newOpusEnc()
170 var mic io.ReadCloser
172 cmd := vors.MakeCmd(*recCmd)
173 mic, err = cmd.StdoutPipe()
183 vors.PreferIPv4 = *prefer4
184 ctrlConn, err := net.DialTCP("tcp", nil, vors.MustResolveTCP(*srvAddr))
186 log.Fatalln("dial server:", err)
188 defer ctrlConn.Close()
189 if err = ctrlConn.SetNoDelay(true); err != nil {
190 log.Fatalln("nodelay:", err)
192 ctrl := vors.NewNSConn(ctrlConn)
194 hs, err := noise.NewHandshakeState(noise.Config{
195 CipherSuite: vors.NoiseCipherSuite,
196 Pattern: noise.HandshakeNK,
199 Prologue: []byte(vors.NoisePrologue),
202 log.Fatalln("noise.NewHandshakeState:", err)
204 buf, _, _, err := hs.WriteMessage(nil, vors.ArgsEncode(
205 []byte(*Name), []byte(*Room), passwdHsh,
208 log.Fatalln("handshake encrypt:", err)
212 w.WriteString(vors.NoisePrologue)
213 netstring.NewWriter(&w).WriteChunk(buf)
216 _, err = io.Copy(ctrlConn, bytes.NewReader(buf))
218 log.Fatalln("write handshake:", err)
223 log.Fatalln("read handshake:", ctrl.Err)
225 buf, txCS, rxCS, err := hs.ReadMessage(nil, buf)
227 log.Fatalln("handshake decrypt:", err)
230 rx := make(chan []byte)
232 for buf := range ctrl.Rx {
233 buf, err = rxCS.Decrypt(buf[:0], nil, buf)
235 log.Println("rx decrypt", err)
243 srvAddrUDP := vors.MustResolveUDP(*srvAddr)
244 conn, err := net.DialUDP("udp", nil, srvAddrUDP)
246 log.Fatalln("connect:", err)
250 args, err := vors.ArgsDecode(buf)
252 log.Fatalln("args decode:", err)
255 log.Fatalln("empty args")
257 var cookie vors.Cookie
258 switch cmd := string(args[0]); cmd {
260 log.Fatalln("handshake failed:", string(args[1]))
262 copy(cookie[:], args[1])
264 log.Fatalln("unexpected post-handshake cmd:", cmd)
266 timeout := time.NewTimer(vors.PingTime)
272 ticker := time.NewTicker(time.Second)
273 if _, err = conn.Write(cookie[:]); err != nil {
274 log.Fatalln("write:", err)
276 WaitForCookieAcceptance:
280 log.Fatalln("cookie acceptance timeout")
282 if _, err = conn.Write(cookie[:]); err != nil {
283 log.Fatalln("write:", err)
286 args, err := vors.ArgsDecode(buf)
288 log.Fatalln("args decode:", err)
291 log.Fatalln("empty args")
293 switch cmd := string(args[0]); cmd {
295 log.Fatalln("cookie acceptance failed:", string(args[1]))
299 Streams[sid] = &Stream{name: *Name, stats: OurStats}
302 log.Fatalln("unexpected post-cookie cmd:", cmd)
304 break WaitForCookieAcceptance
312 var keyCiphOur []byte
315 xof, err := blake2s.NewXOF(chacha20.KeySize+16, nil)
319 xof.Write([]byte(vors.NoisePrologue))
320 xof.Write(hs.ChannelBinding())
321 buf := make([]byte, chacha20.KeySize+16)
322 if _, err = io.ReadFull(xof, buf); err != nil {
325 keyCiphOur, keyMACOur = buf[:chacha20.KeySize], buf[chacha20.KeySize:]
330 LoggerReady := make(chan struct{})
331 GUI, err = gocui.NewGui(gocui.OutputNormal)
336 GUI.SelFgColor = gocui.ColorCyan
338 GUI.SetManagerFunc(guiLayout)
339 if err := GUI.SetKeybinding("", gocui.KeyTab, gocui.ModNone, tabHandle); err != nil {
342 if err := GUI.SetKeybinding("", gocui.KeyF1, gocui.ModNone,
343 func(gui *gocui.Gui, v *gocui.View) error {
350 if err := GUI.SetKeybinding("", gocui.KeyF10, gocui.ModNone,
351 func(gui *gocui.Gui, v *gocui.View) error {
361 v, err := GUI.View("logs")
366 log.Println("connected", "sid:", sid,
367 "addr:", conn.LocalAddr().String())
370 time.Sleep(vors.ScreenRefresh)
371 GUI.Update(func(gui *gocui.Gui) error {
380 time.Sleep(100 * time.Millisecond)
385 for buf := range Ctrl {
386 buf, err = txCS.Encrypt(nil, nil, buf)
388 log.Fatalln("tx encrypt:", err)
390 if err = ctrl.Tx(buf); err != nil {
391 log.Fatalln("tx:", err)
398 time.Sleep(vors.PingTime)
399 Ctrl <- vors.ArgsEncode([]byte(vors.CmdPing))
403 go func(seen *time.Time) {
405 for buf := range rx {
406 args, err := vors.ArgsDecode(buf)
408 log.Fatalln("args decode:", err)
411 log.Fatalln("empty args")
413 switch cmd := string(args[0]); cmd {
418 sidRaw, name, key := args[1], args[2], args[3]
420 log.Println("add", string(name), "sid:", sid)
421 keyCiph, keyMAC := key[:chacha20.KeySize], key[chacha20.KeySize:]
424 in: make(chan []byte, 1<<10),
425 stats: &Stats{dead: make(chan struct{})},
428 dec, err := opus.NewDecoder(vors.Rate, 1)
432 if err = dec.SetComplexity(10); err != nil {
436 var player io.WriteCloser
437 playerTx := make(chan []byte, 5)
440 cmd = vors.MakeCmd(*playCmd)
441 player, err = cmd.StdinPipe()
454 for len(playerTx) > vors.MaxLost {
456 stream.stats.reorder++
458 pcmbuf, ok = <-playerTx
465 if _, err = io.Copy(player,
466 bytes.NewReader(pcmbuf)); err != nil {
467 log.Println("play:", err)
474 var ciph *chacha20.Cipher
475 mac := siphash.New(keyMAC)
476 tag := make([]byte, siphash.Size)
478 pcm := make([]int16, vors.FrameLen)
479 nonce := make([]byte, 12)
483 for buf := range stream.in {
484 copy(nonce[len(nonce)-4:], buf)
486 if _, err = mac.Write(
487 buf[:len(buf)-siphash.Size],
492 if subtle.ConstantTimeCompare(
494 buf[len(buf)-siphash.Size:],
499 ciph, err = chacha20.NewUnauthenticatedCipher(
505 pkt = buf[4+3 : len(buf)-siphash.Size]
506 ciph.XORKeyStream(pkt, pkt)
508 ctr = binary.BigEndian.Uint32(nonce[len(nonce)-4:])
510 // ignore the very first packet in the stream
513 lost = int(ctr - (stream.ctr + 1))
516 stream.actr = uint32(buf[4+0])<<16 |
517 uint32(buf[4+1])<<8 | uint32(buf[4+2])
518 stream.stats.lost += int64(lost)
519 if lost > vors.MaxLost {
522 for ; lost > 0; lost-- {
523 lastDur, err = dec.LastPacketDuration()
525 log.Println("PLC:", err)
528 err = dec.DecodePLC(pcm[:lastDur])
530 log.Println("PLC:", err)
533 stream.stats.AddRMS(pcm)
537 pcmbuf := make([]byte, 2*lastDur)
538 pcmConv(pcmbuf, pcm[:lastDur])
541 _, err = dec.Decode(pkt, pcm)
543 log.Println("decode:", err)
546 stream.stats.AddRMS(pcm)
547 stream.stats.last = time.Now()
551 pcmbuf := make([]byte, 2*len(pcm))
559 go statsDrawer(stream)
561 Streams[sid] = stream
567 log.Println("unknown sid:", sid)
570 log.Println("del", s.name, "sid:", sid)
580 log.Println("unknown sid:", sid)
584 case vors.CmdUnmuted:
588 log.Println("unknown sid:", sid)
596 log.Println("unknown sid:", sid)
599 log.Println(s.name, ":", string(args[2]))
601 log.Fatal("unexpected cmd:", cmd)
606 go func(seen *time.Time) {
607 for now := range time.Tick(vors.PingTime) {
608 if seen.Add(2 * vors.PingTime).Before(now) {
609 log.Println("timeout:", seen)
619 var from *net.UDPAddr
624 buf := make([]byte, 2*vors.FrameLen)
625 n, from, err = conn.ReadFromUDP(buf)
627 log.Println("recvfrom:", err)
631 if from.Port != srvAddrUDP.Port || !from.IP.Equal(srvAddrUDP.IP) {
632 log.Println("wrong addr:", from)
635 if n <= 4+siphash.Size {
636 log.Println("too small:", n)
639 stream = Streams[buf[0]]
641 log.Println("unknown stream:", buf[0])
645 stream.stats.bytes += vors.IPHdrLen(from.IP) + 8 + uint64(n)
646 ctr = binary.BigEndian.Uint32(buf)
647 if ctr <= stream.ctr {
648 stream.stats.reorder++
655 go statsDrawer(&Stream{name: *Name, stats: OurStats})
658 for now := range time.NewTicker(time.Second).C {
659 if !OurStats.last.Add(time.Second).Before(now) {
663 OurStats.bytes += vors.IPHdrLen(srvAddrUDP.IP) + 8 + 1
664 if _, err = conn.Write([]byte{sid}); err != nil {
665 log.Println("send:", err)
674 var ciph *chacha20.Cipher
675 mac := siphash.New(keyMACOur)
676 tag := make([]byte, siphash.Size)
677 buf := make([]byte, 2*vors.FrameLen)
678 pcm := make([]int16, vors.FrameLen)
679 actr := make([]byte, 3)
680 nonce := make([]byte, 12)
681 nonce[len(nonce)-4] = sid
685 _, err = io.ReadFull(mic, buf)
687 log.Println("mic:", err)
694 for i = 0; i < vors.FrameLen; i++ {
695 pcm[i] = int16(uint16(buf[i*2+0]) | (uint16(buf[i*2+1]) << 8))
697 if vad != 0 && vors.RMS(pcm) < vad {
700 n, err = opusEnc.Encode(pcm, buf[4+len(actr):])
709 incr(nonce[len(nonce)-3:])
710 copy(buf, nonce[len(nonce)-4:])
712 ciph, err = chacha20.NewUnauthenticatedCipher(keyCiphOur, nonce)
717 buf[4+len(actr):4+len(actr)+n],
718 buf[4+len(actr):4+len(actr)+n],
721 if _, err = mac.Write(buf[:4+len(actr)+n]); err != nil {
725 copy(buf[4+len(actr)+n:], tag)
726 pkt = buf[:4+len(actr)+n+siphash.Size]
729 OurStats.bytes += vors.IPHdrLen(srvAddrUDP.IP) + 8 + uint64(len(pkt))
730 OurStats.last = time.Now()
732 if _, err = conn.Write(pkt); err != nil {
733 log.Println("send:", err)
739 if err != nil && err != gocui.ErrQuit {