From da5880fade3e65ffb9bfe406b5cc5d5fb747fe39 Mon Sep 17 00:00:00 2001
From: Volker Schukai <volker.schukai@schukai.com>
Date: Sat, 15 May 2021 17:03:12 +0000
Subject: [PATCH] V6 Tracing

---
 .gitlab-ci.yml     |   1 +
 README.md          |  11 +++
 go.mod             |   1 +
 go.sum             |   3 +-
 outbound_test.go   |  14 +++-
 session.go         |  17 +++--
 traceroute.go      | 180 +++++++++++++++++++++++++++++++++++++--------
 traceroute_test.go |   9 ++-
 8 files changed, 197 insertions(+), 39 deletions(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 2b52474..18c1100 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -12,6 +12,7 @@ stages:
   - test
 
 before_script:
+  - go get -u golang.org/x/lint/golint  
 
 unit_tests:
   stage: test
diff --git a/README.md b/README.md
index f465812..1234f53 100644
--- a/README.md
+++ b/README.md
@@ -39,6 +39,17 @@ pkttyagent --process  $(ps -xa | grep "netbeans" )
 pkttyagent --process  $(ps -xa | grep "IntelliJ-IDEA-Ultimate/jbr/bin/java" )
 ```
 
+## Test
+
+`/etc/sudoers` has restricted PATH environment variable. 
+
+When you run `sudo make test`, `/usr/local/go/bin` must be entered in
+`secure_path`.
+
+```
+Defaults   secure_path="....:/usr/local/go/bin"
+```
+
 ## Installation
 
 The recommended way to install this package is
diff --git a/go.mod b/go.mod
index 94ecf48..98a91eb 100644
--- a/go.mod
+++ b/go.mod
@@ -6,4 +6,5 @@ require (
 	github.com/creasty/defaults v1.5.1
 	github.com/jackpal/gateway v1.0.7
 	golang.org/x/net v0.0.0-20210510120150-4163338589ed
+	golang.org/x/sys v0.0.0-20210510120138-977fb7262007 // indirect
 )
diff --git a/go.sum b/go.sum
index ea2dc19..3fa52b7 100644
--- a/go.sum
+++ b/go.sum
@@ -5,8 +5,9 @@ github.com/jackpal/gateway v1.0.7/go.mod h1:aRcO0UFKt+MgIZmRmvOmnejdDT4Y1DNiNOsS
 golang.org/x/net v0.0.0-20210510120150-4163338589ed h1:p9UgmWI9wKpfYmgaV/IZKGdXc5qEK45tDwwwDyjS26I=
 golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c=
 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE=
+golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
diff --git a/outbound_test.go b/outbound_test.go
index 64587a8..4013855 100644
--- a/outbound_test.go
+++ b/outbound_test.go
@@ -1,6 +1,7 @@
 package traceroute
 
 import (
+	"strings"
 	"testing"
 )
 
@@ -8,7 +9,11 @@ func TestGetOutboundIPV6(t *testing.T) {
 	ip, err := getOutboundIP(addressV6)
 
 	if err != nil {
-		t.Errorf("this call should not return an error " + err.Error())
+
+		if !strings.Contains(err.Error(), "no suitable interface was found") {
+			t.Errorf("this call should not return an error " + err.Error())
+		}
+		return
 	}
 	
 	if !ip.isV6() {
@@ -22,7 +27,12 @@ func TestGetOutboundIPV4(t *testing.T) {
 	ip, err := getOutboundIP(addressV4)
 
 	if err != nil {
-		t.Errorf("this call should not return an error " + err.Error())
+
+		if !strings.Contains(err.Error(), "no suitable interface was found") {
+			t.Errorf("this call should not return an error " + err.Error())
+		}
+		return
+
 	}
 
 	if !ip.isV4() {
diff --git a/session.go b/session.go
index b4a51f0..8acc7fd 100644
--- a/session.go
+++ b/session.go
@@ -4,6 +4,7 @@ import (
 	"github.com/creasty/defaults"
 	"golang.org/x/net/icmp"
 	"golang.org/x/net/ipv4"
+	"golang.org/x/net/ipv6"
 	"time"
 )
 
@@ -14,12 +15,15 @@ type Session struct {
 	
 	CallBack func(result Result)
 
-	Timeout time.Duration `default:"2"` // in seconds
+	Timeout time.Duration `default:"5"` // in seconds
 	MaxHops int   `default:"30"`
+	
+	Mode int
 
 	nextHop  int
 	isFinale bool
 
+	ipV6Sock   *ipv6.PacketConn
 	ipV4Sock   *ipv4.PacketConn
 	icmpEcho   icmp.Message
 	readBuffer []byte
@@ -40,18 +44,21 @@ func NewSession(destination string) (*Session, error) {
 	}
 	s.Destination = dest
 
-	var mode int
 	if dest.isV6() {
-		mode = addressV6
+		s.Mode = addressV6
 	} else {
-		mode = addressV4
+		s.Mode = addressV4
 	}
 
 	
 	s.Timeout = s.Timeout*time.Second
 	//* time.Second
 
-	src, err := getOutboundIP(mode)
+	src, err := getOutboundIP(s.Mode)
+	if err!=nil {
+		return nil, err
+	}
+	
 	s.Source = src
 
 	return &s, nil
diff --git a/traceroute.go b/traceroute.go
index c867c04..7d0fdbd 100644
--- a/traceroute.go
+++ b/traceroute.go
@@ -5,12 +5,20 @@ import (
 	"fmt"
 	"golang.org/x/net/icmp"
 	"golang.org/x/net/ipv4"
+	"golang.org/x/net/ipv6"
 	"math/rand"
 	"net"
-
 	"time"
 )
 
+// from internal/iana/const.go
+// Protocol Numbers, Updated: 2017-10-13
+const (
+	ProtocolIPv6ICMP = 58 // ICMP for IPv6
+	ProtocolICMP     = 1  // Internet Control Message
+)
+
+// Result holds the data
 type Result struct {
 	Hop     int
 	Station string
@@ -23,7 +31,7 @@ type Results struct {
 	Hops []Result
 }
 
-func (s *Session) doHop(i int) Result {
+func (s *Session) doV4Hop(i int) Result {
 	s.icmpEcho.Body.(*icmp.Echo).Seq = i
 
 	r := Result{
@@ -62,42 +70,123 @@ func (s *Session) doHop(i int) Result {
 		return r
 	}
 
-	readBytes, _, hopNode, err := s.ipV4Sock.ReadFrom(s.readBuffer)
+	for ; ; {
+		readBytes, _, hopNode, err := s.ipV4Sock.ReadFrom(s.readBuffer)
+
+		if hopNode != nil {
+			r.Station = hopNode.String()
+		}
+
+		if err != nil {
+			r.Err = err
+			return r
+		}
+
+		icmpAnswer, err := icmp.ParseMessage(ProtocolICMP, s.readBuffer[:readBytes])
+
+		if err != nil {
+			r.Err = err
+			return r
+		}
+
+		latency := time.Since(timeNow)
+		r.Latency = latency
+
+		if icmpAnswer.Type == ipv4.ICMPTypeTimeExceeded {
+			s.nextHop++
+			return r
+		}
+
+		if icmpAnswer.Type == ipv4.ICMPTypeEchoReply {
+			s.isFinale = true
+			return r
+		}
 
-	if hopNode != nil {
-		r.Station = hopNode.String()
 	}
 
-	if err != nil {
-		r.Err = err
-		return r
+}
+
+func (s *Session) doV6Hop(i int) Result {
+	s.icmpEcho.Body.(*icmp.Echo).Seq = i
+
+	r := Result{
+		Hop:     i,
+		Station: "*",
 	}
 
-	icmpAnswer, err := icmp.ParseMessage(1, s.readBuffer[:readBytes])
+	writeBuffer, err := s.icmpEcho.Marshal(nil)
 
 	if err != nil {
 		r.Err = err
 		return r
 	}
 
-	latency := time.Since(timeNow)
-	r.Latency = latency
+	if err := s.ipV6Sock.SetHopLimit(i); err != nil {
+		r.Err = fmt.Errorf("socket: %w", err)
+		return r
+	}
 
-	if icmpAnswer.Type == ipv4.ICMPTypeTimeExceeded {
-		s.nextHop++
+	timeNow := time.Now()
+
+	dst := s.Destination
+
+	a := net.IPAddr{
+		IP:   dst.ip,
+		Zone: "",
+	}
+
+	if _, err := s.ipV6Sock.WriteTo(writeBuffer, nil, &a); err != nil {
+		r.Err = err
 		return r
 	}
 
-	if icmpAnswer.Type == ipv4.ICMPTypeEchoReply {
-		s.isFinale = true
+	if err := s.ipV6Sock.SetReadDeadline(time.Now().Add(s.Timeout)); err != nil {
+		r.Err = err
 		return r
 	}
 
-	r.Err = fmt.Errorf("unknown icmp answer: %d", icmpAnswer.Type.Protocol())
+	for ; ; {
+
+		readBytes, _, hopNode, err := s.ipV6Sock.ReadFrom(s.readBuffer)
+
+		if hopNode != nil {
+			r.Station = hopNode.String()
+		}
+
+		if err != nil {
+			r.Err = err
+			return r
+		}
+
+		icmpAnswer, err := icmp.ParseMessage(ProtocolIPv6ICMP, s.readBuffer[:readBytes])
+
+		if err != nil {
+			r.Err = err
+			return r
+		}
+
+		latency := time.Since(timeNow)
+		r.Latency = latency
+
+		if icmpAnswer.Type == ipv6.ICMPTypeTimeExceeded {
+			s.nextHop++
+			return r
+		}
+
+		if icmpAnswer.Type == ipv6.ICMPTypeDestinationUnreachable {
+			s.nextHop++
+			return r
+		}
+
+		if icmpAnswer.Type == ipv6.ICMPTypeEchoReply {
+			s.isFinale = true
+			return r
+		}
+	}
 
-	return r
 }
 
+// TraceRouteV4 run V4 Traceroute
 func (s *Session) TraceRouteV4() (*Results, error) {
 
 	sock, err := net.ListenPacket("ip4:icmp", s.Source.ip.String())
@@ -107,7 +196,6 @@ func (s *Session) TraceRouteV4() (*Results, error) {
 	}
 
 	defer sock.Close()
-
 	s.ipV4Sock = ipv4.NewPacketConn(sock)
 	defer s.ipV4Sock.Close()
 
@@ -119,14 +207,20 @@ func (s *Session) TraceRouteV4() (*Results, error) {
 		Type: ipv4.ICMPTypeEcho, Code: 0, Body: &icmp.Echo{ID: rand.Int(), Data: []byte("")},
 	}
 
+	return s.doTrace(func(i int) Result {
+		return s.doV4Hop(i)
+	})
+}
+
+func (s *Session) doTrace(cb func(i int) Result) (*Results, error) {
 	s.readBuffer = make([]byte, 1500)
 
 	results := Results{}
 
 	for i := 1; i < s.MaxHops; i++ {
-		r:=s.doHop(i)
+		r := cb(i)
 		results.Hops = append(results.Hops, r)
-		if s.CallBack!=nil {
+		if s.CallBack != nil {
 			s.CallBack(r)
 		}
 
@@ -138,10 +232,35 @@ func (s *Session) TraceRouteV4() (*Results, error) {
 	return &results, nil
 }
 
-// currently not implemented
-//func (s *Session) traceRouteV6() error {
-//	return nil
-//}
+// TraceRouteV6 run V6 trace
+func (s *Session) TraceRouteV6() (*Results, error) {
+
+	if s.Source.ip==nil {
+		return nil, errors.New("missing source ip")
+	}
+	
+	sock, err := net.ListenPacket("ip6:ipv6-icmp", s.Source.ip.String())
+
+	if err != nil {
+		return nil, err
+	}
+
+	defer sock.Close()
+	s.ipV6Sock = ipv6.NewPacketConn(sock)
+	defer s.ipV6Sock.Close()
+
+	if err := s.ipV6Sock.SetControlMessage(ipv6.FlagHopLimit|ipv6.FlagDst|ipv6.FlagInterface|ipv6.FlagSrc, true); err != nil {
+		return nil, err
+	}
+
+	s.icmpEcho = icmp.Message{
+		Type: ipv6.ICMPTypeEchoRequest, Code: 0, Body: &icmp.Echo{ID: rand.Int(), Data: []byte("")},
+	}
+
+	return s.doTrace(func(i int) Result {
+		return s.doV6Hop(i)
+	})
+}
 
 // TraceRoute measures the steps to the target host 
 func (s *Session) TraceRoute() (*Results, error) {
@@ -154,12 +273,13 @@ func (s *Session) TraceRoute() (*Results, error) {
 		return results, nil
 	}
 
-	// currently not implemented
-	//if s.Destination.isV6() {
-	//	if err := s.traceRouteV6(); err != nil {
-	//		return nil, err
-	//	}
-	//}
+	if s.Destination.isV6() {
+		results, err := s.TraceRouteV6()
+		if err != nil {
+			return nil, err
+		}
+		return results, nil
+	}
 
 	return nil, errors.New("could not traceroute " + s.Destination.String())
 
diff --git a/traceroute_test.go b/traceroute_test.go
index 13931d4..c07287e 100644
--- a/traceroute_test.go
+++ b/traceroute_test.go
@@ -2,6 +2,7 @@ package traceroute
 
 import (
 	"fmt"
+	"strings"
 	"testing"
 )
 
@@ -12,6 +13,9 @@ func TestTraceroute(t *testing.T) {
 		shouldSessionError bool
 	}{
 		{
+			"ipv6.google.com",
+			false,
+		},		{
 			"www.schukai.com",
 			false,
 		},
@@ -23,7 +27,10 @@ func TestTraceroute(t *testing.T) {
 
 			session, err := NewSession(tc.destination)
 			if err != nil {
-				t.Errorf("%s shout not error %s", tc.destination, err.Error())
+				if !strings.Contains(err.Error(), "no suitable interface was found") {
+					t.Errorf("%s shout not error %s", tc.destination, err.Error())	
+				}
+				return
 			}
 
 			_,err = session.TraceRoute()
-- 
GitLab