diff --git a/software/cmd/mr.duppl/.gitignore b/software/cmd/mr.duppl/.gitignore new file mode 100644 index 0000000..2de41d3 --- /dev/null +++ b/software/cmd/mr.duppl/.gitignore @@ -0,0 +1 @@ +mr.duppl \ No newline at end of file diff --git a/software/cmd/mr.duppl/commands/dump.go b/software/cmd/mr.duppl/commands/dump.go new file mode 100644 index 0000000..3baf596 --- /dev/null +++ b/software/cmd/mr.duppl/commands/dump.go @@ -0,0 +1,119 @@ +package commands + +import ( + "errors" + "fmt" + "io" + "os" + "runtime" + + "github.com/gopacket/gopacket" + "github.com/gopacket/gopacket/layers" + "github.com/gopacket/gopacket/pcapgo" + "github.com/spf13/cobra" + + "github.com/buglloc/mr.duppl/software/pkg/dupplcap" + "github.com/buglloc/mr.duppl/software/pkg/usbp" +) + +var dumpArgs struct { + Interface string + NoFolding bool + Output string +} + +var dumpCmd = &cobra.Command{ + Use: "dump", + SilenceUsage: true, + SilenceErrors: true, + Short: "Dump Mr.Duppl capture", + RunE: func(_ *cobra.Command, _ []string) error { + dev, err := dupplcap.NewDevice(dumpArgs.Interface) + if err != nil { + return fmt.Errorf("opening device: %w", err) + } + defer func() { _ = dev.Close() }() + + if err := dev.StartCapture(!dumpArgs.NoFolding); err != nil { + return fmt.Errorf("start capture: %w", err) + } + defer func() { _ = dev.StopCapture() }() + + if len(dumpArgs.Output) != 0 { + out, err := os.Create(dumpArgs.Output) + if err != nil { + return fmt.Errorf("creating output file: %w", err) + } + defer func() { _ = out.Close() }() + + return dumpCapture(dev, out) + } + + return printCapture(dev) + }, +} + +func dumpCapture(dev *dupplcap.Device, out io.Writer) error { + w, err := pcapgo.NewNgWriterInterface( + out, + pcapgo.NgInterface{ + Name: dev.Iface(), + OS: runtime.GOOS, + LinkType: layers.LinkType(dupplcap.LinkTypeUSBFullSpeed.Int()), + SnapLength: 0, //unlimited + // TimestampResolution: 9, + }, + pcapgo.NgWriterOptions{ + SectionInfo: pcapgo.NgSectionInfo{ + Hardware: runtime.GOARCH, + OS: runtime.GOOS, + Application: "Mr.Duppl", //spread the word + }, + }, + ) + if err != nil { + return fmt.Errorf("open pcapng writer: %w", err) + } + defer func() { _ = w.Flush() }() + + for { + packet, err := dev.Packet() + if err != nil { + if errors.Is(err, io.EOF) { + return nil + } + + return fmt.Errorf("read packet: %w", err) + } + + ci := gopacket.CaptureInfo{ + Length: len(packet), + CaptureLength: len(packet), + InterfaceIndex: 0, + } + err = w.WritePacket(ci, packet) + if err != nil { + return fmt.Errorf("write packet: %w", err) + } + + usbp.Print(packet, os.Stdout) + } +} + +func printCapture(dev *dupplcap.Device) error { + for { + packet, err := dev.Packet() + if err != nil { + return fmt.Errorf("read packet: %w", err) + } + + usbp.Print(packet, os.Stdout) + } +} + +func init() { + flags := dumpCmd.PersistentFlags() + flags.StringVarP(&dumpArgs.Interface, "iface", "i", "", "interface to use") + flags.StringVarP(&dumpArgs.Output, "out", "o", "", "write PcapNG file") + flags.BoolVar(&dumpArgs.NoFolding, "no-packet-folding", false, "disable packet folding") +} diff --git a/software/cmd/mr.duppl/commands/ls.go b/software/cmd/mr.duppl/commands/ls.go new file mode 100644 index 0000000..893e92b --- /dev/null +++ b/software/cmd/mr.duppl/commands/ls.go @@ -0,0 +1,51 @@ +package commands + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/buglloc/mr.duppl/software/pkg/dupplcap" +) + +var lsArgs struct { + Name bool + Path bool +} + +var lsCmd = &cobra.Command{ + Use: "ls", + SilenceUsage: true, + SilenceErrors: true, + Short: "List devices", + RunE: func(_ *cobra.Command, _ []string) error { + if !lsArgs.Name && !lsArgs.Path { + lsArgs.Name = true + lsArgs.Path = true + } + + ifaces, err := dupplcap.Ifaces() + if err != nil { + return fmt.Errorf("unable to get information about interfaces: %w", err) + } + + for _, iface := range ifaces { + switch { + case lsArgs.Name && lsArgs.Path: + fmt.Println(iface.Name, iface.Path) + case lsArgs.Name: + fmt.Println(iface.Name) + case lsArgs.Path: + fmt.Println(iface.Path) + } + } + + return nil + }, +} + +func init() { + flags := lsCmd.PersistentFlags() + flags.BoolVarP(&lsArgs.Name, "name", "n", false, "display device name") + flags.BoolVarP(&lsArgs.Path, "path", "p", false, "display device path") +} diff --git a/software/cmd/mr.duppl/commands/root.go b/software/cmd/mr.duppl/commands/root.go new file mode 100644 index 0000000..6fb9422 --- /dev/null +++ b/software/cmd/mr.duppl/commands/root.go @@ -0,0 +1,22 @@ +package commands + +import ( + "github.com/spf13/cobra" +) + +var rootCmd = &cobra.Command{ + Use: "mr.duppl", + SilenceUsage: true, + SilenceErrors: true, +} + +func Execute() error { + return rootCmd.Execute() +} + +func init() { + rootCmd.AddCommand( + lsCmd, + dumpCmd, + ) +} diff --git a/software/cmd/mr.duppl/main.go b/software/cmd/mr.duppl/main.go new file mode 100644 index 0000000..01852ac --- /dev/null +++ b/software/cmd/mr.duppl/main.go @@ -0,0 +1,15 @@ +package main + +import ( + "fmt" + "os" + + "github.com/buglloc/mr.duppl/software/cmd/mr.duppl/commands" +) + +func main() { + if err := commands.Execute(); err != nil { + _, _ = fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } +} diff --git a/software/go.mod b/software/go.mod index ce19691..729ba4c 100644 --- a/software/go.mod +++ b/software/go.mod @@ -5,16 +5,19 @@ go 1.23.1 require ( github.com/gopacket/gopacket v1.3.0 github.com/kor44/extcap v0.0.1 + github.com/spf13/cobra v1.8.1 github.com/stretchr/testify v1.8.4 go.bug.st/serial v1.6.2 ) require ( - github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect github.com/creack/goselect v0.1.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect github.com/urfave/cli/v2 v2.25.7 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect golang.org/x/net v0.28.0 // indirect diff --git a/software/go.sum b/software/go.sum index ab08bc8..4532496 100644 --- a/software/go.sum +++ b/software/go.sum @@ -1,17 +1,23 @@ -github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0= github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gopacket/gopacket v1.3.0 h1:MouZCc+ej0vnqzB0WeiaO/6+tGvb+KU7UczxoQ+X0Yc= github.com/gopacket/gopacket v1.3.0/go.mod h1:WnFrU1Xkf5lWKV38uKNR9+yYtppn+ZYzOyNqMeH4oNE= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/kor44/extcap v0.0.1 h1:fcNWKzd25nN9sD+mY9z/GWsTfNXNnuIVlJRKpSkeHpQ= github.com/kor44/extcap v0.0.1/go.mod h1:+Pl5vUaEZv3VWQ7EC9eksa+XsLGU0ZUVXGYEjAuC+9k= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= diff --git a/software/pkg/dupplcap/device.go b/software/pkg/dupplcap/device.go index f3c30a1..1599cdc 100644 --- a/software/pkg/dupplcap/device.go +++ b/software/pkg/dupplcap/device.go @@ -1,7 +1,10 @@ package dupplcap import ( + "errors" "fmt" + "io" + "strings" "go.bug.st/serial" ) @@ -12,20 +15,29 @@ const ( ) type Device struct { - port serial.Port + iface string + port serial.Port } -func NewDeviceByIface(iface string) (*Device, error) { - serialPort, err := serial.Open(iface, &serial.Mode{ - BaudRate: 115200, - }) +func NewDevice(nameOrIface string) (*Device, error) { + if len(nameOrIface) != 0 { + if strings.HasPrefix(nameOrIface, Name) { + return NewDeviceByName(nameOrIface) + } + + return NewDeviceByIface(nameOrIface) + } + + ifaces, err := Ifaces() if err != nil { - return nil, fmt.Errorf("open serial port %s: %w", iface, err) + return nil, fmt.Errorf("list ifaces: %w", err) } - return &Device{ - port: serialPort, - }, nil + if len(ifaces) == 0 { + return nil, errors.New("device was not found") + } + + return NewDeviceByIface(ifaces[0].Path) } func NewDeviceByName(name string) (*Device, error) { @@ -45,8 +57,36 @@ func NewDeviceByName(name string) (*Device, error) { return nil, fmt.Errorf("device with name %s not found", name) } +func NewDeviceByIface(iface string) (*Device, error) { + serialPort, err := serial.Open(iface, &serial.Mode{ + BaudRate: 115200, + }) + if err != nil { + return nil, fmt.Errorf("open serial port %s: %w", iface, err) + } + + return &Device{ + iface: iface, + port: serialPort, + }, nil +} + +func (r *Device) Iface() string { + return r.iface +} + func (r *Device) Packet() ([]byte, error) { - return readSlipPacket(r.port) + packet, err := readSlipPacket(r.port) + if err != nil { + var portErr *serial.PortError + if errors.As(err, &portErr) && portErr.Code() == serial.PortClosed { + return nil, io.EOF + } + + return nil, fmt.Errorf("read packet: %w", err) + } + + return packet, nil } func (r *Device) StartCapture(withPacketFolding bool) error { diff --git a/software/pkg/dupplcap/ifaces.go b/software/pkg/dupplcap/ifaces.go index f087599..1da9a91 100644 --- a/software/pkg/dupplcap/ifaces.go +++ b/software/pkg/dupplcap/ifaces.go @@ -8,8 +8,9 @@ import ( ) const ( - targetVID = "2E8A" - targetPID = "5052" + VID = "2E8A" + PID = "5052" + Name = "Mr.Duppl" ) type Iface struct { @@ -29,17 +30,17 @@ func Ifaces() ([]Iface, error) { continue } - if !strings.EqualFold(port.VID, targetVID) { + if !strings.EqualFold(port.VID, VID) { continue } - if !strings.EqualFold(port.PID, targetPID) { + if !strings.EqualFold(port.PID, PID) { continue } out = append(out, Iface{ Path: port.Name, - Name: fmt.Sprintf("Mr.Duppl:%s", port.SerialNumber), + Name: fmt.Sprintf("%s:%s", Name, port.SerialNumber), }) } diff --git a/software/pkg/usbp/printer.go b/software/pkg/usbp/printer.go new file mode 100644 index 0000000..2bb9673 --- /dev/null +++ b/software/pkg/usbp/printer.go @@ -0,0 +1,44 @@ +package usbp + +import ( + "fmt" + "io" +) + +func Print(in []byte, w io.Writer) { + n := len(in) + rowcount := 0 + stop := (n / 8) * 8 + k := 0 + for i := 0; i <= stop; i += 8 { + k++ + switch { + case i+8 < n: + rowcount = 8 + case k*8 < n: + rowcount = 0 + default: + rowcount = n % 8 + } + + for j := 0; j < rowcount; j++ { + _, _ = fmt.Fprintf(w, "%02x ", in[i+j]) + } + for j := rowcount; j < 8; j++ { + _, _ = fmt.Fprint(w, " ") + } + + _, _ = fmt.Fprintf(w, " '%s'\n", viewString(in[i:(i+rowcount)])) + } + +} + +func viewString(b []byte) string { + r := []rune(string(b)) + for i := range r { + if r[i] < 32 || r[i] > 126 { + r[i] = '.' + } + } + return string(r) +}