diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
@@ -0,0 +1,30 @@
+name: Publish release on Github
+
+on:
+ push:
+ tags:
+ - "v*"
+
+permissions:
+ contents: write
+
+jobs:
+ goreleaser:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - name: Set up Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: 1.25
+ - name: Run GoReleaser
+ uses: goreleaser/goreleaser-action@v6
+ with:
+ distribution: goreleaser
+ version: "~> v2"
+ args: release --clean
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/cli/parser.go b/cli/parser.go
@@ -0,0 +1,52 @@
+package cli
+
+import (
+ "github.com/djnnvx/ttr/net"
+ "github.com/spf13/cobra"
+)
+
+const ASCII_ART = `
+
+ ███████████ ███████████ ███████████
+▒█▒▒▒███▒▒▒█▒█▒▒▒███▒▒▒█▒▒███▒▒▒▒▒███
+▒ ▒███ ▒ ▒ ▒███ ▒ ▒███ ▒███
+ ▒███ ▒███ ▒██████████
+ ▒███ ▒███ ▒███▒▒▒▒▒███
+ ▒███ ▒███ ▒███ ▒███
+ █████ █████ █████ █████
+ ▒▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒
+
+
+
+ ====== version 0.1.0 ======== djnn.sh
+
+`
+
+func GetParser(opts *TtrOptions) *cobra.Command {
+
+ var cmd = &cobra.Command{
+ Use: "ttr",
+ Short: "tiny traceroute utility",
+ Long: ASCII_ART,
+ Args: cobra.MinimumNArgs(1),
+ Version: "0.1.0",
+ Run: func(cmd *cobra.Command, args []string) {
+
+ println(ASCII_ART)
+
+ ipType := "ip4"
+ if opts.ForceIPv6 {
+ ipType = "ip6"
+ }
+
+ target := args[0]
+ net.Run(target, ipType)
+
+ },
+ }
+
+ defaults := GetDefaultOptions()
+
+ cmd.Flags().BoolVarP(&opts.ForceIPv6, "v6", "", defaults.ForceIPv6, "Force IPv6 usage")
+ return cmd
+}
diff --git a/cli/types.go b/cli/types.go
@@ -0,0 +1,16 @@
+package cli
+
+type TtrOptions struct {
+
+ // Enable this to force IPv6
+ ForceIPv6 bool
+}
+
+func GetDefaultOptions() TtrOptions {
+
+ opts := TtrOptions{
+ ForceIPv6: false,
+ }
+
+ return opts
+}
diff --git a/go.mod b/go.mod
@@ -4,6 +4,13 @@ go 1.24.0
toolchain go1.24.11
-require golang.org/x/net v0.48.0
+require (
+ github.com/spf13/cobra v1.10.2
+ golang.org/x/net v0.48.0
+)
-require golang.org/x/sys v0.39.0 // indirect
+require (
+ github.com/inconshreveable/mousetrap v1.1.0 // indirect
+ github.com/spf13/pflag v1.0.10 // indirect
+ golang.org/x/sys v0.39.0 // indirect
+)
diff --git a/go.sum b/go.sum
@@ -1,4 +1,15 @@
+github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
+github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
+github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
+github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/main.go b/main.go
@@ -1,207 +1,11 @@
package main
-import (
- "encoding/binary"
- "fmt"
- "net"
- "os"
- "strings"
- "time"
-
- "golang.org/x/net/icmp"
- "golang.org/x/net/ipv4"
-)
-
-const (
- /* Configuration */
- defaultMaxHops = 30
- defaultTimeout = 1 * time.Second
- probesPerHop = 3
- echoPayload = "tiny-traceroute-djnn-sh"
-
- /* Network Protocols & Packet sizing */
- protocolICMP = 1
- readBufferSize = 1500
- minIPv4HeaderLen = 20
- icmpHeaderLen = 8
- ipv4WordSize = 4 /* IHL is measured in 32-bit (4-byte) words */
- ihlMask = 0x0f
- processIDMask = 0xffff
-)
+import "github.com/djnnvx/ttr/cli"
func main() {
- if len(os.Args) != 2 {
- fmt.Printf("Usage: %s <host>\n", os.Args[0])
- os.Exit(1)
- }
-
- destStr := os.Args[1]
- destIP, err := net.ResolveIPAddr("ip4", destStr)
- if err != nil {
- fmt.Printf("Error resolving host: %v\n", err)
- os.Exit(1)
- }
-
- fmt.Printf("traceroute to %s (%s), %d hops max\n", destStr, destIP.String(), defaultMaxHops)
-
- c, pc, err := setupSocket()
- if err != nil {
- fmt.Printf("Socket error: %v\n", err)
- return
- }
- defer c.Close()
-
- traceID := os.Getpid() & processIDMask
-
- for ttl := 1; ttl <= defaultMaxHops; ttl++ {
- fmt.Printf("%2d ", ttl)
-
- var prevAddr string
- destinationReached := false
-
- for probe := 0; probe < probesPerHop; probe++ {
- seq := (ttl * 100) + probe
-
- peerAddr, rtt, err := executeProbe(c, pc, destIP, traceID, seq, ttl)
- if err != nil {
- fmt.Printf("* ")
- } else {
- printProbeResult(peerAddr, prevAddr, rtt)
- prevAddr = peerAddr
-
- if peerAddr == destIP.String() {
- destinationReached = true
- }
- }
- }
-
- fmt.Println()
-
- if destinationReached {
- break
- }
- }
-}
-
-func setupSocket() (*icmp.PacketConn, *ipv4.PacketConn, error) {
- c, err := icmp.ListenPacket("ip4:icmp", "0.0.0.0")
- if err != nil {
- return nil, nil, fmt.Errorf("listen packet: %w (ensure sudo/admin)", err)
- }
- return c, c.IPv4PacketConn(), nil
-}
-
-func executeProbe(c *icmp.PacketConn, pc *ipv4.PacketConn, dest *net.IPAddr, id, seq, ttl int) (string, time.Duration, error) {
- if err := pc.SetTTL(ttl); err != nil {
- return "", 0, err
- }
-
- start := time.Now()
-
- if err := sendEchoRequest(c, dest, id, seq); err != nil {
- return "", 0, err
- }
- peer, err := awaitResponse(pc, id, seq)
- if err != nil {
- return "", 0, err
- }
-
- return cleanIP(peer.String()), time.Since(start), nil
-}
-
-func sendEchoRequest(c *icmp.PacketConn, dest net.Addr, id, seq int) error {
- msg := icmp.Message{
- Type: ipv4.ICMPTypeEcho, Code: 0,
- Body: &icmp.Echo{
- ID: id, Seq: seq,
- Data: []byte(echoPayload),
- },
- }
- b, err := msg.Marshal(nil)
- if err != nil {
- return err
- }
- _, err = c.WriteTo(b, dest)
- return err
-}
-
-func awaitResponse(pc *ipv4.PacketConn, expectedID, expectedSeq int) (net.Addr, error) {
- pc.SetReadDeadline(time.Now().Add(defaultTimeout))
- b := make([]byte, readBufferSize)
-
- for {
- n, _, peer, err := pc.ReadFrom(b)
- if err != nil {
- return nil, err
- }
-
- rm, err := icmp.ParseMessage(protocolICMP, b[:n])
- if err != nil {
- continue
- }
-
- switch rm.Type {
- case ipv4.ICMPTypeEchoReply:
- if echo, ok := rm.Body.(*icmp.Echo); ok {
- if echo.ID == expectedID && echo.Seq == expectedSeq {
- return peer, nil
- }
- }
- case ipv4.ICMPTypeTimeExceeded:
- if te, ok := rm.Body.(*icmp.TimeExceeded); ok {
- if matchOriginalPacket(te.Data, expectedID, expectedSeq) {
- return peer, nil
- }
- }
- }
- }
-}
-
-func matchOriginalPacket(data []byte, expectedID, expectedSeq int) bool {
- if len(data) < minIPv4HeaderLen {
- return false
- }
-
- /* Calculate Internet Header Length (IHL) */
- ihl := int(data[0]&ihlMask) * ipv4WordSize
-
- /* We need the IP header + 8 bytes of the inner ICMP packet */
- if len(data) < ihl+icmpHeaderLen {
- return false
- }
-
- innerICMP := data[ihl:]
-
- /* Inner ICMP Structure: Type(1) + Code(1) + Checksum(2) + ID(2) + Seq(2) */
- origID := binary.BigEndian.Uint16(innerICMP[4:6])
- origSeq := binary.BigEndian.Uint16(innerICMP[6:8])
-
- return int(origID) == expectedID && int(origSeq) == expectedSeq
-}
-
-func printProbeResult(peerIP, prevAddr string, rtt time.Duration) {
- if peerIP != prevAddr {
- fmt.Printf("%s ", resolveHostname(peerIP))
- }
- fmt.Printf("%.3f ms ", float64(rtt.Microseconds())/1000.0)
-}
-
-func resolveHostname(ip string) string {
- names, _ := net.LookupAddr(ip)
- if len(names) > 0 {
- hostname := names[0]
- if strings.HasSuffix(hostname, ".") {
- hostname = hostname[:len(hostname)-1]
- }
- return fmt.Sprintf("%s (%s)", hostname, ip)
- }
- return ip
-}
+ opts := cli.GetDefaultOptions()
+ parser := cli.GetParser(&opts)
-func cleanIP(addr string) string {
- if host, _, err := net.SplitHostPort(addr); err == nil {
- return host
- }
- return addr
+ parser.ExecuteC()
}
diff --git a/net/run.go b/net/run.go
@@ -0,0 +1,208 @@
+package net
+
+import (
+ "encoding/binary"
+ "fmt"
+ "net"
+ "os"
+ "strings"
+ "time"
+
+ "golang.org/x/net/icmp"
+ "golang.org/x/net/ipv4"
+)
+
+const (
+ /* Configuration */
+ defaultMaxHops = 30
+ defaultTimeout = 1 * time.Second
+ probesPerHop = 3
+ echoPayload = "tiny-traceroute-djnn-sh"
+
+ /* Network Protocols & Packet sizing */
+ protocolICMP = 1
+ readBufferSize = 1500
+ minIPv4HeaderLen = 20
+ icmpHeaderLen = 8
+ ipv4WordSize = 4 /* IHL is measured in 32-bit (4-byte) words */
+ ihlMask = 0x0f
+ processIDMask = 0xffff
+)
+
+func Run(target string, ipType string) {
+ destIP, err := net.ResolveIPAddr("ip", target)
+ if err != nil {
+ fmt.Printf("Error resolving host: %v\n", err)
+ os.Exit(1)
+ }
+
+ fmt.Printf("traceroute to %s (%s), %d hops max\n", target, destIP.String(), defaultMaxHops)
+
+ c, pc, err := setupSocket(ipType)
+ if err != nil {
+ fmt.Printf("Socket error: %v\n", err)
+ return
+ }
+ defer c.Close()
+
+ traceID := os.Getpid() & processIDMask
+
+ for ttl := 1; ttl <= defaultMaxHops; ttl++ {
+ fmt.Printf("%2d ", ttl)
+
+ var prevAddr string
+ destinationReached := false
+
+ for probe := 0; probe < probesPerHop; probe++ {
+ seq := (ttl * 100) + probe
+
+ peerAddr, rtt, err := executeProbe(c, pc, destIP, traceID, seq, ttl)
+ if err != nil {
+ fmt.Printf("* ")
+ } else {
+ printProbeResult(peerAddr, prevAddr, rtt)
+ prevAddr = peerAddr
+
+ if peerAddr == destIP.String() {
+ destinationReached = true
+ }
+ }
+ }
+
+ fmt.Println()
+
+ if destinationReached {
+ break
+ }
+ }
+}
+
+func setupSocket(ipVersion string) (*icmp.PacketConn, *ipv4.PacketConn, error) {
+ protocol := "ip4:icmp"
+ network := "0.0.0.0"
+ if ipVersion == "ip6" {
+ protocol = "ip6:ipv6-icmp"
+ network = "::"
+ }
+
+ c, err := icmp.ListenPacket(protocol, network)
+ if err != nil {
+ return nil, nil, fmt.Errorf("listen packet: %w (ensure sudo/admin)", err)
+ }
+ return c, c.IPv4PacketConn(), nil
+}
+
+func executeProbe(c *icmp.PacketConn, pc *ipv4.PacketConn, dest *net.IPAddr, id, seq, ttl int) (string, time.Duration, error) {
+ if err := pc.SetTTL(ttl); err != nil {
+ return "", 0, err
+ }
+
+ start := time.Now()
+
+ if err := sendEchoRequest(c, dest, id, seq); err != nil {
+ return "", 0, err
+ }
+
+ peer, err := awaitResponse(pc, id, seq)
+ if err != nil {
+ return "", 0, err
+ }
+
+ return cleanIP(peer.String()), time.Since(start), nil
+}
+
+func sendEchoRequest(c *icmp.PacketConn, dest net.Addr, id, seq int) error {
+ msg := icmp.Message{
+ Type: ipv4.ICMPTypeEcho, Code: 0,
+ Body: &icmp.Echo{
+ ID: id, Seq: seq,
+ Data: []byte(echoPayload),
+ },
+ }
+ b, err := msg.Marshal(nil)
+ if err != nil {
+ return err
+ }
+ _, err = c.WriteTo(b, dest)
+ return err
+}
+
+func awaitResponse(pc *ipv4.PacketConn, expectedID, expectedSeq int) (net.Addr, error) {
+ pc.SetReadDeadline(time.Now().Add(defaultTimeout))
+ b := make([]byte, readBufferSize)
+
+ for {
+ n, _, peer, err := pc.ReadFrom(b)
+ if err != nil {
+ return nil, err
+ }
+
+ rm, err := icmp.ParseMessage(protocolICMP, b[:n])
+ if err != nil {
+ continue
+ }
+
+ switch rm.Type {
+ case ipv4.ICMPTypeEchoReply:
+ if echo, ok := rm.Body.(*icmp.Echo); ok {
+ if echo.ID == expectedID && echo.Seq == expectedSeq {
+ return peer, nil
+ }
+ }
+ case ipv4.ICMPTypeTimeExceeded:
+ if te, ok := rm.Body.(*icmp.TimeExceeded); ok {
+ if matchOriginalPacket(te.Data, expectedID, expectedSeq) {
+ return peer, nil
+ }
+ }
+ }
+ }
+}
+
+func matchOriginalPacket(data []byte, expectedID, expectedSeq int) bool {
+ if len(data) < minIPv4HeaderLen {
+ return false
+ }
+
+ /* Calculate Internet Header Length (IHL) */
+ ihl := int(data[0]&ihlMask) * ipv4WordSize
+
+ /* We need the IP header + 8 bytes of the inner ICMP packet */
+ if len(data) < ihl+icmpHeaderLen {
+ return false
+ }
+
+ innerICMP := data[ihl:]
+
+ /* Inner ICMP Structure: Type(1) + Code(1) + Checksum(2) + ID(2) + Seq(2) */
+ origID := binary.BigEndian.Uint16(innerICMP[4:6])
+ origSeq := binary.BigEndian.Uint16(innerICMP[6:8])
+
+ return int(origID) == expectedID && int(origSeq) == expectedSeq
+}
+
+func printProbeResult(peerIP, prevAddr string, rtt time.Duration) {
+ if peerIP != prevAddr {
+ fmt.Printf("%s ", resolveHostname(peerIP))
+ }
+ fmt.Printf("%.3f ms ", float64(rtt.Microseconds())/1000.0)
+}
+
+func resolveHostname(ip string) string {
+ names, _ := net.LookupAddr(ip)
+ if len(names) > 0 {
+ hostname := names[0]
+ if strings.HasSuffix(hostname, ".") {
+ hostname = hostname[:len(hostname)-1]
+ }
+ return fmt.Sprintf("%s (%s)", hostname, ip)
+ }
+ return ip
+}
+
+func cleanIP(addr string) string {
+ if host, _, err := net.SplitHostPort(addr); err == nil {
+ return host
+ }
+ return addr
+}
| | | | | | |