/
termshark.go
1369 lines (1205 loc) · 46.6 KB
/
termshark.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source
// code is governed by the MIT license that can be found in the LICENSE
// file.
package main
import (
"bytes"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"sync"
"syscall"
"time"
"github.com/blang/semver"
"github.com/gcla/gowid"
"github.com/gcla/termshark/v2"
"github.com/gcla/termshark/v2/capinfo"
"github.com/gcla/termshark/v2/cli"
"github.com/gcla/termshark/v2/configs/profiles"
"github.com/gcla/termshark/v2/convs"
"github.com/gcla/termshark/v2/pcap"
"github.com/gcla/termshark/v2/shark"
"github.com/gcla/termshark/v2/streams"
"github.com/gcla/termshark/v2/system"
"github.com/gcla/termshark/v2/tty"
"github.com/gcla/termshark/v2/ui"
"github.com/gcla/termshark/v2/widgets/filter"
"github.com/gcla/termshark/v2/widgets/wormhole"
"github.com/gdamore/tcell/v2"
flags "github.com/jessevdk/go-flags"
"github.com/mattn/go-isatty"
"github.com/shibukawa/configdir"
log "github.com/sirupsen/logrus"
"github.com/spf13/viper"
"net/http"
_ "net/http"
_ "net/http/pprof"
)
//======================================================================
// Run cmain() and afterwards make sure all goroutines stop, then exit with
// the correct exit code. Go's main() prototype does not provide for returning
// a value.
func main() {
// TODO - fix this later. goroutinewg is used every time a
// goroutine is started, to ensure we don't terminate until all are
// stopped. Any exception is a bug.
var ensureGoroutinesStopWG sync.WaitGroup
filter.Goroutinewg = &ensureGoroutinesStopWG
termshark.Goroutinewg = &ensureGoroutinesStopWG
pcap.Goroutinewg = &ensureGoroutinesStopWG
streams.Goroutinewg = &ensureGoroutinesStopWG
capinfo.Goroutinewg = &ensureGoroutinesStopWG
convs.Goroutinewg = &ensureGoroutinesStopWG
ui.Goroutinewg = &ensureGoroutinesStopWG
wormhole.Goroutinewg = &ensureGoroutinesStopWG
res := cmain()
ensureGoroutinesStopWG.Wait()
if !termshark.ShouldSwitchTerminal && !termshark.ShouldSwitchBack {
os.Exit(res)
}
os.Clearenv()
for _, e := range termshark.OriginalEnv {
ks := strings.SplitN(e, "=", 2)
if len(ks) == 2 {
os.Setenv(ks[0], ks[1])
}
}
exe, err := os.Executable()
if err != nil {
log.Warnf("Unexpected error determining termshark executable: %v", err)
os.Exit(1)
}
switch {
case termshark.ShouldSwitchTerminal:
os.Setenv("TERMSHARK_ORIGINAL_TERM", os.Getenv("TERM"))
os.Setenv("TERM", fmt.Sprintf("%s-256color", os.Getenv("TERM")))
case termshark.ShouldSwitchBack:
os.Setenv("TERM", os.Getenv("TERMSHARK_ORIGINAL_TERM"))
os.Setenv("TERMSHARK_ORIGINAL_TERM", "")
}
// Need exec because we really need to re-initialize everything, including have
// all init() functions be called again
err = syscall.Exec(exe, os.Args, os.Environ())
if err != nil {
log.Warnf("Unexpected error exec-ing termshark %s: %v", exe, err)
res = 1
}
os.Exit(res)
}
func cmain() int {
startedSuccessfully := false // true if we reached the point where packets were received and the UI started.
uiSuspended := false // true if the UI was suspended due to SIGTSTP
// Preserve in case we need to re-exec e.g. if the user switches TERM
termshark.OriginalEnv = os.Environ()
sigChan := make(chan os.Signal, 100)
// SIGINT and SIGQUIT will arrive only via an external kill command,
// not the keyboard, because our line discipline is set up to pass
// ctrl-c and ctrl-\ to termshark as keypress events. But we slightly
// modify tcell's default and set up ctrl-z to invoke signal SIGTSTP
// on the foreground process group. An alternative would just be to
// recognize ctrl-z in termshark and issue a SIGSTOP to getpid() from
// termshark but this wouldn't stop other processes in a termshark
// pipeline e.g.
//
// tcpdump -i eth0 -w - | termshark -i -
//
// sending SIGSTOP to getpid() would not stop tcpdump. The expectation
// with bash job control is that all processes in the foreground
// process group will be suspended. I could send SIGSTOP to 0, to try
// to get all processes in the group, but if e.g. tcpdump is running
// as root and termshark is not, tcpdump will not be suspended. If
// instead I set the line discipline such that ctrl-z is not passed
// through but maps to SIGTSTP, then tcpdump will be stopped by ctrl-z
// via the shell by virtue of the fact that when all pipeline
// processes start running, they use the same tty line discipline.
system.RegisterForSignals(sigChan)
stdConf := configdir.New("", "termshark")
dirs := stdConf.QueryFolders(configdir.Cache)
if err := dirs[0].CreateParentDir("dummy"); err != nil {
fmt.Fprintf(os.Stderr, "Warning: could not create cache dir: %v\n", err)
}
dirs = stdConf.QueryFolders(configdir.Global)
if err := dirs[0].CreateParentDir("dummy"); err != nil {
fmt.Fprintf(os.Stderr, "Warning: could not create config dir: %v\n", err)
} else {
if err = os.MkdirAll(filepath.Join(dirs[0].Path, "profiles"), 0755); err != nil {
fmt.Fprintf(os.Stderr, "Warning: could not create profiles dir: %v\n", err)
}
}
err := profiles.ReadDefaultConfig(dirs[0].Path)
if err != nil {
fmt.Fprintf(os.Stderr, fmt.Sprintf("%s\n", err.Error()))
}
// Used to determine if we should run tshark instead e.g. stdout is not a tty
var tsopts cli.Tshark
// Add help flag. This is no use for the user and we don't want to display
// help for this dummy set of flags designed to check for pass-thru to tshark - but
// if help is on, then we'll detect it, parse the flags as termshark, then
// display the intended help.
tsFlags := flags.NewParser(&tsopts, flags.IgnoreUnknown|flags.HelpFlag)
_, err = tsFlags.ParseArgs(os.Args)
passthru := true
if err != nil {
// If it's because of --help, then skip the tty check, and display termshark's help. This
// ensures we don't display a useless help, and further that you can pipe termshark's help
// into PAGER without invoking tshark.
if ferr, ok := err.(*flags.Error); ok && ferr.Type == flags.ErrHelp {
passthru = false
} else {
return 1
}
}
// On Windows, termshark itself is used to tail the pcap generated by dumpcap, and the output
// is fed into tshark -T psml ...
if tsopts.TailFileValue() != "" {
err = termshark.TailFile(tsopts.TailFileValue())
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v", err)
return 1
} else {
return 0
}
}
// From here, the current profile is referenced. So load it up prior to first use. If the user
// provides a non-existent profile name, it should be an error, just as for Wireshark.
if tsopts.Profile != "" {
if err = profiles.Use(tsopts.Profile); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
return 1
}
}
// If this variable is set, it's set by termshark internally, and termshark is guaranteed to
// construct a valid command-line invocation. So it doesn't matter if I do this after the CLI
// parsing logic because there's no risk of an error causing a short-circuit and this command
// not being run. The reason to do it after the CLI parsing logic is so that I have the correct
// config profile loaded, needed for the tshark command.
if os.Getenv("TERMSHARK_CAPTURE_MODE") == "1" {
err = system.DumpcapExt(termshark.DumpcapBin(), termshark.TSharkBin(), os.Args[1:]...)
if err != nil {
return 1
} else {
return 0
}
}
// Run after accessing the config so I can use the configured tshark binary, if there is one. I need that
// binary in the case that termshark is run where stdout is not a tty, in which case I exec tshark - but
// it makes sense to use the one in termshark.toml
if passthru &&
(cli.FlagIsTrue(tsopts.PassThru) ||
(tsopts.PassThru == "auto" && !isatty.IsTerminal(os.Stdout.Fd())) ||
tsopts.PrintIfaces) {
tsharkBin, kverr := termshark.TSharkPath()
if kverr != nil {
fmt.Fprintf(os.Stderr, kverr.KeyVals["msg"].(string))
return 1
}
args := []string{}
for _, arg := range os.Args[1:] {
if !termshark.StringInSlice(arg, cli.TermsharkOnly) && !termshark.StringIsArgPrefixOf(arg, cli.TermsharkOnly) {
args = append(args, arg)
}
}
args = append([]string{tsharkBin}, args...)
if runtime.GOOS != "windows" {
err = syscall.Exec(tsharkBin, args, os.Environ())
if err != nil {
fmt.Fprintf(os.Stderr, "Error execing tshark binary: %v\n", err)
return 1
}
} else {
// No exec() on windows
c := exec.Command(args[0], args[1:]...)
c.Stdout = os.Stdout
c.Stderr = os.Stderr
err = c.Start()
if err != nil {
fmt.Fprintf(os.Stderr, "Error starting tshark: %v\n", err)
return 1
}
err = c.Wait()
if err != nil {
fmt.Fprintf(os.Stderr, "Error waiting for tshark: %v\n", err)
return 1
}
return 0
}
}
// Termshark's own command line arguments. Used if we don't pass through to tshark.
var opts cli.Termshark
// Parse the args now as intended for termshark
tmFlags := flags.NewParser(&opts, flags.PassDoubleDash)
var filterArgs []string
filterArgs, err = tmFlags.Parse()
if err != nil {
fmt.Fprintf(os.Stderr, "Command-line error: %v\n\n", err)
ui.WriteHelp(tmFlags, os.Stderr)
return 1
}
if opts.Help {
ui.WriteHelp(tmFlags, os.Stdout)
return 0
}
if len(opts.Version) > 0 {
res := 0
ui.WriteVersion(tmFlags, os.Stdout)
if len(opts.Version) > 1 {
if tsharkBin, kverr := termshark.TSharkPath(); kverr != nil {
fmt.Fprintf(os.Stderr, kverr.KeyVals["msg"].(string))
res = 1
} else {
if ver, err := termshark.TSharkVersion(tsharkBin); err != nil {
fmt.Fprintf(os.Stderr, "Could not determine version of tshark from binary %s\n", tsharkBin)
res = 1
} else {
ui.WriteTsharkVersion(tmFlags, tsharkBin, ver, os.Stdout)
}
}
}
return res
}
usetty := opts.TtyValue()
if usetty != "" {
if ttyf, err := os.Open(usetty); err != nil {
fmt.Fprintf(os.Stderr, "Could not open terminal %s: %v.\n", usetty, err)
return 1
} else {
if !isatty.IsTerminal(ttyf.Fd()) {
fmt.Fprintf(os.Stderr, "%s is not a terminal.\n", usetty)
ttyf.Close()
return 1
}
ttyf.Close()
}
} else {
// Always override - in case the user has GOWID_TTY in a shell script (if they're
// using the gcla fork of tcell for another application).
usetty = "/dev/tty"
}
// Allow the user to override the shell's TERM variable this way. Perhaps the user runs
// under screen/tmux, and the TERM variable doesn't reflect the fact their preferred
// terminal emumlator supports 256 colors.
termVar := profiles.ConfString("main.term", "")
if termVar != "" {
fmt.Fprintf(os.Stderr, "Configuration file overrides TERM setting, using TERM=%s\n", termVar)
os.Setenv("TERM", termVar)
}
var psrcs []pcap.IPacketSource
defer func() {
for _, psrc := range psrcs {
if psrc != nil {
if remover, ok := psrc.(pcap.ISourceRemover); ok {
remover.Remove()
}
}
}
}()
pcapf := string(opts.Pcap)
// If no interface specified, and no pcap specified via -r, then we assume the first
// argument is a pcap file e.g. termshark foo.pcap
if pcapf == "" && len(opts.Ifaces) == 0 {
pcapf = string(opts.Args.FilterOrPcap)
// `termshark` => `termshark -i 1` (livecapture on default interface if no args)
if pcapf == "" {
if termshark.IsTerminal(os.Stdin.Fd()) {
pfile, err := system.PickFile()
switch err {
case nil:
// We're on termux/android, and we were given a file. Note that termux
// makes a copy, so we ought to clean that up when termshark terminates.
psrcs = append(psrcs, pcap.TemporaryFileSource{pcap.FileSource{Filename: pfile}})
case system.NoPicker:
// We're not on termux/android. Treat like this:
// $ termshark
// # use network interface 1 - maps to
// # termshark -i 1
psrcs = append(psrcs, pcap.InterfaceSource{Iface: "1"})
default:
// We're on termux/android, but got an unexpected error.
//if err != termshark.NoPicker {
// !NoPicker means we could be on android/termux, but something else went wrong
if err = system.PickFileError(err.Error()); err != nil {
// Termux's toast ran into an error...! Maybe not installed?
fmt.Fprintf(os.Stderr, err.Error())
}
return 1
}
} else {
// $ cat foo.pcap | termshark
// # use stdin - maps to
// $ cat foo.pcap | termshark -r -
psrcs = append(psrcs, pcap.FileSource{Filename: "-"})
}
}
} else {
// Add it to filter args. Figure out later if they're capture or display.
filterArgs = append(filterArgs, opts.Args.FilterOrPcap)
}
if pcapf != "" && len(opts.Ifaces) > 0 {
fmt.Fprintf(os.Stderr, "Please supply either a pcap or one or more live captures.\n")
return 1
}
// Invariant: pcap != "" XOR len(opts.Ifaces) > 0
if len(psrcs) == 0 {
switch {
case pcapf != "":
psrcs = append(psrcs, pcap.FileSource{Filename: pcapf})
case len(opts.Ifaces) > 0:
for _, iface := range opts.Ifaces {
psrcs = append(psrcs, pcap.InterfaceSource{Iface: iface})
}
}
}
// Here we check for
// (a) sources named '-' - these need rewritten to /dev/fd/N and stdin needs to be moved
// (b) fifo sources - these are switched from -r to -i because that's what tshark needs
haveStdin := false
for pi, psrc := range psrcs {
switch {
case psrc.Name() == "-":
if haveStdin {
fmt.Fprintf(os.Stderr, "Requested live capture %v (\"stdin\") cannot be supplied more than once.\n", psrc.Name())
return 1
}
if termshark.IsTerminal(os.Stdin.Fd()) {
fmt.Fprintf(os.Stderr, "Requested live capture is %v (\"stdin\") but stdin is a tty.\n", psrc.Name())
fmt.Fprintf(os.Stderr, "Perhaps you intended to pipe packet input to termshark?\n")
return 1
}
if runtime.GOOS != "windows" {
psrcs[pi] = pcap.PipeSource{Descriptor: "/dev/fd/0", Fd: int(os.Stdin.Fd())}
haveStdin = true
} else {
fmt.Fprintf(os.Stderr, "Sorry, termshark does not yet support piped input on Windows.\n")
return 1
}
default:
stat, err := os.Stat(psrc.Name())
if err != nil {
if psrc.IsFile() || psrc.IsFifo() {
// Means this was supplied with -r - since any file sources means there's (a) 1 and (b)
// no other sources. So it must stat. Note if we started with -i fifo, this check
// isn't done... but it still ought to exist.
fmt.Fprintf(os.Stderr, "Error reading file %s: %v.\n", psrc.Name(), err)
return 1
}
continue
}
if stat.Mode()&os.ModeNamedPipe != 0 {
// If termshark was invoked with -r myfifo, switch to -i myfifo, which tshark uses. This
// also puts termshark in "interface" mode where it assumes the source is unbounded
// (e.g. a different spinner)
psrcs[pi] = pcap.FifoSource{Filename: psrc.Name()}
} else {
if pcapffile, err := os.Open(psrc.Name()); err != nil {
// Do this up front before the UI starts to catch simple errors quickly - like
// the file not being readable. It's possible that tshark would be able to read
// it and the termshark user not, but unlikely.
fmt.Fprintf(os.Stderr, "Error reading file %s: %v.\n", psrc.Name(), err)
return 1
} else {
pcapffile.Close()
}
}
}
}
// Means files
fileSrcs := pcap.FileSystemSources(psrcs)
if len(fileSrcs) == 1 {
if len(psrcs) > 1 {
fmt.Fprintf(os.Stderr, "You can't specify both a pcap and a live capture.\n")
return 1
}
} else if len(fileSrcs) > 1 {
fmt.Fprintf(os.Stderr, "You can't specify more than one pcap.\n")
return 1
}
// Invariant: len(psrcs) > 0
// Invariant: len(fileSrcs) == 1 => len(psrcs) == 1
// go-flags returns [""] when no extra args are provided, so I can't just
// test the length of this slice
termshark.ReverseStringSlice(filterArgs)
argsFilter := strings.Join(filterArgs, " ")
// Work out capture filter afterwards because we need to determine first
// whether any potential first argument is intended as a pcap file instead of
// a capture filter.
captureFilter := opts.CaptureFilter
// Meaning there are only live captures
if len(fileSrcs) == 0 && argsFilter != "" {
if opts.CaptureFilter != "" {
fmt.Fprintf(os.Stderr, "Two capture filters provided - '%s' and '%s' - please supply one only.\n", opts.CaptureFilter, argsFilter)
return 1
}
captureFilter = argsFilter
}
// -w something
if opts.WriteTo != "" {
if len(fileSrcs) > 0 {
fmt.Fprintf(os.Stderr, "The -w flag is incompatible with regular capture sources %v\n", fileSrcs)
return 1
}
if opts.WriteTo == "-" {
fmt.Fprintf(os.Stderr, "Cannot set -w to stdout. Target file must be regular or a symlink.\n")
return 1
}
// If the file does not exist, then proceed. If it does exist, check it is something "normal".
if _, err = os.Stat(string(opts.WriteTo)); err == nil || !os.IsNotExist(err) {
if !system.FileRegularOrLink(string(opts.WriteTo)) {
fmt.Fprintf(os.Stderr, "Cannot set -w to %s. Target file must be regular or a symlink.\n", opts.WriteTo)
return 1
}
}
}
displayFilter := opts.DisplayFilter
// Validate supplied filters e.g. no capture filter when reading from file
if len(fileSrcs) > 0 {
if captureFilter != "" {
fmt.Fprintf(os.Stderr, "Cannot use a capture filter when reading from a pcap file - '%s' and '%s'.\n", captureFilter, pcapf)
return 1
}
if argsFilter != "" {
if opts.DisplayFilter != "" {
fmt.Fprintf(os.Stderr, "Two display filters provided - '%s' and '%s' - please supply one only.\n", opts.DisplayFilter, argsFilter)
return 1
}
displayFilter = argsFilter
}
}
// Here we now have an accurate view of all psrcs - either file, fifo, pipe or interface
// Helpful to use logging when enumerating interfaces below, so do it first
if !opts.LogTty {
logfile := termshark.CacheFile("termshark.log")
logfd, err := os.OpenFile(logfile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0666)
if err != nil {
fmt.Fprintf(os.Stderr, "Could not create log file %s: %v\n", logfile, err)
return 1
}
// Don't close it - just let the descriptor be closed at exit. logrus is used
// in many places, some outside of this main function, and closing results in
// an error often on freebsd.
//defer logfd.Close()
log.SetOutput(logfd)
}
debug := false
if (opts.Debug.Set && opts.Debug.Val == true) || (!opts.Debug.Set && profiles.ConfBool("main.debug", false)) {
debug = true
}
if debug {
for _, addr := range termshark.LocalIPs() {
log.Infof("Starting debug web server at http://%s:6060/debug/pprof/", addr)
}
go func() {
log.Println(http.ListenAndServe("0.0.0.0:6060", nil))
}()
}
for _, dir := range []string{termshark.CacheDir(), termshark.DefaultPcapDir(), termshark.PcapDir()} {
if _, err = os.Stat(dir); os.IsNotExist(err) {
err = os.Mkdir(dir, 0777)
if err != nil {
fmt.Fprintf(os.Stderr, "Unexpected error making dir %s: %v", dir, err)
return 1
}
}
}
// Write this pcap out here because the color validation code later depends on empty.pcap
emptyPcap := termshark.CacheFile("empty.pcap")
if _, err := os.Stat(emptyPcap); os.IsNotExist(err) {
err = termshark.WriteEmptyPcap(emptyPcap)
if err != nil {
fmt.Fprintf(os.Stderr, "Could not create dummy pcap %s: %v", emptyPcap, err)
return 1
}
}
tsharkBin, kverr := termshark.TSharkPath()
if kverr != nil {
fmt.Fprintf(os.Stderr, kverr.KeyVals["msg"].(string))
return 1
}
// Here, tsharkBin is a fully-qualified tshark binary that exists on the fs (absent race
// conditions...)
valids := profiles.ConfStrings("main.validated-tsharks")
if !termshark.StringInSlice(tsharkBin, valids) {
tver, err := termshark.TSharkVersion(tsharkBin)
if err != nil {
fmt.Fprintf(os.Stderr, "Could not determine tshark version: %v\n", err)
return 1
}
// This is the earliest version I could determine gives reliable results in termshark.
// tshark compiled against tag v1.10.1 doesn't populate the hex view.
mver, _ := semver.Make("1.10.2")
if tver.LT(mver) {
fmt.Fprintf(os.Stderr, "termshark will not operate correctly with a tshark older than %v (found %v)\n", mver, tver)
return 1
}
valids = append(valids, tsharkBin)
profiles.SetConf("main.validated-tsharks", valids)
}
// If the last tshark we used isn't the same as the current one, then remove the cached fields
// data structure so it can be regenerated.
if tsharkBin != profiles.ConfString("main.last-used-tshark", "") {
termshark.DeleteCachedFields()
}
// Write out the last-used tshark path. We do this to make the above fields cache be consistent
// with the tshark binary we're using.
profiles.SetConf("main.last-used-tshark", tsharkBin)
// Determine if the current binary supports color. Tshark will fail with an error if it's too old
// and you supply the --color flag. Assume true, and check if our current binary is not in the
// validate list.
ui.PacketColorsSupported = true
colorTsharks := profiles.ConfStrings("main.color-tsharks")
if !termshark.StringInSlice(tsharkBin, colorTsharks) {
ui.PacketColorsSupported, err = termshark.TSharkSupportsColor(tsharkBin)
if err != nil {
ui.PacketColorsSupported = false
} else {
colorTsharks = append(colorTsharks, tsharkBin)
profiles.SetConf("main.color-tsharks", colorTsharks)
}
}
// If any of opts.Ifaces is provided as a number, it's meant as the index of the interfaces as
// per the order returned by the OS. useIface will always be the name of the interface.
var systemInterfaces map[int][]string
// See if the interface argument is an integer
for pi, psrc := range psrcs {
checkInterfaceName := false
ifaceIdx := -1
if psrc.IsInterface() {
if i, err := strconv.Atoi(psrc.Name()); err == nil {
ifaceIdx = i
}
// If it's a fifo, then always treat is as a fifo and not a reference to something in tshark -D
if ifaceIdx != -1 {
// if the argument is an integer, then confirm it in the output of tshark -D
checkInterfaceName = true
} else if runtime.GOOS == "windows" {
// If we're on windows, then all interfaces - indices and names -
// will be in tshark -D, so confirm it there
checkInterfaceName = true
}
}
if checkInterfaceName {
if systemInterfaces == nil {
systemInterfaces, err = termshark.Interfaces()
if err != nil {
fmt.Fprintf(os.Stderr, "Could not enumerate network interfaces: %v\n", err)
return 1
}
}
gotit := false
var canonicalName string
iLoop:
for n, i := range systemInterfaces { // (7, ["NDIS_...", "Local Area..."])
if n == ifaceIdx {
gotit = true
canonicalName = i[0]
break
} else {
for _, iname := range i {
if iname == psrc.Name() {
gotit = true
canonicalName = i[0]
break iLoop
}
}
}
}
if gotit {
// Guaranteed that psrc.IsInterface() is true
// Use the canonical name e.g. "NDIS_...". Then the temporary filename will
// have a more meaningful name.
psrcs[pi] = pcap.InterfaceSource{Iface: canonicalName}
} else {
fmt.Fprintf(os.Stderr, "Could not find network interface %s\n", psrc.Name())
return 1
}
}
}
watcher, err := termshark.NewConfigWatcher()
if err != nil {
fmt.Fprintf(os.Stderr, "Problem constructing config file watcher: %v", err)
return 1
}
defer watcher.Close()
//======================================================================
// If != "", then the name of the file to which packets are saved when read from an
// interface source. We can't just use the loader because the user might clear then load
// a recent pcap on top of the originally loaded packets.
ifacePcapFilename := ""
defer func() {
// if useIface != "" then we run dumpcap with the -i option - which
// means the packet source is either an interface, a pipe, or a
// fifo. In all cases, we save the packets to a file so that if a
// filter is applied, we can restart - and so that we preserve the
// capture at the end of running termshark.
if len(pcap.FileSystemSources(psrcs)) == 0 && startedSuccessfully && !ui.WriteToSelected && !ui.WriteToDeleted {
fmt.Fprintf(os.Stderr, "Packets read from %s have been saved in %s\n", pcap.SourcesString(psrcs), ifacePcapFilename)
}
}()
//======================================================================
ifaceExitCode := 0
stderr := &bytes.Buffer{}
var ifaceErr error
// This is deferred until after the app is Closed - otherwise messages written to stdout/stderr are
// swallowed by tcell.
defer func() {
if ifaceExitCode != 0 {
fmt.Fprintf(os.Stderr, "Cannot capture on device %s", pcap.SourcesString(psrcs))
if ifaceErr != nil {
fmt.Fprintf(os.Stderr, ": %v", ifaceErr)
}
fmt.Fprintf(os.Stderr, " (exit code %d)\n", ifaceExitCode)
if stderr.Len() != 0 {
// The default capture bin is termshark itself, with a special environment
// variable set that causes it to try dumpcap, then tshark, in that order (for
// efficiency of capture, but falling back to tshark for extcap interfaces).
// But telling the user the capture process is "termshark" is misleading.
cbin := termshark.CaptureBin()
if cbin == "termshark" {
cbin = "the capture process"
}
fmt.Fprintf(os.Stderr, "Standard error stream from %s:\n", cbin)
fmt.Fprintf(os.Stderr, "------\n%s\n------\n", stderr.String())
}
if runtime.GOOS == "linux" && os.Geteuid() != 0 {
fmt.Fprintf(os.Stderr, "You might need: sudo setcap cap_net_raw,cap_net_admin+eip %s\n", termshark.PrivilegedBin())
fmt.Fprintf(os.Stderr, "Or try running with sudo or as root.\n")
}
fmt.Fprintf(os.Stderr, "See https://termshark.io/no-root for more info.\n")
}
}()
// Initialize application state for dark mode and auto-scroll
ui.DarkMode = profiles.ConfBool("main.dark-mode", true)
ui.AutoScroll = profiles.ConfBool("main.auto-scroll", true)
ui.PacketColors = profiles.ConfBool("main.packet-colors", true)
// Set them up here so they have access to any command-line flags that
// need to be passed to the tshark commands used
pdmlArgs := profiles.ConfStringSlice("main.pdml-args", []string{})
psmlArgs := profiles.ConfStringSlice("main.psml-args", []string{})
if opts.TimestampFormat != "" {
psmlArgs = append(psmlArgs, "-t", opts.TimestampFormat)
}
tsharkArgs := profiles.ConfStringSlice("main.tshark-args", []string{})
if ui.PacketColors && !ui.PacketColorsSupported {
log.Warnf("Packet coloring is enabled, but %s does not support --color", tsharkBin)
ui.PacketColors = false
}
cacheSize := profiles.ConfInt("main.pcap-cache-size", 64)
bundleSize := profiles.ConfInt("main.pcap-bundle-size", 1000)
if bundleSize <= 0 {
maxBundleSize := 100000
log.Infof("Config specifies pcap-bundle-size as %d - setting to max (%d)", bundleSize, maxBundleSize)
bundleSize = maxBundleSize
}
var ifaceTmpFile string
var waitingForPackets bool
// no file sources - so interface or fifo
if len(pcap.FileSystemSources(psrcs)) == 0 {
if opts.WriteTo != "" {
ifaceTmpFile = string(opts.WriteTo)
ui.WriteToSelected = true
} else {
srcNames := make([]string, 0, len(psrcs))
for _, psrc := range psrcs {
srcNames = append(srcNames, psrc.Name())
}
ifaceTmpFile = pcap.TempPcapFile(srcNames...)
}
waitingForPackets = true
} else {
// Start UI right away, reading from a file
close(ui.StartUIChan)
}
// Need to figure out possible changes to COLORTERM before creating the
// tcell screen. Note that even though apprunner.Start() below will create
// a new screen, it will use a terminfo that it constructed the first time
// we call NewApp(), because tcell stores these in a global map. So if the
// first terminfo is created in an environment with COLORTERM=truecolor,
// the terminfo Go struct is extended with codes that emit truecolor-compatible
// ansi codes for colors. Then if I later create a new screen without COLORTERM,
// tcell will still use the extended terminfo struct and emit truecolor-codes
// anyway.
//
// If you are using base16-shell, the lowest colors 0-21 in the 256 color space
// will be remapped to whatever colors the terminal base16 theme sets up. If you
// are using a termshark theme that expresses colors in RGB style (#7799AA), and
// termshark is running in a 256-color terminal, then termshark will find the closest
// match for the RGB color in the 256 color-space. But termshark assumes that colors
// 0-21 are set up normally, and not remapped. If the closest match is one of those
// colors, then the theme won't look as expected. A workaround is to tell
// gowid not to use colors 0-21 when finding the closest match.
if profiles.ConfKeyExists("main.ignore-base16-colors") {
gowid.IgnoreBase16 = profiles.ConfBool("main.ignore-base16-colors", false)
} else {
// Try to auto-detect whether or not base16-shell is installed and in-use
gowid.IgnoreBase16 = (os.Getenv("BASE16_SHELL") != "")
}
if gowid.IgnoreBase16 {
log.Infof("Will not consider colors 0-21 from the terminal 256-color-space when interpolating theme colors")
// If main.respect-colorterm=true then termshark will leave COLORTERM set and use
// 24-bit color if possible. The problem with this, in the presence of base16, is that
// some terminal-emulators - e.g. gnome-terminal - still seems to map RGB ANSI codes
// colors that are set at values 0-21 in the 256-color space. I'm not sure if this is
// just an implementation snafu, or if something else is going on... In any case,
// termshark will fall back to 256-colors if base16 is detected because I can
// programmatically avoid choosing colors 0-21 for anything termshark needs.
if os.Getenv("COLORTERM") != "" && !profiles.ConfBool("main.respect-colorterm", false) {
log.Infof("Pessimistically disabling 24-bit color to avoid conflicts with base16")
os.Unsetenv("COLORTERM")
}
}
// the app variable is created here so I can bind it in the defer below
var app *gowid.App
// Do this before ui.Build. If ui.Build fails (e.g. bad TERM), then the filter will be left
// running, so we need the defer to be in effect here and not after the processing of ui.Build's
// error
defer func() {
if ui.FilterWidget != nil {
ui.FilterWidget.Close()
}
if ui.SearchWidget != nil {
ui.SearchWidget.Close(app)
}
if ui.CurrentWormholeWidget != nil {
ui.CurrentWormholeWidget.Close()
}
if ui.CurrentColsWidget != nil {
ui.CurrentColsWidget.Close()
}
}()
if app, err = ui.Build(usetty); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
// Tcell returns ExitError now because if its internal terminfo DB does not have
// a matching entry, it tries to build one with infocmp.
if _, ok := termshark.RootCause(err).(*exec.ExitError); ok {
fmt.Fprintf(os.Stderr, "Termshark could not recognize your terminal. Try changing $TERM.\n")
}
return 1
}
appRunner := app.Runner()
pcap.PcapCmds = pcap.MakeCommands(opts.DecodeAs, tsharkArgs, pdmlArgs, psmlArgs, ui.PacketColors)
pcap.PcapOpts = pcap.Options{
CacheSize: cacheSize,
PacketsPerLoad: bundleSize,
}
// This is a global. The type supports swapping out the real loader by embedding it via
// pointer, but I assume this only happens in the main goroutine.
ui.Loader = &pcap.PacketLoader{ParentLoader: pcap.NewPcapLoader(pcap.PcapCmds, &pcap.Runner{app}, pcap.PcapOpts)}
// Populate the filter widget initially - runs asynchronously
go ui.FilterWidget.UpdateCompletions(app)
ui.Running = false
validator := filter.DisplayFilterValidator{
Invalid: &filter.ValidateCB{
App: app,
Fn: func(app gowid.IApp) {
if !ui.Running {
fmt.Fprintf(os.Stderr, "Invalid filter: %s\n", displayFilter)
ui.RequestQuit()
} else {
app.Run(gowid.RunFunction(func(app gowid.IApp) {
ui.OpenError(fmt.Sprintf("Invalid filter: %s", displayFilter), app)
}))
}
},
},
}
// Do this before the load starts, so that the PSML process has a guaranteed safe
// PSML column format to use when it begins. The call to RequestLoadPcapWithCheck,
// for example, will access the setting for preferred PSML columns.
//
// Init a global variable with the list of all valid tshark columns and
// their formats. This might start a tshark process if the data isn't
// cached. If so, print a message to console - "initializing". I'm not doing
// anything smarter or async - it's not worth it, this should take a fraction
// of a second.
err = shark.InitValidColumns()
// If this message is needed, we want it to appear after the init message for the packet
// columns - after InitValidColumns
if waitingForPackets {
fmt.Fprintf(os.Stderr, fmt.Sprintf("(The termshark UI will start when packets are detected on %s...)\n",
strings.Join(pcap.SourcesNames(psrcs), " or ")))
}
// Refresh
fileSrcs = pcap.FileSystemSources(psrcs)
if len(fileSrcs) > 0 {
psrc := fileSrcs[0]
absfile, err := filepath.Abs(psrc.Name())
if err != nil {
fmt.Fprintf(os.Stderr, "Could not determine working directory: %v\n", err)
return 1
}
doit := func(app gowid.IApp) {
app.Run(gowid.RunFunction(func(app gowid.IApp) {
ui.FilterWidget.SetValue(displayFilter, app)
}))
ui.RequestLoadPcap(absfile, displayFilter, ui.NoGlobalJump, app)
}
validator.Valid = &filter.ValidateCB{Fn: doit, App: app}
validator.EmptyCB = &filter.ValidateCB{Fn: doit, App: app}
validator.Validate(displayFilter)
} else {
// Verifies whether or not we will be able to read from the interface (hopefully)
ifaceExitCode = 0
for _, psrc := range psrcs {
if psrc.IsInterface() {
if ifaceExitCode, ifaceErr = termshark.RunForStderr(
termshark.CaptureBin(),
[]string{"-i", psrc.Name(), "-a", "duration:1"},
append(os.Environ(), "TERMSHARK_CAPTURE_MODE=1"),
stderr,
); ifaceExitCode != 0 {
return 1
}
} else {
// We only test one - the assumption is that if dumpcap can read from eth0, it can also read from eth1, ... And
// this lets termshark start up more quickly.
break
}
}
doLoad := func(app gowid.IApp) {
ifacePcapFilename = ifaceTmpFile
ui.RequestLoadInterfaces(psrcs, captureFilter, displayFilter, ifaceTmpFile, app)
}
ifValid := func(app gowid.IApp) {
app.Run(gowid.RunFunction(func(app gowid.IApp) {
ui.FilterWidget.SetValue(displayFilter, app)
}))
doLoad(app)
}
validator.Valid = &filter.ValidateCB{Fn: ifValid, App: app}
validator.EmptyCB = &filter.ValidateCB{Fn: doLoad, App: app}
validator.Validate(displayFilter)
}
quitIssuedToApp := false
wasLoadingPdmlLastTime := ui.Loader.PdmlLoader.IsLoading()
wasLoadingAnythingLastTime := ui.Loader.LoadingAnything()
// Keep track of this across runs of the main loop so we don't go backwards (because
// that looks wrong to the user)
var prevProgPercentage float64
progTicker := time.NewTicker(time.Duration(200) * time.Millisecond)
ctrlzLineDisc := tty.TerminalSignals{}
// This is used to stop iface load and any stream reassembly. Make sure to
// avoid any stream reassembly errors, since this is a controlled shutdown
// but the tshark processes reading data for stream reassembly may still
// complain about interruptions
stopLoaders := func() {
if ui.StreamLoader != nil {
ui.StreamLoader.SuppressErrors = true