commit e4af31660de887aa4949afda8872ed7717fd27d9 Author: 186526 Date: Thu May 5 00:48:40 2022 +0800 Init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..adf8f72 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# ---> Go +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work + diff --git a/GeoLite2-ASN.mmdb b/GeoLite2-ASN.mmdb new file mode 100644 index 0000000..5c233c3 Binary files /dev/null and b/GeoLite2-ASN.mmdb differ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..28ad722 --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2021 186526 + +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/go.mod b/go.mod new file mode 100644 index 0000000..7f54ef2 --- /dev/null +++ b/go.mod @@ -0,0 +1,17 @@ +module tracer + +go 1.18 + +require ( + github.com/aeden/traceroute v0.0.0-20210211061815-03f5f7cb7908 + github.com/oschwald/maxminddb-golang v1.9.0 +) + +require ( + github.com/vishvananda/netlink v1.1.0 // indirect + github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df // indirect + golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 // indirect + golang.org/x/sys v0.0.0-20220325203850-36772127a21f // indirect +) + +replace github.com/aeden/traceroute => ./lib diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e0d7533 --- /dev/null +++ b/go.sum @@ -0,0 +1,15 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/oschwald/maxminddb-golang v1.9.0 h1:tIk4nv6VT9OiPyrnDAfJS1s1xKDQMZOsGojab6EjC1Y= +github.com/oschwald/maxminddb-golang v1.9.0/go.mod h1:TK+s/Z2oZq0rSl4PSeAEoP0bgm82Cp5HyvYbt8K3zLY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJH8j0= +github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= +github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df h1:OviZH7qLw/7ZovXvuNyL3XQl8UFofeikI1NW1Gypu7k= +github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220325203850-36772127a21f h1:TrmogKRsSOxRMJbLYGrB4SBbW+LJcEllYBLME5Zk5pU= +golang.org/x/sys v0.0.0-20220325203850-36772127a21f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= diff --git a/lib/go.mod b/lib/go.mod new file mode 100644 index 0000000..9803e91 --- /dev/null +++ b/lib/go.mod @@ -0,0 +1,7 @@ +module traceroute + +go 1.18 + +require golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 + +require golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect diff --git a/lib/go.sum b/lib/go.sum new file mode 100644 index 0000000..42a678e --- /dev/null +++ b/lib/go.sum @@ -0,0 +1,4 @@ +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/lib/traceroute.go b/lib/traceroute.go new file mode 100644 index 0000000..584e12f --- /dev/null +++ b/lib/traceroute.go @@ -0,0 +1,288 @@ +// Package traceroute provides functions for executing a tracroute to a remote +// host. +package traceroute + +import ( + "errors" + "fmt" + "net" + "os" + "time" + + "github.com/vishvananda/netlink" + "golang.org/x/net/icmp" + "golang.org/x/net/ipv4" +) + +const DEFAULT_PORT = 33434 +const DEFAULT_MAX_HOPS = 30 +const DEFAULT_FIRST_HOP = 1 +const DEFAULT_TIMEOUT_MS = 1000 +const DEFAULT_RETRIES = 3 +const DEFAULT_PACKET_SIZE = 60 + +func socketAddr(ip net.IP) (IPAddr [4]byte, err error) { + routes, err := netlink.RouteGet(ip) + + if err != nil { + return + } + + copy(IPAddr[:], routes[0].Src.To4()) + err = errors.New("You do not appear to be connected to the Internet") + return +} + +// Given a host name convert it to a 4 byte IP address. +func destAddr(dest string) (destAddr [4]byte, IPAddr net.IPAddr, err error) { + addrs, err := net.LookupHost(dest) + if err != nil { + return + } + addr := addrs[0] + + ipAddr, err := net.ResolveIPAddr("ip", addr) + if err != nil { + return + } + copy(destAddr[:], ipAddr.IP.To4()) + IPAddr.IP = ipAddr.IP + return +} + +// TracrouteOptions type +type TracerouteOptions struct { + port int + maxHops int + firstHop int + timeoutMs int + retries int + packetSize int +} + +func (options *TracerouteOptions) Port() int { + if options.port == 0 { + options.port = DEFAULT_PORT + } + return options.port +} + +func (options *TracerouteOptions) SetPort(port int) { + options.port = port +} + +func (options *TracerouteOptions) MaxHops() int { + if options.maxHops == 0 { + options.maxHops = DEFAULT_MAX_HOPS + } + return options.maxHops +} + +func (options *TracerouteOptions) SetMaxHops(maxHops int) { + options.maxHops = maxHops +} + +func (options *TracerouteOptions) FirstHop() int { + if options.firstHop == 0 { + options.firstHop = DEFAULT_FIRST_HOP + } + return options.firstHop +} + +func (options *TracerouteOptions) SetFirstHop(firstHop int) { + options.firstHop = firstHop +} + +func (options *TracerouteOptions) TimeoutMs() int { + if options.timeoutMs == 0 { + options.timeoutMs = DEFAULT_TIMEOUT_MS + } + return options.timeoutMs +} + +func (options *TracerouteOptions) SetTimeoutMs(timeoutMs int) { + options.timeoutMs = timeoutMs +} + +func (options *TracerouteOptions) Retries() int { + if options.retries == 0 { + options.retries = DEFAULT_RETRIES + } + return options.retries +} + +func (options *TracerouteOptions) SetRetries(retries int) { + options.retries = retries +} + +func (options *TracerouteOptions) PacketSize() int { + if options.packetSize == 0 { + options.packetSize = DEFAULT_PACKET_SIZE + } + return options.packetSize +} + +func (options *TracerouteOptions) SetPacketSize(packetSize int) { + options.packetSize = packetSize +} + +// TracerouteHop type +type TracerouteHop struct { + Success bool + Address [4]byte + Host string + N int + ElapsedTime time.Duration + TTL int +} + +func (hop *TracerouteHop) AddressString() string { + return fmt.Sprintf("%v.%v.%v.%v", hop.Address[0], hop.Address[1], hop.Address[2], hop.Address[3]) +} + +func (hop *TracerouteHop) HostOrAddressString() string { + hostOrAddr := hop.AddressString() + if hop.Host != "" { + hostOrAddr = hop.Host + } + return hostOrAddr +} + +// TracerouteResult type +type TracerouteResult struct { + DestinationAddress [4]byte + Hops []TracerouteHop +} + +func notify(hop TracerouteHop, channels []chan TracerouteHop) { + for _, c := range channels { + c <- hop + } +} + +func closeNotify(channels []chan TracerouteHop) { + for _, c := range channels { + close(c) + } +} + +func Traceroute(dest string, options *TracerouteOptions, c ...chan TracerouteHop) (result TracerouteResult, err error) { + result.Hops = []TracerouteHop{} + destAddrBytes, destIPAddr, err := destAddr(dest) + result.DestinationAddress = destAddrBytes + if err != nil { + return + } + + timeoutDuration, err := time.ParseDuration(fmt.Sprintf("%vs", options.TimeoutMs())) + + if err != nil { + return + } + + ttl := options.FirstHop() + retry := 0 + + socketAddr, err := socketAddr(destIPAddr.IP) + + connection, err := net.ListenPacket("ip4:1", fmt.Sprintf("%v.%v.%v.%v", socketAddr[0], socketAddr[1], socketAddr[2], socketAddr[3])) + if err != nil { + return + } + + defer connection.Close() + packet := ipv4.NewPacketConn(connection) + + if err := packet.SetControlMessage(ipv4.FlagTTL|ipv4.FlagSrc|ipv4.FlagDst|ipv4.FlagInterface, true); err != nil { + return TracerouteResult{}, err + } + + icmpMessage := icmp.Message{ + Type: ipv4.ICMPTypeEcho, Code: 0, + Body: &icmp.Echo{ + ID: os.Getpid() & 0xffff, + Data: []byte("HELLO-R-U-THERE"), + }, + } + + rb := make([]byte, 1500) + + for { + icmpMessage.Body.(*icmp.Echo).Seq = ttl + + wb, err := icmpMessage.Marshal(nil) + + if err != nil { + return TracerouteResult{}, err + } + + if err := packet.SetTTL(ttl); err != nil { + return TracerouteResult{}, err + } + + start := time.Now() + if _, err := packet.WriteTo(wb, nil, &destIPAddr); err != nil { + return TracerouteResult{}, err + } + + if err := packet.SetReadDeadline(start.Add(timeoutDuration)); err != nil { + return TracerouteResult{}, err + } + + var n int + var srcAddr net.Addr + + n, _, srcAddr, err = packet.ReadFrom(rb) + + if err != nil { + if err, ok := err.(net.Error); ok && err.Timeout() { + // means timeout here + notify(TracerouteHop{Success: false, TTL: ttl}, c) + retry += 1 + if retry > options.Retries() { + ttl += 1 + retry = 0 + } + continue + } + return TracerouteResult{}, err + } + + elapsed := time.Since(start) + rm, err := icmp.ParseMessage(1, rb[:n]) + + if err != nil { + return TracerouteResult{}, err + } + + srcAddrBytes, _, _ := destAddr(srcAddr.String()) + + hop := TracerouteHop{ + Success: true, + Address: srcAddrBytes, + N: n, + ElapsedTime: elapsed, + TTL: ttl, + } + + currHost, err := net.LookupAddr(srcAddr.String()) + if err == nil { + hop.Host = currHost[0] + } + + if rm.Type == ipv4.ICMPTypeEchoReply || rm.Type == ipv4.ICMPTypeTimeExceeded { + notify(hop, c) + result.Hops = append(result.Hops, hop) + } + + ttl += 1 + retry = 0 + + if ttl > options.MaxHops() || rm.Type == ipv4.ICMPTypeEchoReply { + closeNotify(c) + return result, nil + } + + time.Sleep(time.Millisecond * 500) + } +} diff --git a/tracer.go b/tracer.go new file mode 100644 index 0000000..6056d47 --- /dev/null +++ b/tracer.go @@ -0,0 +1,258 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "log" + "net" + "strconv" + "strings" + "time" + + "io/ioutil" + "net/http" + "net/url" + + "encoding/json" + + "github.com/aeden/traceroute" + "github.com/oschwald/maxminddb-golang" +) + +func remove(slice []string, s int) []string { + if s < len(slice) { + return append(slice[:s], slice[s+1:]...) + } + return slice +} + +type record struct { + ASN int `maxminddb:"autonomous_system_number"` + ASO string `maxminddb:"autonomous_system_organization"` +} + +type IPDetail struct { + Country string `json:"country"` + Province string `json:"province"` + City string `json:"city"` + ISP string `json:"isp"` +} + +type APIResponse struct { + Code int `json:"code"` + Detail IPDetail `json:"data"` +} + +//go:embed `GeoLite2-ASN.mmdb` +var buffer []byte + +func parseIPFromMap42(ip net.IP) (record, APIResponse, error) { + params := url.Values{} + + URL, err := url.Parse("http://ipip.map.dn42/whois") + + if err != nil { + return record{}, APIResponse{}, err + } + + params.Set("ip", ip.String()) + params.Set("lang", "cn") + + URL.RawQuery = params.Encode() + + resp, err := http.Get(URL.String()) + + if err != nil { + return record{}, APIResponse{}, err + } + + defer resp.Body.Close() + + body, _ := ioutil.ReadAll(resp.Body) + var res struct { + ASN string `json:"as"` + Area string `json:"area"` + } + _ = json.Unmarshal(body, &res) + + ASN, err := strconv.Atoi(strings.Replace(res.ASN, "AS", "", -1)) + if err != nil { + return record{}, APIResponse{}, err + } + + record := record{ + ASN: ASN, + } + + APIResponse := APIResponse{ + Code: 0, + Detail: IPDetail{ + ISP: strings.Join(remove(remove(strings.Split(res.Area, "\t"), 6), 5), " "), + }, + } + return record, APIResponse, nil +} + +func parseIPFromMaxminddb(ip net.IP) record { + db, err := maxminddb.FromBytes(buffer) + if err != nil { + log.Fatal(err) + } + defer db.Close() + + var record record + + err = db.Lookup(ip, &record) + + if err != nil { + log.Fatal(err) + } + + return record +} + +func parseIPFromBilibiliAPI(ip net.IP) APIResponse { + params := url.Values{} + + URL, err := url.Parse("https://api.live.bilibili.com/client/v1/Ip/getInfoNew") + + if err != nil { + log.Fatal(err) + } + + params.Set("ip", ip.String()) + + URL.RawQuery = params.Encode() + + resp, err := http.Get(URL.String()) + + if err != nil { + log.Fatal(err) + } + + defer resp.Body.Close() + + body, _ := ioutil.ReadAll(resp.Body) + var res APIResponse + _ = json.Unmarshal(body, &res) + return res +} + +func printHop(hop traceroute.TracerouteHop) { + addr := address(hop.Address) + ip := net.ParseIP(addr) + + var info record + var APIResponse APIResponse + var err error + + if _, Prefix, _ := net.ParseCIDR("172.16.0.0/12"); Prefix.Contains(ip) { + info, APIResponse, err = parseIPFromMap42(ip) + if err != nil { + log.Fatal(err) + } + } else { + info = parseIPFromMaxminddb(ip) + APIResponse = parseIPFromBilibiliAPI(ip) + } + + hostOrAddr := addr + + if hop.Host != "" { + hostOrAddr = hop.Host + } + + var ASN string + + if hop.Success { + if info.ASN == 0 { + ASN = "unknown" + } else { + ASN = fmt.Sprintf("AS%v", info.ASN) + } + if APIResponse.Code != 0 { + fmt.Printf("%-3d %8v %15v (%v) %8v\n", hop.TTL, ASN, hostOrAddr, addr, hop.ElapsedTime.Round(time.Microsecond)) + } else { + fmt.Printf("%-3d %8v %15v (%v) %8v ", hop.TTL, ASN, hostOrAddr, addr, hop.ElapsedTime.Round(time.Microsecond)) + + var flag bool = false + + if APIResponse.Detail.Country != "" { + flag = true + fmt.Printf(APIResponse.Detail.Country) + } + if APIResponse.Detail.Province != "" { + if flag { + fmt.Printf(", ") + } + fmt.Print(APIResponse.Detail.Province) + flag = true + } + if APIResponse.Detail.City != "" { + if flag { + fmt.Printf(", ") + } + fmt.Print(APIResponse.Detail.City) + flag = true + } + + if APIResponse.Detail.ISP != "" { + if flag { + fmt.Print(" ") + } + fmt.Print(APIResponse.Detail.ISP) + flag = true + } + + fmt.Printf("\n") + } + + } else { + fmt.Printf("%-3d *\n", hop.TTL) + } +} + +func address(address [4]byte) string { + return fmt.Sprintf("%v.%v.%v.%v", address[0], address[1], address[2], address[3]) +} + +func main() { + var m = flag.Int("m", traceroute.DEFAULT_MAX_HOPS, `Set the max time-to-live (max number of hops) used in outgoing probe packets (default is 64)`) + var f = flag.Int("f", traceroute.DEFAULT_FIRST_HOP, `Set the first used time-to-live, e.g. the first hop (default is 1)`) + var q = flag.Int("q", 1, `Set the number of probes per "ttl" to nqueries (default is one probe).`) + + flag.Parse() + host := flag.Arg(0) + options := traceroute.TracerouteOptions{} + options.SetRetries(*q - 1) + options.SetMaxHops(*m) + options.SetFirstHop(*f) + + ipAddr, err := net.ResolveIPAddr("ip", host) + if err != nil { + return + } + + fmt.Printf("traceroute to %v (%v), %v hops max, %v byte packets, using ICMP methods.\n", host, ipAddr, options.MaxHops(), options.PacketSize()) + + c := make(chan traceroute.TracerouteHop, 0) + go func() { + for { + hop, ok := <-c + if !ok { + fmt.Println() + return + } + printHop(hop) + } + }() + + res, err := traceroute.Traceroute(ipAddr.String(), &options, c) + if err != nil { + fmt.Printf("Error: %v", err) + } + + printHop(res.Hops[len(res.Hops)-1]) + +}