This commit is contained in:
186526 2022-05-05 00:48:40 +08:00
commit e4af31660d
Signed by: 186526
GPG Key ID: C7EB1E6B8CC5E51D
9 changed files with 621 additions and 0 deletions

23
.gitignore vendored Normal file
View File

@ -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

BIN
GeoLite2-ASN.mmdb Normal file

Binary file not shown.

9
LICENSE Normal file
View File

@ -0,0 +1,9 @@
MIT License
Copyright (c) 2021 186526 <i@186526.xyz>
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.

17
go.mod Normal file
View File

@ -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

15
go.sum Normal file
View File

@ -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=

7
lib/go.mod Normal file
View File

@ -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

4
lib/go.sum Normal file
View File

@ -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=

288
lib/traceroute.go Normal file
View File

@ -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)
}
}

258
tracer.go Normal file
View File

@ -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])
}