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.stargrave.org/opus/v2"
39 vors "go.stargrave.org/vors/internal"
40 "golang.org/x/crypto/blake2s"
41 "golang.org/x/crypto/chacha20"
52 Streams = map[byte]*Stream{}
53 Finish = make(chan struct{})
54 OurStats = &Stats{dead: make(chan struct{})}
55 Name = flag.String("name", "test", "username")
56 Room = flag.String("room", "/", "room name")
60 func parseSID(s string) byte {
61 n, err := strconv.Atoi(s)
66 log.Fatal("too big stream num")
71 func makeCmd(cmd string) *exec.Cmd {
72 args := strings.Fields(cmd)
74 return exec.Command(args[0])
76 return exec.Command(args[0], args[1:]...)
79 func incr(data []byte) {
80 for i := len(data) - 1; i >= 0; i-- {
89 const soxParams = "--no-show-progress --buffer 1920 --channels 1 --endian little --encoding signed --rate 48000 --bits 16 --type raw -"
92 srvAddr := flag.String("srv", "vors.home.arpa:"+strconv.Itoa(vors.DefaultPort),
93 "host:TCP/UDP port to connect to")
94 srvPubB64 := flag.String("pub", "", "server's public key, Base64")
95 recCmd := flag.String("rec", "rec "+soxParams, "rec command")
96 playCmd := flag.String("play", "play "+soxParams, "play command")
97 vadRaw := flag.Uint("vad", 0, "VAD threshold")
98 passwd := flag.String("passwd", "", "protected room's password")
99 muteToggle := flag.String("mute-toggle", "", "path to FIFO to toggle mute")
100 version := flag.Bool("version", false, "print version")
101 warranty := flag.Bool("warranty", false, "print warranty information")
103 log.SetFlags(log.Lmicroseconds | log.Lshortfile)
106 fmt.Println(vors.Warranty)
110 fmt.Println(vors.GetVersion())
114 srvPub, err := base64.RawURLEncoding.DecodeString(*srvPubB64)
118 *Name = strings.ReplaceAll(*Name, " ", "-")
121 if *muteToggle == "" {
125 fd, err := os.OpenFile(*muteToggle, os.O_WRONLY, os.FileMode(0666))
136 fd.WriteString(reply + "\n")
138 time.Sleep(time.Second)
142 vad := uint64(*vadRaw)
143 opusEnc := newOpusEnc()
144 var mic io.ReadCloser
146 cmd := makeCmd(*recCmd)
147 mic, err = cmd.StdoutPipe()
157 ctrl, err := net.DialTCP("tcp", nil, vors.MustResolveTCP(*srvAddr))
159 log.Fatalln("dial server:", err)
162 if err = ctrl.SetNoDelay(true); err != nil {
163 log.Fatalln("nodelay:", err)
166 hs, err := noise.NewHandshakeState(noise.Config{
167 CipherSuite: vors.NoiseCipherSuite,
168 Pattern: noise.HandshakeNK,
171 Prologue: []byte(vors.NoisePrologue),
174 log.Fatalln("noise.NewHandshakeState:", err)
176 buf, _, _, err := hs.WriteMessage(nil, []byte(*Name+" "+*Room+" "+*passwd))
178 log.Fatalln("handshake encrypt:", err)
182 []byte(vors.NoisePrologue),
183 byte((len(buf) & 0xFF00) >> 8),
184 byte((len(buf) & 0x00FF) >> 0),
188 _, err = io.Copy(ctrl, bytes.NewReader(buf))
190 log.Fatalln("write handshake:", err)
193 buf, err = vors.PktRead(ctrl)
195 log.Fatalln("read handshake:", err)
197 buf, txCS, rxCS, err := hs.ReadMessage(nil, buf)
199 log.Fatalln("handshake decrypt:", err)
202 rx := make(chan []byte)
205 buf, err := vors.PktRead(ctrl)
207 log.Println("rx", err)
210 buf, err = rxCS.Decrypt(buf[:0], nil, buf)
212 log.Println("rx decrypt", err)
220 srvAddrUDP := vors.MustResolveUDP(*srvAddr)
221 conn, err := net.DialUDP("udp", nil, srvAddrUDP)
223 log.Fatalln("connect:", err)
227 cols := strings.Fields(string(buf))
228 if cols[0] != "OK" || len(cols) != 2 {
229 log.Fatalln("handshake failed:", cols)
231 var cookie vors.Cookie
232 cookieRaw, err := hex.DecodeString(cols[1])
236 copy(cookie[:], cookieRaw)
237 timeout := time.NewTimer(vors.PingTime)
243 ticker := time.NewTicker(time.Second)
244 if _, err = conn.Write(cookie[:]); err != nil {
245 log.Fatalln("write:", err)
247 WaitForCookieAcceptance:
251 log.Fatalln("cookie acceptance timeout")
253 if _, err = conn.Write(cookie[:]); err != nil {
254 log.Fatalln("write:", err)
257 cols = strings.Fields(string(buf))
258 if cols[0] != "SID" || len(cols) != 2 {
259 log.Fatalln("cookie acceptance failed:", string(buf))
261 sid = parseSID(cols[1])
262 Streams[sid] = &Stream{name: *Name, stats: OurStats}
263 break WaitForCookieAcceptance
271 var keyCiphOur []byte
274 xof, err := blake2s.NewXOF(32+16, nil)
278 xof.Write([]byte(vors.NoisePrologue))
279 xof.Write(hs.ChannelBinding())
280 buf := make([]byte, 32+16)
281 if _, err = io.ReadFull(xof, buf); err != nil {
284 keyCiphOur, keyMACOur = buf[:32], buf[32:]
289 LoggerReady := make(chan struct{})
290 GUI, err = gocui.NewGui(gocui.OutputNormal)
295 GUI.SelFgColor = gocui.ColorCyan
297 GUI.SetManagerFunc(guiLayout)
298 if err := GUI.SetKeybinding("", gocui.KeyF10, gocui.ModNone, guiQuit); err != nil {
301 if err := GUI.SetKeybinding("", gocui.KeyEnter, gocui.ModNone, mute); err != nil {
307 v, err := GUI.View("logs")
312 log.Println("connected", "sid:", sid,
313 "addr:", conn.LocalAddr().String())
316 time.Sleep(vors.ScreenRefresh)
317 GUI.Update(func(gui *gocui.Gui) error {
326 time.Sleep(100 * time.Millisecond)
332 time.Sleep(vors.PingTime)
333 buf, err := txCS.Encrypt(nil, nil, []byte(vors.CmdPing))
335 log.Fatalln("tx encrypt:", err)
337 if err = vors.PktWrite(ctrl, buf); err != nil {
338 log.Fatalln("tx:", err)
343 go func(seen *time.Time) {
345 for buf := range rx {
346 if string(buf) == vors.CmdPong {
351 cols := strings.Fields(string(buf))
354 sidRaw, name, keyHex := cols[1], cols[2], cols[3]
355 log.Println("add", name, "sid:", sidRaw)
356 sid := parseSID(sidRaw)
357 key, err := hex.DecodeString(keyHex)
361 keyCiph, keyMAC := key[:32], key[32:]
364 in: make(chan []byte, 1<<10),
365 stats: &Stats{dead: make(chan struct{})},
368 dec, err := opus.NewDecoder(vors.Rate, 1)
372 if err = dec.SetComplexity(10); err != nil {
376 var player io.WriteCloser
377 playerTx := make(chan []byte, 5)
380 cmd = makeCmd(*playCmd)
381 player, err = cmd.StdinPipe()
394 for len(playerTx) > 1 {
396 stream.stats.reorder++
398 pcmbuf, ok = <-playerTx
402 if _, err = io.Copy(player,
403 bytes.NewReader(pcmbuf)); err != nil {
404 log.Println("play:", err)
411 var ciph *chacha20.Cipher
412 mac := siphash.New(keyMAC)
413 tag := make([]byte, siphash.Size)
415 pcm := make([]int16, vors.FrameLen)
416 nonce := make([]byte, 12)
420 for buf := range stream.in {
421 copy(nonce[len(nonce)-4:], buf)
423 if _, err = mac.Write(buf[: len(buf)-siphash.Size]); err != nil {
427 if subtle.ConstantTimeCompare(
429 buf[len(buf)-siphash.Size:],
434 ciph, err = chacha20.NewUnauthenticatedCipher(keyCiph, nonce)
438 pkt = buf[4 : len(buf)-siphash.Size]
439 ciph.XORKeyStream(pkt, pkt)
441 ctr = binary.BigEndian.Uint32(nonce[len(nonce)-4:])
443 // ignore the very first packet in the stream
446 lost = int(ctr - (stream.ctr + 1))
449 stream.stats.lost += int64(lost)
450 if lost > vors.MaxLost {
453 for ; lost > 0; lost-- {
454 lastDur, err = dec.LastPacketDuration()
456 log.Println("PLC:", err)
459 err = dec.DecodePLC(pcm[:lastDur])
461 log.Println("PLC:", err)
464 stream.stats.AddRMS(pcm)
468 pcmbuf := make([]byte, 2*lastDur)
469 pcmConv(pcmbuf, pcm[:lastDur])
472 _, err = dec.Decode(pkt, pcm)
474 log.Println("decode:", err)
477 stream.stats.AddRMS(pcm)
478 stream.stats.last = time.Now()
482 pcmbuf := make([]byte, 2*len(pcm))
490 go statsDrawer(stream.stats, stream.name)
491 Streams[sid] = stream
493 sid := parseSID(cols[1])
496 log.Println("unknown sid:", sid)
499 log.Println("del", s.name, "sid:", cols[1])
504 log.Fatal("unknown cmd:", cols[0])
509 go func(seen *time.Time) {
510 for now := range time.Tick(vors.PingTime) {
511 if seen.Add(2 * vors.PingTime).Before(now) {
512 log.Println("timeout:", seen)
522 var from *net.UDPAddr
527 buf := make([]byte, 2*vors.FrameLen)
528 n, from, err = conn.ReadFromUDP(buf)
530 log.Println("recvfrom:", err)
534 if from.Port != srvAddrUDP.Port || !from.IP.Equal(srvAddrUDP.IP) {
535 log.Println("wrong addr:", from)
538 if n <= 4+siphash.Size {
539 log.Println("too small:", n)
542 stream = Streams[buf[0]]
544 // log.Println("unknown stream:", buf[0])
548 stream.stats.bytes += vors.IPHdrLen(from.IP) + 8 + uint64(n)
549 ctr = binary.BigEndian.Uint32(buf)
550 if ctr <= stream.ctr {
551 stream.stats.reorder++
558 go statsDrawer(OurStats, *Name)
561 for now := range time.NewTicker(time.Second).C {
562 if !OurStats.last.Add(time.Second).Before(now) {
566 OurStats.bytes += vors.IPHdrLen(srvAddrUDP.IP) + 8 + 1
567 if _, err = conn.Write([]byte{sid}); err != nil {
568 log.Println("send:", err)
578 var ciph *chacha20.Cipher
579 mac := siphash.New(keyMACOur)
580 tag := make([]byte, siphash.Size)
581 buf := make([]byte, 2*vors.FrameLen)
582 pcm := make([]int16, vors.FrameLen)
583 nonce := make([]byte, 12)
584 nonce[len(nonce)-4] = sid
588 _, err = io.ReadFull(mic, buf)
590 log.Println("mic:", err)
596 for i = 0; i < vors.FrameLen; i++ {
597 pcm[i] = int16(uint16(buf[i*2+0]) | (uint16(buf[i*2+1]) << 8))
599 if vad != 0 && vors.RMS(pcm) < vad {
602 n, err = opusEnc.Encode(pcm, buf[4:])
611 incr(nonce[len(nonce)-3:])
612 copy(buf, nonce[len(nonce)-4:])
613 ciph, err = chacha20.NewUnauthenticatedCipher(keyCiphOur, nonce)
617 ciph.XORKeyStream(buf[4:4+n], buf[4:4+n])
619 if _, err = mac.Write(buf[: 4+n]); err != nil {
624 pkt = buf[:4+n+siphash.Size]
627 OurStats.bytes += vors.IPHdrLen(srvAddrUDP.IP) + 8 + uint64(len(pkt))
628 OurStats.last = time.Now()
630 if _, err = conn.Write(pkt); err != nil {
631 log.Println("send:", err)
638 if err != nil && err != gocui.ErrQuit {