From db373915bd81c7377df00518e0967f05da3a78ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torbj=C3=B6rn=20Einarson?= Date: Thu, 27 Jun 2024 10:21:35 +0200 Subject: [PATCH] feat: start with single tool pcap-replay --- .github/workflows/go.yml | 36 ++++++ .github/workflows/golangci-lint.yml | 30 +++++ .gitignore | 18 +++ .golangci.yml | 11 ++ .pre-commit-config.yaml | 18 +++ .wwhrd.yml | 11 ++ CHANGELOG.md | 20 +++ CONTRIBUTING.md | 62 ++++++++++ LICENSE | 8 ++ Makefile | 47 +++++++ README.md | 73 +++++++++++ cmd/pcap-replay/main.go | 124 +++++++++++++++++++ cmd/pcap-replay/pcap.go | 75 ++++++++++++ cmd/pcap-replay/sender.go | 183 ++++++++++++++++++++++++++++ internal/version.go | 22 ++++ readme-go-default.md | 19 +++ 16 files changed, 757 insertions(+) create mode 100644 .github/workflows/go.yml create mode 100644 .github/workflows/golangci-lint.yml create mode 100644 .gitignore create mode 100644 .golangci.yml create mode 100644 .pre-commit-config.yaml create mode 100644 .wwhrd.yml create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 cmd/pcap-replay/main.go create mode 100644 cmd/pcap-replay/pcap.go create mode 100644 cmd/pcap-replay/sender.go create mode 100644 internal/version.go create mode 100644 readme-go-default.md diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml new file mode 100644 index 0000000..59840d2 --- /dev/null +++ b/.github/workflows/go.yml @@ -0,0 +1,36 @@ +name: Go + +on: + push: + branches: [main] + pull_request: + branches: [main] + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +jobs: + build: + strategy: + matrix: + platform: [macos-latest, ubuntu-latest, windows-latest] + go-version: ["1.22"] + runs-on: ${{ matrix.platform }} + steps: + - name: Setup Go + uses: actions/setup-go@v3 + with: + go-version: ${{ matrix.go-version }} + + - name: Checkout + uses: actions/checkout@v3 + + - name: Download Go dependencies + run: go mod download + env: + GOPROXY: "https://proxy.golang.org" + + - name: Build + run: go build -v ./... + + - name: Test + run: go test -v ./... diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml new file mode 100644 index 0000000..444255d --- /dev/null +++ b/.github/workflows/golangci-lint.yml @@ -0,0 +1,30 @@ +name: golangci-lint +on: + push: + tags: + - v* + branches: + - main + pull_request: + workflow_dispatch: +jobs: + golangci: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v4 + with: + go-version: 1.21 + cache: false + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 + with: + version: v1.54.2 + args: --skip-files=xml + only-new-issues: true + - name: go-report-card + uses: creekorful/goreportcard-action@v1.0 + with: + only-new-issues: true + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7efe153 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the make coverage target +coverage.* + +# Dependency directories (remove the comment below to include it) +vendor/ + +# Configuration from VSCode +*.DS_Store \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..421ac3d --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,11 @@ +linters: + enable: + - lll + +linters-settings: + lll: + # max line length, lines longer will be reported. Default is 120. + # '\t' is counted as 1 character by default, and can be changed with the tab-width option + line-length: 120 + # tab width in spaces. Default to 1. + tab-width: 4 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..767f14d --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,18 @@ + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.3.0 + hooks: + - id: trailing-whitespace + - repo: https://github.com/dnephin/pre-commit-golang + rev: v0.5.0 + hooks: + - id: go-fmt + - id: golangci-lint + - id: go-unit-tests + - repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook + rev: v8.0.0 + hooks: + - id: commitlint + stages: [commit-msg] + additional_dependencies: ['@commitlint/config-conventional'] \ No newline at end of file diff --git a/.wwhrd.yml b/.wwhrd.yml new file mode 100644 index 0000000..bff6ba4 --- /dev/null +++ b/.wwhrd.yml @@ -0,0 +1,11 @@ +denylist: + +allowlist: + - Apache-2.0 + - MIT + - BSD-2-Clause + - BSD-3-Clause + - CC0-1.0 + +exceptions: + diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..6d922cf --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,20 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +- Nothing yet + +## [0.1.0] - 2024-06-27 + +### Added + +- New tool pcap-replay that resends all UDP packets carrying UDP/TS or UDP/RTP/TS streams +- initial version of the repo + +[Unreleased]: https://github.com/Eyevinn/mp2ts-tools/releases/tag/v0.1.0...HEAD +[0.1.0]: https://github.com/Eyevinn/mp2ts-tools/releases/tag/v0.1.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..1f1d56c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,62 @@ +# Submitting Issues + +We use GitHub issues to track public bugs. If you are submitting a bug, please provide +as much info as possible to make it easier to reproduce the issue and update unit tests. + +# Contributing Code + +This project uses [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/#summary). + +We follow the [GitHub Flow](https://guides.github.com/introduction/flow/index.html) so all contributions happen through pull requests. We actively welcome your pull requests: + +1. Fork the repo and create your branch from master. +2. If you've added code that should be tested, add tests. +3. If you've changed APIs, update the documentation. +4. Ensure the test suite passes. +5. Issue that pull request! + +When submitting code changes your submissions are understood to be under the same MIT License that covers the project. Feel free to contact Eyevinn Technology if that's a concern. + +# Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +- The use of sexualized language or imagery and unwelcome sexual attention or advances +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or electronic address, without explicit permission +- Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the Contributor Covenant, version 1.4, available at http://contributor-covenant.org/version/1/4 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..62ab335 --- /dev/null +++ b/LICENSE @@ -0,0 +1,8 @@ +The MIT License (MIT) +Copyright (c) 2023 Eyevinn Technology AB + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3f69f46 --- /dev/null +++ b/Makefile @@ -0,0 +1,47 @@ +.PHONY: all +all: lint test coverage build + +.PHONY: build +build: pcap-replay + +.PHONY: lint +lint: prepare + golangci-lint run + +.PHONY: prepare +prepare: + go mod vendor + go mod tidy + +pcap-replay: + go build -ldflags "-X github.com/Eyevinn/pcap-tools/internal.commitVersion=$$(git describe --tags HEAD) -X github.com/Eyevinn/pcap-tools/internal.commitDate=$$(git log -1 --format=%ct)" -o out/$@ ./cmd/$@ + +.PHONY: test +test: prepare + go test ./... + +.PHONY: coverage +coverage: + # Ignore (allow) packages without any tests + set -o pipefail + go test ./... -coverprofile coverage.out + set +o pipefail + go tool cover -html=coverage.out -o coverage.html + go tool cover -func coverage.out -o coverage.txt + tail -1 coverage.txt + + + +.PHONY: clean +clean: + rm -f out/* + rm -r examples-out/* + +.PHONY: install +install: all + cp out/* $(GOPATH)/bin/ + +.PHONY: update +update: + go get -t -u ./... + diff --git a/README.md b/README.md new file mode 100644 index 0000000..217a487 --- /dev/null +++ b/README.md @@ -0,0 +1,73 @@ +

