ttr.git


ttr

~ tiny traceroute in golang
git clone https://github.com/djnnvx/ttr.git
log | files | refs | archive | README | LICENSE

commit aab83c5acd9475cd277c3952346da362bdf6e920
Author: djnn <[email protected]>
Date: Fri, 12 Dec 2025 12:46:09 +0900

~ genesis

Diffstat:
ALICENSE | 9+++++++++
AMakefile | 28++++++++++++++++++++++++++++
AREADME.txt | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ago.mod | 9+++++++++
Ago.sum | 4++++
Amain.go | 207+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atracert | 0

7 files changed, 335 insertions(+), 0 deletions(-)

diff --git a/LICENSE b/LICENSE
@@ -0,0 +1,9 @@
+MIT License
+
+Copyright (c) 2025 djnn
+
+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
@@ -0,0 +1,28 @@
+TARGET 		= ttr
+SRC 		= main.go
+
+
+.PHONY: all
+all: compile
+
+.PHONY: help
+help:
+	@echo "\033[34mttr targets:\033[0m"
+	@perl -nle'print $& if m{^[a-zA-Z_-\d]+:.*?## .*$$}' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-22s\033[0m %s\n", $$1, $$2}'
+
+.PHONY: compile
+compile: ## compile the project
+	@go build -o $(TARGET) $(SRC)
+
+.PHONY: clean
+clean: ## cleans up the project
+	rm -f $(TARGET)
+
+.PHONY: tidy
+tidy: ## runs tidy and formatting
+	@go mod tidy
+	@gofmt -s -w .
+
+.PHONY: release-build
+release-build: ## makes a release build locally on the current commit
+	@goreleaser release --skip=publish --snapshot
diff --git a/README.txt b/README.txt
@@ -0,0 +1,78 @@
+
+ ███████████ ███████████ ███████████
+▒█▒▒▒███▒▒▒█▒█▒▒▒███▒▒▒█▒▒███▒▒▒▒▒███
+▒   ▒███  ▒ ▒   ▒███  ▒  ▒███    ▒███
+    ▒███        ▒███     ▒██████████
+    ▒███        ▒███     ▒███▒▒▒▒▒███
+    ▒███        ▒███     ▒███    ▒███
+    █████       █████    █████   █████
+   ▒▒▒▒▒       ▒▒▒▒▒    ▒▒▒▒▒   ▒▒▒▒▒
+
+
+
+ ====== version 0.1.0 ======== djnn.sh
+
+
+  ~ source code: https://evil.djnn.sh/ttr/log.html
+
+
+1. Simple usage
+
+  To build, simply type "make" and let the compiler install the correct toolchain,
+  dependencies and build the program for you. It is statically compiled, you may
+  add it to a container if you wish but I had no need for it.
+
+
+  Then, here is the expected output:
+
+  ./ttr djnn.sh
+
+  traceroute to djnn.sh (172.67.154.8), 30 hops max
+  1  192.168.REDACTED 2.432 ms  4.263 ms  2.700 ms
+  2  REDACTED.ne.jp (113.33.REDACTED) 5.260 ms  4.054 ms  3.849 ms
+  3  210.79.REDACTED 4.202 ms  4.357 ms  3.544 ms
+  4  * * *
+  5  163.139.REDACTED 3.342 ms  3.219 ms  2.634 ms
+  6  163.139.REDACTED 3.640 ms  5.679 ms  3.632 ms
+  7  * * 163.139.REDACTED 7.895 ms
+  8  103.22.REDACTED 42.821 ms  6.990 ms  8.304 ms
+  9  172.67.REDACTED 5.022 ms  4.132 ms  3.660 ms
+
+
+  NOTE - using without sudo:
+
+  After building, you can simply run the following command, if you're on Linux:
+
+
+    sudo setcap cap_net_raw+ep ./tracert
+    ./ttr djnn.sh
+
+
+  This will give your program the necessary permissions so that it is able to
+  filter packets without your kernel blocking you.
+
+
+2. How does it work ?
+
+  There are several resources online that explain how traceroute works, but I'll
+  document it here too. In the IP packet, there is a field called TTL (Time-to-live),
+  every time the packet passes through a router, this field is decreased by one.
+  It's a feature to improve the reliability of the protocol.
+
+  If the TTL reaches 0, we optionally receive back a ICMP message, "Time Exceeded".
+  This is very much an optional feature in the spec, so you might not see it all
+  the time. But the way this program works is by intentionally setting low TTL
+  values to make packets "die"vat specific hops so we can hear back from the
+  routers at those locations.
+
+
+3. Project management
+
+  If you find bugs or issues, you can contact me through the method described
+  here: https://djnn.sh/pgp
+
+  The source code is accessible for free, but Pull Requests are not accepted.
+  No guaranteed is given on whether the program works or not, no promises is
+  made on feature support. The behaviour can completely change from one version
+  to the next, and I take no responsibility as the main developer if it affects
+  critical operations for you.
diff --git a/go.mod b/go.mod
@@ -0,0 +1,9 @@
+module github.com/djnnvx/ttr
+
+go 1.24.0
+
+toolchain go1.24.11
+
+require golang.org/x/net v0.48.0
+
+require golang.org/x/sys v0.39.0 // indirect
diff --git a/go.sum b/go.sum
@@ -0,0 +1,4 @@
+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=
diff --git a/main.go b/main.go
@@ -0,0 +1,207 @@
+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
+)
+
+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
+}
+
+func cleanIP(addr string) string {
+	if host, _, err := net.SplitHostPort(addr); err == nil {
+		return host
+	}
+	return addr
+}
diff --git a/tracert b/tracert
Binary files differ.