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