+pcap-tools +

+ +
+Tools for PCAP files with TS streams +
+ +
+
+ +![Test](https://github.com/Eyevinn/mp2ts-tools/workflows/Go/badge.svg) +[![golangci-lint](https://github.com/Eyevinn/mp2ts-tools/actions/workflows/golangci-lint.yml/badge.svg)](https://github.com/Eyevinn/mp2ts-tools/actions/workflows/golangci-lint.yml) +[![GoDoc](https://godoc.org/github.com/Eyevinn/mp2ts-tools?status.svg)](http://godoc.org/github.com/Eyevinn/mp2ts-tools) +[![Go Report Card](https://goreportcard.com/badge/github.com/Eyevinn/mp2ts-tools)](https://goreportcard.com/report/github.com/Eyevinn/mp2ts-tools) +[![github release](https://img.shields.io/github/v/release/Eyevinn/pcap-tools?style=flat-square)](https://github.com/Eyevinn/pcap-tools/releases) +[![license](https://img.shields.io/github/license/eyevinn/pcap-tools.svg?style=flat-square)](LICENSE) + +[![PRs welcome](https://img.shields.io/badge/PRs-welcome-ff69b4.svg?style=flat-square)](https://github.com/eyevinn/pcap-tools/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) +[![made with heart by Eyevinn](https://img.shields.io/badge/made%20with%20%E2%99%A5%20by-Eyevinn-59cbe8.svg?style=flat-square)](https://github.com/eyevinn) +[![Slack](http://slack.streamingtech.se/badge.svg)](http://slack.streamingtech.se) + +
+ +Tools for investigating and reusing tcpdump/Wireshark captures of TS streams. + +The tools available this far are: + +* pcap-replay + +## Requirements + +This project uses Go version 1.22 or later. + +## Installation / Usage + + +Use the `Makefile` to get build artifacts into the out directory, +or use the standard go build steps: + +```sh +go mod tidy +cd cmd/pcap-replay +go run . +``` + +## Development + +Uses standard Go tool chain. + +## Contributing + +See [CONTRIBUTING](CONTRIBUTING.md) + +## License + +This project is licensed under the MIT License, see [LICENSE](LICENSE). + +# Support + +Join our [community on Slack](http://slack.streamingtech.se) where you can post any questions regarding any of our open source projects. Eyevinn's consulting business can also offer you: + +* Further development of this component +* Customization and integration of this component into your platform +* Support and maintenance agreement + +Contact [sales@eyevinn.se](mailto:sales@eyevinn.se) if you are interested. + +# About Eyevinn Technology + +[Eyevinn Technology](https://www.eyevinntechnology.se) is an independent consultant firm specialized in video and streaming. Independent in a way that we are not commercially tied to any platform or technology vendor. As our way to innovate and push the industry forward we develop proof-of-concepts and tools. The things we learn and the code we write we share with the industry in [blogs](https://dev.to/video) and by open sourcing the code we have written. + +Want to know more about Eyevinn and how it is to work here. Contact us at work@eyevinn.se! diff --git a/cmd/pcap-replay/main.go b/cmd/pcap-replay/main.go new file mode 100644 index 0000000..27a2a52 --- /dev/null +++ b/cmd/pcap-replay/main.go @@ -0,0 +1,124 @@ +package main + +import ( + "context" + "flag" + "fmt" + "os" + "strconv" + "strings" + + log "github.com/sirupsen/logrus" + + "github.com/Eyevinn/pcap-tools/internal" +) + +var usg = `Usage of %s: + +%s replays UDP streams from a Wireshark/tcpdump capture. + +The IP address is changed to the specified value and destination ports can +be mapped to new values. One can alternatively export the streams to files. +` + +type options struct { + pcap string + dstAddr string + portMap string + dstDir string + logLevel string + naiveLoop bool + version bool +} + +func parseOptions() (*options, error) { + var opts options + flag.StringVar(&opts.pcap, "pcap", "", "Input PCAP file") + flag.StringVar(&opts.dstAddr, "addr", "", "Destination IP address for replayed streams") + flag.StringVar(&opts.dstDir, "dir", "", "Destination directory for exported streams") + flag.StringVar(&opts.portMap, "portmap", "", "Port mapping (e.g. 1234:5678,2345:6789)") + flag.BoolVar(&opts.naiveLoop, "naiveloop", false, "Loop the PCAP file in a naive way without rewrite of packets") + flag.StringVar(&opts.logLevel, "loglevel", "info", "Log level (info, debug, warn)") + flag.BoolVar(&opts.version, "version", false, "Get version") + + flag.Usage = func() { + parts := strings.Split(os.Args[0], "/") + name := parts[len(parts)-1] + fmt.Fprintf(os.Stderr, usg, name, name) + fmt.Fprintf(os.Stderr, "\nRun as: %s options with options:\n\n", name) + flag.PrintDefaults() + } + + flag.Parse() + return &opts, nil +} + +func main() { + ctx := context.Background() + opts, err := parseOptions() + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %s\n\n", err) + flag.Usage() + os.Exit(1) + } + if opts.version { + fmt.Printf("ew-pcap-replay %s\n", internal.GetVersion()) + os.Exit(0) + } + if opts.pcap == "" || (opts.dstAddr == "" && opts.dstDir == "") { + fmt.Fprintf(os.Stderr, "pcap and either addr or dir must be specified") + flag.Usage() + os.Exit(1) + } + err = run(ctx, opts) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %s\n", err) + os.Exit(1) + } +} + +func run(ctx context.Context, opts *options) error { + hdlr := createUDPHandler(opts.dstAddr, opts.dstDir) + nrLoops := 1 + portMap, err := parsePortMap(opts.portMap) + if err != nil { + return err + } + for { + err := processPCAP(ctx, opts.pcap, hdlr, portMap) + if err != nil { + return err + } + if !opts.naiveLoop { + break + } + log.Infof("Loop %d done", nrLoops) + hdlr = createUDPHandler(opts.dstAddr, opts.dstDir) + nrLoops++ + } + return nil +} + +func parsePortMap(pMap string) (map[int]int, error) { + portMap := make(map[int]int) + if pMap == "" { + return portMap, nil + } + parts := strings.Split(pMap, ",") + for _, p := range parts { + pp := strings.Split(p, ":") + if len(pp) != 2 { + return nil, fmt.Errorf("invalid port mapping, not two parts: %s", p) + } + src, err := strconv.Atoi(pp[0]) + if err != nil { + return nil, fmt.Errorf("first port map entry not a number: %s", p) + } + dst, err := strconv.Atoi(pp[1]) + if err != nil { + return nil, fmt.Errorf("second port map entry not a number: %s", p) + } + portMap[src] = dst + } + return portMap, nil +} diff --git a/cmd/pcap-replay/pcap.go b/cmd/pcap-replay/pcap.go new file mode 100644 index 0000000..74bd9d3 --- /dev/null +++ b/cmd/pcap-replay/pcap.go @@ -0,0 +1,75 @@ +package main + +import ( + "context" + "fmt" + + "github.com/google/gopacket" + "github.com/google/gopacket/layers" + "github.com/google/gopacket/pcap" + log "github.com/sirupsen/logrus" +) + +func processPCAP(ctx context.Context, pcapFile string, udpHandler *udpHandler, portMap map[int]int) error { + if pcapFile == "" { + return fmt.Errorf("pcapFile is required") + } + handle, err := pcap.OpenOffline(pcapFile) + if err != nil { + return err + } + defer handle.Close() + + return processPcapHandle(ctx, handle, udpHandler, portMap) +} + +func processPcapHandle(ctx context.Context, handle *pcap.Handle, udpHandler *udpHandler, portMap map[int]int) error { + // Loop through packets from source + packetSource := gopacket.NewPacketSource(handle, handle.LinkType()) + packetChannel := packetSource.Packets() + first := true +Loop: + for { + select { + case <-ctx.Done(): + return nil + case packet := <-packetChannel: + if packet == nil { + return nil + } + udpLayer := packet.Layer(layers.LayerTypeUDP) + if udpLayer == nil { + continue Loop + } + timestamp := packet.Metadata().Timestamp + ipLayer := packet.Layer(layers.LayerTypeIPv4) + if ipLayer == nil { + continue + } + ip := ipLayer.(*layers.IPv4) + udp, _ := udpLayer.(*layers.UDP) + if udp == nil { + continue + } + dstPort := int(udp.DstPort) + if newPort, ok := portMap[dstPort]; ok { + dstPort = newPort + } + dst := fmt.Sprintf("%s:%d", ip.DstIP, dstPort) + udpPayload := udp.Payload + done, err := udpHandler.AddPacket(dst, udpPayload, timestamp) + if err != nil { + return fmt.Errorf("add UDP packet: %w", err) + } + if done { + break Loop + } + if first { + log.Infof("Start time of capture: %v\n", timestamp) + first = false + } + log.Debugf("From %s:%d to %s:%d length=%d\n", ip.SrcIP, udp.SrcPort, ip.DstIP, dstPort, len(udp.Payload)) + } + } + return nil +} diff --git a/cmd/pcap-replay/sender.go b/cmd/pcap-replay/sender.go new file mode 100644 index 0000000..859fb3a --- /dev/null +++ b/cmd/pcap-replay/sender.go @@ -0,0 +1,183 @@ +package main + +import ( + "bytes" + "fmt" + "net" + "os" + "path" + "strconv" + "strings" + "time" + + log "github.com/sirupsen/logrus" +) + +const ( + tsPacketSize = 188 +) + +type udpHandler struct { + dstAddr string + dstDir string + pktNr int + maxNrPackets int + maxNrSeconds int + outfiles map[string]*os.File + streams map[string]bool + conn *net.UDPConn + lastPayload []byte + startTimeStamp time.Time + startTime time.Time + firstSame int + nrRepeatedPkts int + started bool +} + +func createUDPHandler(dstAddr string, dstDir string) *udpHandler { + maxNrPackets := 1_000_000_000_000 + maxNrSeconds := 1_000_000 + uh := &udpHandler{ + dstAddr: dstAddr, + dstDir: dstDir, + maxNrPackets: maxNrPackets, + maxNrSeconds: maxNrSeconds, + streams: make(map[string]bool), + outfiles: make(map[string]*os.File), + lastPayload: make([]byte, 7*tsPacketSize), + started: false, + nrRepeatedPkts: 0, + } + return uh +} + +func (u *udpHandler) AddPacket(dst string, udpPayload []byte, timestamp time.Time) (done bool, err error) { + if !u.started { + u.started = true + u.startTimeStamp = timestamp.UTC() + u.startTime = time.Now() + } + _, ok := u.streams[dst] + if !ok { + u.streams[dst] = true + log.Infof("Found new UDP stream %s", dst) + if u.dstDir != "" { + dstFileName := strings.Replace(dst, ".", "_", -1) + dstFileName = strings.Replace(dstFileName, ":", "_", -1) + ".ts" + dstFilePath := path.Join(u.dstDir, dstFileName) + err := os.MkdirAll(u.dstDir, os.ModePerm) + if err != nil { + return true, err + } + fh, err := os.Create(dstFilePath) + if err != nil { + return false, err + } + u.outfiles[dst] = fh + } + } + if bytes.Equal(udpPayload, u.lastPayload) { + if u.firstSame == -1 { + u.firstSame = u.pktNr + } + u.nrRepeatedPkts++ + u.pktNr++ + return + } + u.firstSame = -1 + + ok = true + extraBytes := len(udpPayload) % 188 + switch extraBytes { + case 0: // One or more TS packets + // Do nothing + case 12: // RTP. Remove 12-byte header + udpPayload = udpPayload[12:] + default: + ok = false // only count, nothing else + if u.streams[dst] { + log.Infof("stream %q: udp payload size %d indicates not a TS stream", dst, len(udpPayload)) + u.streams[dst] = false + } + } + u.pktNr++ + if ok { + // Copy the full payload to lastPayload. First set the size to the same as udpPayload. + if len(udpPayload) > int(cap(u.lastPayload)) { + return false, fmt.Errorf("udp payload size %d is larger than capacity %d", + len(udpPayload), cap(u.lastPayload)) + } + u.lastPayload = u.lastPayload[:len(udpPayload)] + copy(u.lastPayload, udpPayload) + if u.dstDir != "" { + _, _ = u.outfiles[dst].Write(udpPayload) + } + u.pktNr++ + timeDiff := timestamp.Sub(u.startTimeStamp) + if u.pktNr%10000 == 0 { + log.Infof("Read and sent %d packets %.3fs (%d repeated)", u.pktNr, timeDiff.Seconds(), u.nrRepeatedPkts) + } + if u.maxNrPackets > 0 && u.pktNr >= u.maxNrPackets { + done = true + } + if u.maxNrSeconds > 0 { + secondsPassed := int(timestamp.Sub(u.startTimeStamp).Seconds()) + if secondsPassed >= u.maxNrSeconds { + done = true + } + } + if u.dstAddr != "" { + err := u.sendPacket(dst, udpPayload, timestamp) + if err != nil { + return done, err + } + } + } + return done, nil +} + +func (u *udpHandler) sendPacket(dst string, udpPayload []byte, timestamp time.Time) error { + now := time.Now() + timeStampDiff := timestamp.Sub(u.startTimeStamp) + timeDiff := now.Sub(u.startTime) + diff := timeStampDiff - timeDiff + if diff > 0 { + time.Sleep(diff) + } + if u.conn == nil { + conn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4zero, Port: 0}) + if err != nil { + return fmt.Errorf("error creating UDP connection: %w", err) + } + u.conn = conn + } + port, err := strconv.Atoi(strings.Split(dst, ":")[1]) + if err != nil { + return fmt.Errorf("error parsing port number: %w", err) + } + + dstIP, err := toIP(u.dstAddr) + if err != nil { + return err + } + n, err := u.conn.WriteTo(udpPayload, &net.UDPAddr{IP: dstIP, Port: port}) + if err != nil { + return fmt.Errorf("error sending UDP packet: %w", err) + } + if n != len(udpPayload) { + return fmt.Errorf("sent %d bytes, expected %d", n, len(udpPayload)) + } + return nil +} + +func toIP(s string) (net.IP, error) { + parts := strings.Split(s, ".") + if len(parts) != 4 { + return net.IP{}, fmt.Errorf("invalid IP address: %s", s) + } + toByte := func(s string) byte { + b, _ := strconv.Atoi(s) + return byte(b) + } + return net.IP{toByte(parts[0]), toByte(parts[1]), toByte(parts[2]), toByte(parts[3])}, nil +} diff --git a/internal/version.go b/internal/version.go new file mode 100644 index 0000000..2b08126 --- /dev/null +++ b/internal/version.go @@ -0,0 +1,22 @@ +package internal + +import ( + "fmt" + "strconv" + "time" +) + +var ( + commitVersion string = "v0.1" // May be updated using build flags + commitDate string = "1719478088" // commitDate in Epoch seconds (may be overridden using build flags) +) + +// GetVersion - get version and also commitHash and commitDate if inserted via Makefile +func GetVersion() string { + seconds, _ := strconv.Atoi(commitDate) + if commitDate != "" { + t := time.Unix(int64(seconds), 0) + return fmt.Sprintf("%s, date: %s", commitVersion, t.Format("2006-01-02")) + } + return commitVersion +} diff --git a/readme-go-default.md b/readme-go-default.md new file mode 100644 index 0000000..0aea225 --- /dev/null +++ b/readme-go-default.md @@ -0,0 +1,19 @@ +# go-default + +## Get started + +1. Run go mod init github/Eyevinn/name +2. Edit the installed files appropriately +3. Put any executables in directories with proper names under cmd/cmd1 etc + and update the Makefile + +## Included + +The defaults for all go projects include: + +- A gitignore file +- Github actions for running tests and golang-ci-lint +- A Makefile for running tests, coverage, and update repo +- A README skeleton (update badges to Go, see e.g. mp4ff) +- A CHANGELOG.md file that should be changed manually +- A config file for pre-commit (see https://pre-commit.com)