]> Sergey Matveev's repositories - vors.git/blob - cmd/client/main.go
Shorter flags
[vors.git] / cmd / client / main.go
1 // VoRS -- Vo(IP) Really Simple
2 // Copyright (C) 2024 Sergey Matveev <stargrave@stargrave.org>
3 //
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.
7 //
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.
12 //
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/>.
15
16 package main
17
18 import (
19         "bytes"
20         "crypto/subtle"
21         "encoding/base64"
22         "encoding/binary"
23         "flag"
24         "fmt"
25         "io"
26         "log"
27         "net"
28         "os"
29         "os/exec"
30         "strconv"
31         "strings"
32         "sync"
33         "time"
34
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"
43 )
44
45 type Stream struct {
46         name     string
47         ctr      uint32
48         actr     uint32
49         muted    bool
50         silenced bool
51         in       chan []byte
52         stats    *Stats
53 }
54
55 var (
56         Streams  = map[byte]*Stream{}
57         StreamsM sync.RWMutex
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")
62         Muted    bool
63         Ctrl     = make(chan []byte)
64 )
65
66 func incr(data []byte) {
67         for i := len(data) - 1; i >= 0; i-- {
68                 data[i]++
69                 if data[i] != 0 {
70                         return
71                 }
72         }
73         panic("overflow")
74 }
75
76 func muteToggle() (muted bool) {
77         Muted = !Muted
78         if Ctrl != nil {
79                 var cmd string
80                 if Muted {
81                         cmd = vors.CmdMuted
82                 } else {
83                         cmd = vors.CmdUnmuted
84                 }
85                 Ctrl <- vors.ArgsEncode([]byte(cmd))
86         }
87         return Muted
88 }
89
90 func main() {
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")
106                 flag.PrintDefaults()
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
111 by pressing Enter.
112
113 Press F1 to toggle mute -- no sending of microphone audio to server).
114 Press F10 to quit.
115
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.`)
123         }
124         flag.Parse()
125         log.SetFlags(log.Lmicroseconds)
126
127         if *warranty {
128                 fmt.Println(vors.Warranty)
129                 return
130         }
131         if *version {
132                 fmt.Println(vors.GetVersion())
133                 return
134         }
135
136         var passwdHsh []byte
137         if *passwd != "" {
138                 hsh := blake2s.Sum256([]byte(*passwd))
139                 passwdHsh = hsh[:]
140         }
141
142         srvPub, err := base64.RawURLEncoding.DecodeString(*srvPubB64)
143         if err != nil {
144                 log.Fatal(err)
145         }
146         *Name = strings.ReplaceAll(*Name, " ", "-")
147
148         go func() {
149                 if *muteTogglePth == "" {
150                         return
151                 }
152                 for {
153                         fd, err := os.OpenFile(*muteTogglePth, os.O_WRONLY, os.FileMode(0666))
154                         if err != nil {
155                                 log.Fatalln(err)
156                         }
157                         var reply string
158                         if muteToggle() {
159                                 reply = "muted"
160                         } else {
161                                 reply = "unmuted"
162                         }
163                         fd.WriteString(reply + "\n")
164                         fd.Close()
165                         time.Sleep(time.Second)
166                 }
167         }()
168
169         vad := uint64(*vadRaw)
170         opusEnc := newOpusEnc()
171         var mic io.ReadCloser
172         if *recCmd != "" {
173                 cmd := vors.MakeCmd(*recCmd)
174                 mic, err = cmd.StdoutPipe()
175                 if err != nil {
176                         log.Fatal(err)
177                 }
178                 err = cmd.Start()
179                 if err != nil {
180                         log.Fatal(err)
181                 }
182         }
183
184         vors.PreferIPv4 = *prefer4
185         ctrlConn, err := net.DialTCP("tcp", nil, vors.MustResolveTCP(*srvAddr))
186         if err != nil {
187                 log.Fatalln("dial server:", err)
188         }
189         defer ctrlConn.Close()
190         if err = ctrlConn.SetNoDelay(true); err != nil {
191                 log.Fatalln("nodelay:", err)
192         }
193         ctrl := vors.NewNSConn(ctrlConn)
194
195         hs, err := noise.NewHandshakeState(noise.Config{
196                 CipherSuite: vors.NoiseCipherSuite,
197                 Pattern:     noise.HandshakeNK,
198                 Initiator:   true,
199                 PeerStatic:  srvPub,
200                 Prologue:    []byte(vors.NoisePrologue),
201         })
202         if err != nil {
203                 log.Fatalln("noise.NewHandshakeState:", err)
204         }
205         buf, _, _, err := hs.WriteMessage(nil, vors.ArgsEncode(
206                 []byte(*Name), []byte(*Room), passwdHsh,
207         ))
208         if err != nil {
209                 log.Fatalln("handshake encrypt:", err)
210         }
211         {
212                 var w bytes.Buffer
213                 w.WriteString(vors.NoisePrologue)
214                 netstring.NewWriter(&w).WriteChunk(buf)
215                 buf = w.Bytes()
216         }
217         _, err = io.Copy(ctrlConn, bytes.NewReader(buf))
218         if err != nil {
219                 log.Fatalln("write handshake:", err)
220                 return
221         }
222         buf = <-ctrl.Rx
223         if buf == nil {
224                 log.Fatalln("read handshake:", ctrl.Err)
225         }
226         buf, txCS, rxCS, err := hs.ReadMessage(nil, buf)
227         if err != nil {
228                 log.Fatalln("handshake decrypt:", err)
229         }
230
231         rx := make(chan []byte)
232         go func() {
233                 for buf := range ctrl.Rx {
234                         buf, err = rxCS.Decrypt(buf[:0], nil, buf)
235                         if err != nil {
236                                 log.Println("rx decrypt", err)
237                                 break
238                         }
239                         rx <- buf
240                 }
241                 Finish <- struct{}{}
242         }()
243
244         srvAddrUDP := vors.MustResolveUDP(*srvAddr)
245         conn, err := net.DialUDP("udp", nil, srvAddrUDP)
246         if err != nil {
247                 log.Fatalln("connect:", err)
248         }
249         var sid byte
250         {
251                 args, err := vors.ArgsDecode(buf)
252                 if err != nil {
253                         log.Fatalln("args decode:", err)
254                 }
255                 if len(args) < 2 {
256                         log.Fatalln("empty args")
257                 }
258                 var cookie vors.Cookie
259                 switch cmd := string(args[0]); cmd {
260                 case vors.CmdErr:
261                         log.Fatalln("handshake failed:", string(args[1]))
262                 case vors.CmdCookie:
263                         copy(cookie[:], args[1])
264                 default:
265                         log.Fatalln("unexpected post-handshake cmd:", cmd)
266                 }
267                 timeout := time.NewTimer(vors.PingTime)
268                 defer func() {
269                         if !timeout.Stop() {
270                                 <-timeout.C
271                         }
272                 }()
273                 ticker := time.NewTicker(time.Second)
274                 if _, err = conn.Write(cookie[:]); err != nil {
275                         log.Fatalln("write:", err)
276                 }
277         WaitForCookieAcceptance:
278                 for {
279                         select {
280                         case <-timeout.C:
281                                 log.Fatalln("cookie acceptance timeout")
282                         case <-ticker.C:
283                                 if _, err = conn.Write(cookie[:]); err != nil {
284                                         log.Fatalln("write:", err)
285                                 }
286                         case buf = <-rx:
287                                 args, err := vors.ArgsDecode(buf)
288                                 if err != nil {
289                                         log.Fatalln("args decode:", err)
290                                 }
291                                 if len(args) < 2 {
292                                         log.Fatalln("empty args")
293                                 }
294                                 switch cmd := string(args[0]); cmd {
295                                 case vors.CmdErr:
296                                         log.Fatalln("cookie acceptance failed:", string(args[1]))
297                                 case vors.CmdSID:
298                                         sid = args[1][0]
299                                         StreamsM.Lock()
300                                         Streams[sid] = &Stream{name: *Name, stats: OurStats}
301                                         StreamsM.Unlock()
302                                 default:
303                                         log.Fatalln("unexpected post-cookie cmd:", cmd)
304                                 }
305                                 break WaitForCookieAcceptance
306                         }
307                 }
308                 if !timeout.Stop() {
309                         <-timeout.C
310                 }
311         }
312
313         var keyCiphOur []byte
314         var keyMACOur []byte
315         {
316                 xof, err := blake2s.NewXOF(chacha20.KeySize+16, nil)
317                 if err != nil {
318                         log.Fatalln(err)
319                 }
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 {
324                         log.Fatalln(err)
325                 }
326                 keyCiphOur, keyMACOur = buf[:chacha20.KeySize], buf[chacha20.KeySize:]
327         }
328
329         seen := time.Now()
330
331         LoggerReady := make(chan struct{})
332         GUI, err = gocui.NewGui(gocui.OutputNormal)
333         if err != nil {
334                 log.Fatal(err)
335         }
336         defer GUI.Close()
337         GUI.SelFgColor = gocui.ColorCyan
338         GUI.Highlight = true
339         GUI.SetManagerFunc(guiLayout)
340         if err := GUI.SetKeybinding("", gocui.KeyTab, gocui.ModNone, tabHandle); err != nil {
341                 log.Fatal(err)
342         }
343         if err := GUI.SetKeybinding("", gocui.KeyF1, gocui.ModNone,
344                 func(gui *gocui.Gui, v *gocui.View) error {
345                         muteToggle()
346                         return nil
347                 },
348         ); err != nil {
349                 log.Fatal(err)
350         }
351         if err := GUI.SetKeybinding("", gocui.KeyF10, gocui.ModNone,
352                 func(gui *gocui.Gui, v *gocui.View) error {
353                         Finish <- struct{}{}
354                         return gocui.ErrQuit
355                 },
356         ); err != nil {
357                 log.Fatal(err)
358         }
359
360         go func() {
361                 <-GUIReadyC
362                 v, err := GUI.View("logs")
363                 if err != nil {
364                         log.Fatal(err)
365                 }
366                 log.SetOutput(v)
367                 log.Println("connected", "sid:", sid,
368                         "addr:", conn.LocalAddr().String())
369                 close(LoggerReady)
370                 for {
371                         time.Sleep(vors.ScreenRefresh)
372                         GUI.Update(func(gui *gocui.Gui) error {
373                                 return nil
374                         })
375                 }
376         }()
377
378         go func() {
379                 <-Finish
380                 go GUI.Close()
381                 time.Sleep(100 * time.Millisecond)
382                 os.Exit(0)
383         }()
384
385         go func() {
386                 for buf := range Ctrl {
387                         buf, err = txCS.Encrypt(nil, nil, buf)
388                         if err != nil {
389                                 log.Fatalln("tx encrypt:", err)
390                         }
391                         if err = ctrl.Tx(buf); err != nil {
392                                 log.Fatalln("tx:", err)
393                         }
394                 }
395         }()
396
397         go func() {
398                 for {
399                         time.Sleep(vors.PingTime)
400                         Ctrl <- vors.ArgsEncode([]byte(vors.CmdPing))
401                 }
402         }()
403
404         go func(seen *time.Time) {
405                 var now time.Time
406                 for buf := range rx {
407                         args, err := vors.ArgsDecode(buf)
408                         if err != nil {
409                                 log.Fatalln("args decode:", err)
410                         }
411                         if len(args) == 0 {
412                                 log.Fatalln("empty args")
413                         }
414                         switch cmd := string(args[0]); cmd {
415                         case vors.CmdPong:
416                                 now = time.Now()
417                                 *seen = now
418                         case vors.CmdAdd:
419                                 sidRaw, name, key := args[1], args[2], args[3]
420                                 sid := sidRaw[0]
421                                 log.Println("add", string(name), "sid:", sid)
422                                 keyCiph, keyMAC := key[:chacha20.KeySize], key[chacha20.KeySize:]
423                                 stream := &Stream{
424                                         name:  string(name),
425                                         in:    make(chan []byte, 1<<10),
426                                         stats: &Stats{dead: make(chan struct{})},
427                                 }
428                                 go func() {
429                                         dec, err := opus.NewDecoder(vors.Rate, 1)
430                                         if err != nil {
431                                                 log.Fatal(err)
432                                         }
433                                         if err = dec.SetComplexity(10); err != nil {
434                                                 log.Fatal(err)
435                                         }
436
437                                         var player io.WriteCloser
438                                         playerTx := make(chan []byte, 5)
439                                         var cmd *exec.Cmd
440                                         if *playCmd != "" {
441                                                 cmd = vors.MakeCmd(*playCmd)
442                                                 player, err = cmd.StdinPipe()
443                                                 if err != nil {
444                                                         log.Fatal(err)
445                                                 }
446                                                 err = cmd.Start()
447                                                 if err != nil {
448                                                         log.Fatal(err)
449                                                 }
450                                                 go func() {
451                                                         var pcmbuf []byte
452                                                         var ok bool
453                                                         var err error
454                                                         for {
455                                                                 for len(playerTx) > vors.MaxLost {
456                                                                         <-playerTx
457                                                                         stream.stats.reorder++
458                                                                 }
459                                                                 pcmbuf, ok = <-playerTx
460                                                                 if !ok {
461                                                                         break
462                                                                 }
463                                                                 if stream.silenced {
464                                                                         continue
465                                                                 }
466                                                                 if _, err = io.Copy(player,
467                                                                         bytes.NewReader(pcmbuf)); err != nil {
468                                                                         log.Println("play:", err)
469                                                                 }
470                                                         }
471                                                         cmd.Process.Kill()
472                                                 }()
473                                         }
474
475                                         var ciph *chacha20.Cipher
476                                         mac := siphash.New(keyMAC)
477                                         tag := make([]byte, siphash.Size)
478                                         var ctr uint32
479                                         pcm := make([]int16, vors.FrameLen)
480                                         nonce := make([]byte, 12)
481                                         var pkt []byte
482                                         lost := -1
483                                         var lastDur int
484                                         for buf := range stream.in {
485                                                 copy(nonce[len(nonce)-4:], buf)
486                                                 mac.Reset()
487                                                 if _, err = mac.Write(
488                                                         buf[:len(buf)-siphash.Size],
489                                                 ); err != nil {
490                                                         log.Fatal(err)
491                                                 }
492                                                 mac.Sum(tag[:0])
493                                                 if subtle.ConstantTimeCompare(
494                                                         tag[:siphash.Size],
495                                                         buf[len(buf)-siphash.Size:],
496                                                 ) != 1 {
497                                                         stream.stats.bads++
498                                                         continue
499                                                 }
500                                                 ciph, err = chacha20.NewUnauthenticatedCipher(
501                                                         keyCiph, nonce,
502                                                 )
503                                                 if err != nil {
504                                                         log.Fatal(err)
505                                                 }
506                                                 pkt = buf[4+3 : len(buf)-siphash.Size]
507                                                 ciph.XORKeyStream(pkt, pkt)
508
509                                                 ctr = binary.BigEndian.Uint32(nonce[len(nonce)-4:])
510                                                 if lost == -1 {
511                                                         // ignore the very first packet in the stream
512                                                         lost = 0
513                                                 } else {
514                                                         lost = int(ctr - (stream.ctr + 1))
515                                                 }
516                                                 stream.ctr = ctr
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 {
521                                                         lost = 0
522                                                 }
523                                                 for ; lost > 0; lost-- {
524                                                         lastDur, err = dec.LastPacketDuration()
525                                                         if err != nil {
526                                                                 log.Println("PLC:", err)
527                                                                 continue
528                                                         }
529                                                         err = dec.DecodePLC(pcm[:lastDur])
530                                                         if err != nil {
531                                                                 log.Println("PLC:", err)
532                                                                 continue
533                                                         }
534                                                         stream.stats.AddRMS(pcm)
535                                                         if cmd == nil {
536                                                                 continue
537                                                         }
538                                                         pcmbuf := make([]byte, 2*lastDur)
539                                                         pcmConv(pcmbuf, pcm[:lastDur])
540                                                         playerTx <- pcmbuf
541                                                 }
542                                                 _, err = dec.Decode(pkt, pcm)
543                                                 if err != nil {
544                                                         log.Println("decode:", err)
545                                                         continue
546                                                 }
547                                                 stream.stats.AddRMS(pcm)
548                                                 stream.stats.last = time.Now()
549                                                 if cmd == nil {
550                                                         continue
551                                                 }
552                                                 pcmbuf := make([]byte, 2*len(pcm))
553                                                 pcmConv(pcmbuf, pcm)
554                                                 playerTx <- pcmbuf
555                                         }
556                                         if cmd != nil {
557                                                 close(playerTx)
558                                         }
559                                 }()
560                                 go statsDrawer(stream)
561                                 StreamsM.Lock()
562                                 Streams[sid] = stream
563                                 StreamsM.Unlock()
564                         case vors.CmdDel:
565                                 sid := args[1][0]
566                                 s := Streams[sid]
567                                 if s == nil {
568                                         log.Println("unknown sid:", sid)
569                                         continue
570                                 }
571                                 log.Println("del", s.name, "sid:", sid)
572                                 StreamsM.Lock()
573                                 delete(Streams, sid)
574                                 StreamsM.Unlock()
575                                 close(s.in)
576                                 close(s.stats.dead)
577                         case vors.CmdMuted:
578                                 sid := args[1][0]
579                                 s := Streams[sid]
580                                 if s == nil {
581                                         log.Println("unknown sid:", sid)
582                                         continue
583                                 }
584                                 s.muted = true
585                         case vors.CmdUnmuted:
586                                 sid := args[1][0]
587                                 s := Streams[sid]
588                                 if s == nil {
589                                         log.Println("unknown sid:", sid)
590                                         continue
591                                 }
592                                 s.muted = false
593                         case vors.CmdChat:
594                                 sid := args[1][0]
595                                 s := Streams[sid]
596                                 if s == nil {
597                                         log.Println("unknown sid:", sid)
598                                         continue
599                                 }
600                                 log.Println(s.name, ":", string(args[2]))
601                         default:
602                                 log.Fatal("unexpected cmd:", cmd)
603                         }
604                 }
605         }(&seen)
606
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)
611                                 Finish <- struct{}{}
612                                 break
613                         }
614                 }
615         }(&seen)
616
617         go func() {
618                 <-LoggerReady
619                 var n int
620                 var from *net.UDPAddr
621                 var err error
622                 var stream *Stream
623                 var ctr uint32
624                 for {
625                         buf := make([]byte, 2*vors.FrameLen)
626                         n, from, err = conn.ReadFromUDP(buf)
627                         if err != nil {
628                                 log.Println("recvfrom:", err)
629                                 Finish <- struct{}{}
630                                 break
631                         }
632                         if from.Port != srvAddrUDP.Port || !from.IP.Equal(srvAddrUDP.IP) {
633                                 log.Println("wrong addr:", from)
634                                 continue
635                         }
636                         if n <= 4+siphash.Size {
637                                 log.Println("too small:", n)
638                                 continue
639                         }
640                         stream = Streams[buf[0]]
641                         if stream == nil {
642                                 log.Println("unknown stream:", buf[0])
643                                 continue
644                         }
645                         stream.stats.pkts++
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++
650                                 continue
651                         }
652                         stream.in <- buf[:n]
653                 }
654         }()
655
656         go statsDrawer(&Stream{name: *Name, stats: OurStats})
657         go func() {
658                 <-LoggerReady
659                 for now := range time.NewTicker(time.Second).C {
660                         if !OurStats.last.Add(time.Second).Before(now) {
661                                 continue
662                         }
663                         OurStats.pkts++
664                         OurStats.bytes += vors.IPHdrLen(srvAddrUDP.IP) + 8 + 1
665                         if _, err = conn.Write([]byte{sid}); err != nil {
666                                 log.Println("send:", err)
667                         }
668                 }
669         }()
670         go func() {
671                 if *recCmd == "" {
672                         return
673                 }
674                 <-LoggerReady
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
683                 var pkt []byte
684                 var n, i int
685                 for {
686                         _, err = io.ReadFull(mic, buf)
687                         if err != nil {
688                                 log.Println("mic:", err)
689                                 break
690                         }
691                         incr(actr[:])
692                         if Muted {
693                                 continue
694                         }
695                         for i = 0; i < vors.FrameLen; i++ {
696                                 pcm[i] = int16(uint16(buf[i*2+0]) | (uint16(buf[i*2+1]) << 8))
697                         }
698                         if vad != 0 && vors.RMS(pcm) < vad {
699                                 continue
700                         }
701                         n, err = opusEnc.Encode(pcm, buf[4+len(actr):])
702                         if err != nil {
703                                 log.Fatal(err)
704                         }
705                         if n <= 2 {
706                                 // DTX
707                                 continue
708                         }
709
710                         incr(nonce[len(nonce)-3:])
711                         copy(buf, nonce[len(nonce)-4:])
712                         copy(buf[4:], actr)
713                         ciph, err = chacha20.NewUnauthenticatedCipher(keyCiphOur, nonce)
714                         if err != nil {
715                                 log.Fatal(err)
716                         }
717                         ciph.XORKeyStream(
718                                 buf[4+len(actr):4+len(actr)+n],
719                                 buf[4+len(actr):4+len(actr)+n],
720                         )
721                         mac.Reset()
722                         if _, err = mac.Write(buf[:4+len(actr)+n]); err != nil {
723                                 log.Fatal(err)
724                         }
725                         mac.Sum(tag[:0])
726                         copy(buf[4+len(actr)+n:], tag)
727                         pkt = buf[:4+len(actr)+n+siphash.Size]
728
729                         OurStats.pkts++
730                         OurStats.bytes += vors.IPHdrLen(srvAddrUDP.IP) + 8 + uint64(len(pkt))
731                         OurStats.last = time.Now()
732                         OurStats.AddRMS(pcm)
733                         if _, err = conn.Write(pkt); err != nil {
734                                 log.Println("send:", err)
735                         }
736                 }
737         }()
738
739         err = GUI.MainLoop()
740         if err != nil && err != gocui.ErrQuit {
741                 log.Fatal(err)
742         }
743 }