From a2568ac3caee57f849b08cb036e8a44440bbf3c5 Mon Sep 17 00:00:00 2001 From: ysicing Date: Sun, 10 Sep 2023 09:34:54 +0800 Subject: [PATCH] fix(update): update update Signed-off-by: ysicing --- cmd/debug/chinaroute.go | 556 +----------------- internal/pkg/chinaroute/chinaroute.go | 21 + internal/pkg/chinaroute/chinaroute_unix.go | 550 +++++++++++++++++ internal/pkg/chinaroute/chinaroute_windows.go | 14 + 4 files changed, 590 insertions(+), 551 deletions(-) create mode 100644 internal/pkg/chinaroute/chinaroute.go create mode 100644 internal/pkg/chinaroute/chinaroute_unix.go create mode 100644 internal/pkg/chinaroute/chinaroute_windows.go diff --git a/cmd/debug/chinaroute.go b/cmd/debug/chinaroute.go index 937f545..cde6f9a 100644 --- a/cmd/debug/chinaroute.go +++ b/cmd/debug/chinaroute.go @@ -7,38 +7,14 @@ package debug import ( - "context" "fmt" - "net" - "sort" - "strings" - "sync" - "sync/atomic" - "syscall" "time" - "github.com/cockroachdb/errors" "github.com/ergoapi/util/color" "github.com/spf13/cobra" + "github.com/ysicing/tiga/internal/pkg/chinaroute" "github.com/ysicing/tiga/internal/pkg/myip" "github.com/ysicing/tiga/pkg/factory" - "golang.org/x/net/icmp" - "golang.org/x/net/ipv4" - "golang.org/x/net/ipv6" -) - -type Result struct { - i int - s string -} - -var ( - ips = []string{"219.141.136.12", "202.106.50.1", "221.179.155.161", "202.96.209.133", "210.22.97.1", - "211.136.112.200", "58.60.188.222", "210.21.196.6", "120.196.165.24", "61.139.2.69", "119.6.6.6", - "211.137.96.205"} - names = []string{"北京电信", "北京联通", "北京移动", "上海电信", "上海联通", "上海移动", "广州电信", "广州联通", "广州移动", - "成都电信", "成都联通", "成都移动"} - m = map[string]string{"AS4134": "电信163 [普通线路]", "AS4809": "电信CN2 [优质线路]", "AS4837": "联通4837[普通线路]", "AS9929": "联通9929[优质线路]", "AS9808": "移动CMI [普通线路]", "AS58453": "移动CMI [普通线路]"} ) func ChinaRouteCommand(f factory.Factory) *cobra.Command { @@ -48,7 +24,7 @@ func ChinaRouteCommand(f factory.Factory) *cobra.Command { Run: func(cmd *cobra.Command, args []string) { var ( s [12]string - c = make(chan Result) + c = make(chan chinaroute.Result) t = time.After(time.Second * 10) ) t1 := time.Now() @@ -56,15 +32,15 @@ func ChinaRouteCommand(f factory.Factory) *cobra.Command { ipinfo := myip.NewIPInfoIO().IP() f.GetLog().Infof("国家: %s 城市: %s 服务商: %s", color.SGreen(ipinfo.Country), color.SGreen(ipinfo.City), color.SGreen(ipinfo.Org)) - for i := range ips { - go trace(c, i) + for i := range chinaroute.ChinaIPS { + go chinaroute.ChinaTrace(c, i) } loop: for range s { select { case o := <-c: - s[o.i] = o.s + s[o.I] = o.S case <-t: break loop } @@ -79,525 +55,3 @@ func ChinaRouteCommand(f factory.Factory) *cobra.Command { } return cmd } - -func trace(ch chan Result, i int) { - hops, err := Trace(net.ParseIP(ips[i])) - if err != nil { - s := fmt.Sprintf("%v %-15s %v", names[i], ips[i], err) - ch <- Result{i, s} - return - } - - for _, h := range hops { - for _, n := range h.Nodes { - asn := ipAsn(n.IP.String()) - as := m[asn] - switch asn { - case "": - continue - case "AS9929": - as = color.SBlue(as) - case "AS4809": - as = color.SMagenta(as) - default: - as = color.SWhite(as) - } - - s := fmt.Sprintf("%v %-15s %-23s", names[i], ips[i], as) - ch <- Result{i, s} - return - } - } - s := fmt.Sprintf("%v %-15s %v", names[i], ips[i], color.SRed("测试超时")) - ch <- Result{i, s} -} - -func ipAsn(ip string) string { - switch { - case strings.HasPrefix(ip, "59.43"): - return "AS4809" - case strings.HasPrefix(ip, "202.97"): - return "AS4134" - case strings.HasPrefix(ip, "218.105") || strings.HasPrefix(ip, "210.51"): - return "AS9929" - case strings.HasPrefix(ip, "219.158"): - return "AS4837" - case strings.HasPrefix(ip, "223.118") || strings.HasPrefix(ip, "223.119") || strings.HasPrefix(ip, "223.120") || strings.HasPrefix(ip, "223.121"): - return "AS58453" - default: - return "" - } -} - -// DefaultConfig is the default configuration for Tracer. -var DefaultConfig = Config{ - Delay: 50 * time.Millisecond, - Timeout: 500 * time.Millisecond, - MaxHops: 15, - Count: 1, - Networks: []string{"ip4:icmp", "ip4:ip"}, -} - -// DefaultTracer is a tracer with DefaultConfig. -var DefaultTracer = &Tracer{ - Config: DefaultConfig, -} - -// Config is a configuration for Tracer. -type Config struct { - Delay time.Duration - Timeout time.Duration - MaxHops int - Count int - Networks []string - Addr *net.IPAddr -} - -// Tracer is a traceroute tool based on raw IP packets. -// It can handle multiple sessions simultaneously. -type Tracer struct { - Config - - once sync.Once - conn *net.IPConn - err error - - mu sync.RWMutex - sess map[string][]*Session - seq uint32 -} - -// Trace starts sending IP packets increasing TTL until MaxHops and calls h for each reply. -func (t *Tracer) Trace(ctx context.Context, ip net.IP, h func(reply *Reply)) error { - sess, err := t.NewSession(ip) - if err != nil { - return err - } - defer sess.Close() - - delay := time.NewTicker(t.Delay) - defer delay.Stop() - - max := t.MaxHops - for n := 0; n < t.Count; n++ { - for ttl := 1; ttl <= t.MaxHops && ttl <= max; ttl++ { - err = sess.Ping(ttl) - if err != nil { - return err - } - select { - case <-delay.C: - case r := <-sess.Receive(): - if max > r.Hops && ip.Equal(r.IP) { - max = r.Hops - } - h(r) - case <-ctx.Done(): - return ctx.Err() - } - } - } - if sess.isDone(max) { - return nil - } - deadline := time.After(t.Timeout) - for { - select { - case r := <-sess.Receive(): - if max > r.Hops && ip.Equal(r.IP) { - max = r.Hops - } - h(r) - if sess.isDone(max) { - return nil - } - case <-deadline: - return nil - case <-ctx.Done(): - return ctx.Err() - } - } -} - -// NewSession returns new tracer session. -func (t *Tracer) NewSession(ip net.IP) (*Session, error) { - t.once.Do(t.init) - if t.err != nil { - return nil, t.err - } - return newSession(t, shortIP(ip)), nil -} - -func (t *Tracer) init() { - for _, network := range t.Networks { - t.conn, t.err = t.listen(network, t.Addr) - if t.err != nil { - continue - } - go t.serve(t.conn) - return - } -} - -func (t *Tracer) listen(network string, laddr *net.IPAddr) (*net.IPConn, error) { - conn, err := net.ListenIP(network, laddr) - if err != nil { - return nil, err - } - raw, err := conn.SyscallConn() - if err != nil { - conn.Close() - return nil, err - } - _ = raw.Control(func(fd uintptr) { - err = syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IP, syscall.IP_HDRINCL, 1) - }) - if err != nil { - conn.Close() - return nil, err - } - return conn, nil -} - -// Close closes listening socket. -// Tracer can not be used after Close is called. -func (t *Tracer) Close() { - t.mu.Lock() - defer t.mu.Unlock() - if t.conn != nil { - t.conn.Close() - } -} - -func (t *Tracer) serve(conn *net.IPConn) error { - defer conn.Close() - buf := make([]byte, 1500) - for { - n, from, err := conn.ReadFromIP(buf) - if err != nil { - return err - } - err = t.serveData(from.IP, buf[:n]) - if err != nil { - continue - } - } -} - -func (t *Tracer) serveData(from net.IP, b []byte) error { - if from.To4() == nil { - // TODO: implement ProtocolIPv6ICMP - return errUnsupportedProtocol - } - now := time.Now() - msg, err := icmp.ParseMessage(ProtocolICMP, b) - if err != nil { - return err - } - if msg.Type == ipv4.ICMPTypeEchoReply { - echo := msg.Body.(*icmp.Echo) - return t.serveReply(from, &packet{from, uint16(echo.ID), 1, now}) - } - b = getReplyData(msg) - if len(b) < ipv4.HeaderLen { - return errMessageTooShort - } - switch b[0] >> 4 { - case ipv4.Version: - ip, err := ipv4.ParseHeader(b) - if err != nil { - return err - } - return t.serveReply(ip.Dst, &packet{from, uint16(ip.ID), ip.TTL, now}) - case ipv6.Version: - ip, err := ipv6.ParseHeader(b) - if err != nil { - return err - } - return t.serveReply(ip.Dst, &packet{from, uint16(ip.FlowLabel), ip.HopLimit, now}) - default: - return errUnsupportedProtocol - } -} - -func (t *Tracer) sendRequest(dst net.IP, ttl int) (*packet, error) { - id := uint16(atomic.AddUint32(&t.seq, 1)) - b := newPacket(id, dst, ttl) - req := &packet{dst, id, ttl, time.Now()} - _, err := t.conn.WriteToIP(b, &net.IPAddr{IP: dst}) - if err != nil { - return nil, err - } - return req, nil -} - -func (t *Tracer) addSession(s *Session) { - t.mu.Lock() - defer t.mu.Unlock() - if t.sess == nil { - t.sess = make(map[string][]*Session) - } - t.sess[string(s.ip)] = append(t.sess[string(s.ip)], s) -} - -func (t *Tracer) removeSession(s *Session) { - t.mu.Lock() - defer t.mu.Unlock() - a := t.sess[string(s.ip)] - for i, it := range a { - if it == s { - t.sess[string(s.ip)] = append(a[:i], a[i+1:]...) - return - } - } -} - -func (t *Tracer) serveReply(dst net.IP, res *packet) error { - t.mu.RLock() - defer t.mu.RUnlock() - a := t.sess[string(shortIP(dst))] - for _, s := range a { - s.handle(res) - } - return nil -} - -// Session is a tracer session. -type Session struct { - t *Tracer - ip net.IP - ch chan *Reply - - mu sync.RWMutex - probes []*packet -} - -// NewSession returns new session. -func NewSession(ip net.IP) (*Session, error) { - return DefaultTracer.NewSession(ip) -} - -func newSession(t *Tracer, ip net.IP) *Session { - s := &Session{ - t: t, - ip: ip, - ch: make(chan *Reply, 64), - } - t.addSession(s) - return s -} - -// Ping sends single ICMP packet with specified TTL. -func (s *Session) Ping(ttl int) error { - req, err := s.t.sendRequest(s.ip, ttl+1) - if err != nil { - return err - } - s.mu.Lock() - s.probes = append(s.probes, req) - s.mu.Unlock() - return nil -} - -// Receive returns channel to receive ICMP replies. -func (s *Session) Receive() <-chan *Reply { - return s.ch -} - -// isDone returns true if session does not have unresponsed requests with TTL <= ttl. -func (s *Session) isDone(ttl int) bool { - s.mu.RLock() - defer s.mu.RUnlock() - for _, r := range s.probes { - if r.TTL <= ttl { - return false - } - } - return true -} - -func (s *Session) handle(res *packet) { - now := res.Time - n := 0 - var req *packet - s.mu.Lock() - for _, r := range s.probes { - if now.Sub(r.Time) > s.t.Timeout { - continue - } - if r.ID == res.ID { - req = r - continue - } - s.probes[n] = r - n++ - } - s.probes = s.probes[:n] - s.mu.Unlock() - if req == nil { - return - } - hops := req.TTL - res.TTL + 1 - if hops < 1 { - hops = 1 - } - select { - case s.ch <- &Reply{ - IP: res.IP, - RTT: res.Time.Sub(req.Time), - Hops: hops, - }: - default: - } -} - -// Close closes tracer session. -func (s *Session) Close() { - s.t.removeSession(s) -} - -type packet struct { - IP net.IP - ID uint16 - TTL int - Time time.Time -} - -func shortIP(ip net.IP) net.IP { - if v := ip.To4(); v != nil { - return v - } - return ip -} - -func getReplyData(msg *icmp.Message) []byte { - switch b := msg.Body.(type) { - case *icmp.TimeExceeded: - return b.Data - case *icmp.DstUnreach: - return b.Data - case *icmp.ParamProb: - return b.Data - } - return nil -} - -var ( - errMessageTooShort = errors.New("message too short") - errUnsupportedProtocol = errors.New("unsupported protocol") - errNoReplyData = errors.New("no reply data") -) - -func newPacket(id uint16, dst net.IP, ttl int) []byte { - // TODO: reuse buffers... - msg := icmp.Message{ - Type: ipv4.ICMPTypeEcho, - Body: &icmp.Echo{ - ID: int(id), - Seq: int(id), - }, - } - p, _ := msg.Marshal(nil) - ip := &ipv4.Header{ - Version: ipv4.Version, - Len: ipv4.HeaderLen, - TotalLen: ipv4.HeaderLen + len(p), - TOS: 16, - ID: int(id), - Dst: dst, - Protocol: ProtocolICMP, - TTL: ttl, - } - buf, err := ip.Marshal() - if err != nil { - return nil - } - return append(buf, p...) -} - -// IANA Assigned Internet Protocol Numbers -const ( - ProtocolICMP = 1 - ProtocolTCP = 6 - ProtocolUDP = 17 - ProtocolIPv6ICMP = 58 -) - -// Reply is a reply packet. -type Reply struct { - IP net.IP - RTT time.Duration - Hops int -} - -// Node is a detected network node. -type Node struct { - IP net.IP - RTT []time.Duration -} - -// Hop is a set of detected nodes. -type Hop struct { - Nodes []*Node - Distance int -} - -// Add adds node from r. -func (h *Hop) Add(r *Reply) *Node { - var node *Node - for _, it := range h.Nodes { - if it.IP.Equal(r.IP) { - node = it - break - } - } - if node == nil { - node = &Node{IP: r.IP} - h.Nodes = append(h.Nodes, node) - } - node.RTT = append(node.RTT, r.RTT) - return node -} - -// Trace is a simple traceroute tool using DefaultTracer. -func Trace(ip net.IP) ([]*Hop, error) { - hops := make([]*Hop, 0, DefaultTracer.MaxHops) - touch := func(dist int) *Hop { - for _, h := range hops { - if h.Distance == dist { - return h - } - } - h := &Hop{Distance: dist} - hops = append(hops, h) - return h - } - err := DefaultTracer.Trace(context.Background(), ip, func(r *Reply) { - touch(r.Hops).Add(r) - }) - if err != nil && err != context.DeadlineExceeded { - return nil, err - } - sort.Slice(hops, func(i, j int) bool { - return hops[i].Distance < hops[j].Distance - }) - last := len(hops) - 1 - for i := last; i >= 0; i-- { - h := hops[i] - if len(h.Nodes) == 1 && ip.Equal(h.Nodes[0].IP) { - continue - } - if i == last { - break - } - i++ - node := hops[i].Nodes[0] - i++ - for _, it := range hops[i:] { - node.RTT = append(node.RTT, it.Nodes[0].RTT...) - } - hops = hops[:i] - break - } - return hops, nil -} diff --git a/internal/pkg/chinaroute/chinaroute.go b/internal/pkg/chinaroute/chinaroute.go new file mode 100644 index 0000000..8c0f9cc --- /dev/null +++ b/internal/pkg/chinaroute/chinaroute.go @@ -0,0 +1,21 @@ +// Copyright (c) 2023 ysicing(ysicing.me, ysicing@ysicing.cloud) All rights reserved. +// Use of this source code is covered by the following dual licenses: +// (1) Y PUBLIC LICENSE 1.0 (YPL 1.0) +// (2) Affero General Public License 3.0 (AGPL 3.0) +// License that can be found in the LICENSE file. + +package chinaroute + +type Result struct { + I int + S string +} + +var ( + ChinaIPS = []string{"219.141.136.12", "202.106.50.1", "221.179.155.161", "202.96.209.133", "210.22.97.1", + "211.136.112.200", "58.60.188.222", "210.21.196.6", "120.196.165.24", "61.139.2.69", "119.6.6.6", + "211.137.96.205"} + names = []string{"北京电信", "北京联通", "北京移动", "上海电信", "上海联通", "上海移动", "广州电信", "广州联通", "广州移动", + "成都电信", "成都联通", "成都移动"} + m = map[string]string{"AS4134": "电信163 [普通线路]", "AS4809": "电信CN2 [优质线路]", "AS4837": "联通4837[普通线路]", "AS9929": "联通9929[优质线路]", "AS9808": "移动CMI [普通线路]", "AS58453": "移动CMI [普通线路]"} +) diff --git a/internal/pkg/chinaroute/chinaroute_unix.go b/internal/pkg/chinaroute/chinaroute_unix.go new file mode 100644 index 0000000..846e5e2 --- /dev/null +++ b/internal/pkg/chinaroute/chinaroute_unix.go @@ -0,0 +1,550 @@ +// Copyright (c) 2023 ysicing(ysicing.me, ysicing@ysicing.cloud) All rights reserved. +// Use of this source code is covered by the following dual licenses: +// (1) Y PUBLIC LICENSE 1.0 (YPL 1.0) +// (2) Affero General Public License 3.0 (AGPL 3.0) +// License that can be found in the LICENSE file. + +//go:build !windows +// +build !windows + +package chinaroute + +import ( + "context" + "errors" + "fmt" + "net" + "sort" + "strings" + "sync" + "sync/atomic" + "syscall" + "time" + + "github.com/ergoapi/util/color" + "golang.org/x/net/icmp" + "golang.org/x/net/ipv4" + "golang.org/x/net/ipv6" +) + +// DefaultConfig is the default configuration for Tracer. +var DefaultConfig = Config{ + Delay: 50 * time.Millisecond, + Timeout: 500 * time.Millisecond, + MaxHops: 15, + Count: 1, + Networks: []string{"ip4:icmp", "ip4:ip"}, +} + +// DefaultTracer is a tracer with DefaultConfig. +var DefaultTracer = &Tracer{ + Config: DefaultConfig, +} + +// Config is a configuration for Tracer. +type Config struct { + Delay time.Duration + Timeout time.Duration + MaxHops int + Count int + Networks []string + Addr *net.IPAddr +} + +// Tracer is a traceroute tool based on raw IP packets. +// It can handle multiple sessions simultaneously. +type Tracer struct { + Config + + once sync.Once + conn *net.IPConn + err error + + mu sync.RWMutex + sess map[string][]*Session + seq uint32 +} + +// Trace starts sending IP packets increasing TTL until MaxHops and calls h for each reply. +func (t *Tracer) Trace(ctx context.Context, ip net.IP, h func(reply *Reply)) error { + sess, err := t.NewSession(ip) + if err != nil { + return err + } + defer sess.Close() + + delay := time.NewTicker(t.Delay) + defer delay.Stop() + + max := t.MaxHops + for n := 0; n < t.Count; n++ { + for ttl := 1; ttl <= t.MaxHops && ttl <= max; ttl++ { + err = sess.Ping(ttl) + if err != nil { + return err + } + select { + case <-delay.C: + case r := <-sess.Receive(): + if max > r.Hops && ip.Equal(r.IP) { + max = r.Hops + } + h(r) + case <-ctx.Done(): + return ctx.Err() + } + } + } + if sess.isDone(max) { + return nil + } + deadline := time.After(t.Timeout) + for { + select { + case r := <-sess.Receive(): + if max > r.Hops && ip.Equal(r.IP) { + max = r.Hops + } + h(r) + if sess.isDone(max) { + return nil + } + case <-deadline: + return nil + case <-ctx.Done(): + return ctx.Err() + } + } +} + +// NewSession returns new tracer session. +func (t *Tracer) NewSession(ip net.IP) (*Session, error) { + t.once.Do(t.init) + if t.err != nil { + return nil, t.err + } + return newSession(t, shortIP(ip)), nil +} + +func (t *Tracer) init() { + for _, network := range t.Networks { + t.conn, t.err = t.listen(network, t.Addr) + if t.err != nil { + continue + } + go t.serve(t.conn) + return + } +} + +func (t *Tracer) listen(network string, laddr *net.IPAddr) (*net.IPConn, error) { + conn, err := net.ListenIP(network, laddr) + if err != nil { + return nil, err + } + raw, err := conn.SyscallConn() + if err != nil { + conn.Close() + return nil, err + } + _ = raw.Control(func(fd uintptr) { + err = syscall.SetsockoptInt(int(fd), syscall.IPPROTO_IP, syscall.IP_HDRINCL, 1) + }) + if err != nil { + conn.Close() + return nil, err + } + return conn, nil +} + +// Close closes listening socket. +// Tracer can not be used after Close is called. +func (t *Tracer) Close() { + t.mu.Lock() + defer t.mu.Unlock() + if t.conn != nil { + t.conn.Close() + } +} + +func (t *Tracer) serve(conn *net.IPConn) error { + defer conn.Close() + buf := make([]byte, 1500) + for { + n, from, err := conn.ReadFromIP(buf) + if err != nil { + return err + } + err = t.serveData(from.IP, buf[:n]) + if err != nil { + continue + } + } +} + +func (t *Tracer) serveData(from net.IP, b []byte) error { + if from.To4() == nil { + // TODO: implement ProtocolIPv6ICMP + return errUnsupportedProtocol + } + now := time.Now() + msg, err := icmp.ParseMessage(ProtocolICMP, b) + if err != nil { + return err + } + if msg.Type == ipv4.ICMPTypeEchoReply { + echo := msg.Body.(*icmp.Echo) + return t.serveReply(from, &packet{from, uint16(echo.ID), 1, now}) + } + b = getReplyData(msg) + if len(b) < ipv4.HeaderLen { + return errMessageTooShort + } + switch b[0] >> 4 { + case ipv4.Version: + ip, err := ipv4.ParseHeader(b) + if err != nil { + return err + } + return t.serveReply(ip.Dst, &packet{from, uint16(ip.ID), ip.TTL, now}) + case ipv6.Version: + ip, err := ipv6.ParseHeader(b) + if err != nil { + return err + } + return t.serveReply(ip.Dst, &packet{from, uint16(ip.FlowLabel), ip.HopLimit, now}) + default: + return errUnsupportedProtocol + } +} + +func (t *Tracer) sendRequest(dst net.IP, ttl int) (*packet, error) { + id := uint16(atomic.AddUint32(&t.seq, 1)) + b := newPacket(id, dst, ttl) + req := &packet{dst, id, ttl, time.Now()} + _, err := t.conn.WriteToIP(b, &net.IPAddr{IP: dst}) + if err != nil { + return nil, err + } + return req, nil +} + +func (t *Tracer) addSession(s *Session) { + t.mu.Lock() + defer t.mu.Unlock() + if t.sess == nil { + t.sess = make(map[string][]*Session) + } + t.sess[string(s.ip)] = append(t.sess[string(s.ip)], s) +} + +func (t *Tracer) removeSession(s *Session) { + t.mu.Lock() + defer t.mu.Unlock() + a := t.sess[string(s.ip)] + for i, it := range a { + if it == s { + t.sess[string(s.ip)] = append(a[:i], a[i+1:]...) + return + } + } +} + +func (t *Tracer) serveReply(dst net.IP, res *packet) error { + t.mu.RLock() + defer t.mu.RUnlock() + a := t.sess[string(shortIP(dst))] + for _, s := range a { + s.handle(res) + } + return nil +} + +// Session is a tracer session. +type Session struct { + t *Tracer + ip net.IP + ch chan *Reply + + mu sync.RWMutex + probes []*packet +} + +// NewSession returns new session. +func NewSession(ip net.IP) (*Session, error) { + return DefaultTracer.NewSession(ip) +} + +func newSession(t *Tracer, ip net.IP) *Session { + s := &Session{ + t: t, + ip: ip, + ch: make(chan *Reply, 64), + } + t.addSession(s) + return s +} + +// Ping sends single ICMP packet with specified TTL. +func (s *Session) Ping(ttl int) error { + req, err := s.t.sendRequest(s.ip, ttl+1) + if err != nil { + return err + } + s.mu.Lock() + s.probes = append(s.probes, req) + s.mu.Unlock() + return nil +} + +// Receive returns channel to receive ICMP replies. +func (s *Session) Receive() <-chan *Reply { + return s.ch +} + +// isDone returns true if session does not have unresponsed requests with TTL <= ttl. +func (s *Session) isDone(ttl int) bool { + s.mu.RLock() + defer s.mu.RUnlock() + for _, r := range s.probes { + if r.TTL <= ttl { + return false + } + } + return true +} + +func (s *Session) handle(res *packet) { + now := res.Time + n := 0 + var req *packet + s.mu.Lock() + for _, r := range s.probes { + if now.Sub(r.Time) > s.t.Timeout { + continue + } + if r.ID == res.ID { + req = r + continue + } + s.probes[n] = r + n++ + } + s.probes = s.probes[:n] + s.mu.Unlock() + if req == nil { + return + } + hops := req.TTL - res.TTL + 1 + if hops < 1 { + hops = 1 + } + select { + case s.ch <- &Reply{ + IP: res.IP, + RTT: res.Time.Sub(req.Time), + Hops: hops, + }: + default: + } +} + +// Close closes tracer session. +func (s *Session) Close() { + s.t.removeSession(s) +} + +type packet struct { + IP net.IP + ID uint16 + TTL int + Time time.Time +} + +func shortIP(ip net.IP) net.IP { + if v := ip.To4(); v != nil { + return v + } + return ip +} + +func getReplyData(msg *icmp.Message) []byte { + switch b := msg.Body.(type) { + case *icmp.TimeExceeded: + return b.Data + case *icmp.DstUnreach: + return b.Data + case *icmp.ParamProb: + return b.Data + } + return nil +} + +var ( + errMessageTooShort = errors.New("message too short") + errUnsupportedProtocol = errors.New("unsupported protocol") + errNoReplyData = errors.New("no reply data") +) + +func newPacket(id uint16, dst net.IP, ttl int) []byte { + // TODO: reuse buffers... + msg := icmp.Message{ + Type: ipv4.ICMPTypeEcho, + Body: &icmp.Echo{ + ID: int(id), + Seq: int(id), + }, + } + p, _ := msg.Marshal(nil) + ip := &ipv4.Header{ + Version: ipv4.Version, + Len: ipv4.HeaderLen, + TotalLen: ipv4.HeaderLen + len(p), + TOS: 16, + ID: int(id), + Dst: dst, + Protocol: ProtocolICMP, + TTL: ttl, + } + buf, err := ip.Marshal() + if err != nil { + return nil + } + return append(buf, p...) +} + +// IANA Assigned Internet Protocol Numbers +const ( + ProtocolICMP = 1 + ProtocolTCP = 6 + ProtocolUDP = 17 + ProtocolIPv6ICMP = 58 +) + +// Reply is a reply packet. +type Reply struct { + IP net.IP + RTT time.Duration + Hops int +} + +// Node is a detected network node. +type Node struct { + IP net.IP + RTT []time.Duration +} + +// Hop is a set of detected nodes. +type Hop struct { + Nodes []*Node + Distance int +} + +// Add adds node from r. +func (h *Hop) Add(r *Reply) *Node { + var node *Node + for _, it := range h.Nodes { + if it.IP.Equal(r.IP) { + node = it + break + } + } + if node == nil { + node = &Node{IP: r.IP} + h.Nodes = append(h.Nodes, node) + } + node.RTT = append(node.RTT, r.RTT) + return node +} + +// Trace is a simple traceroute tool using DefaultTracer. +func Trace(ip net.IP) ([]*Hop, error) { + hops := make([]*Hop, 0, DefaultTracer.MaxHops) + touch := func(dist int) *Hop { + for _, h := range hops { + if h.Distance == dist { + return h + } + } + h := &Hop{Distance: dist} + hops = append(hops, h) + return h + } + err := DefaultTracer.Trace(context.Background(), ip, func(r *Reply) { + touch(r.Hops).Add(r) + }) + if err != nil && err != context.DeadlineExceeded { + return nil, err + } + sort.Slice(hops, func(i, j int) bool { + return hops[i].Distance < hops[j].Distance + }) + last := len(hops) - 1 + for i := last; i >= 0; i-- { + h := hops[i] + if len(h.Nodes) == 1 && ip.Equal(h.Nodes[0].IP) { + continue + } + if i == last { + break + } + i++ + node := hops[i].Nodes[0] + i++ + for _, it := range hops[i:] { + node.RTT = append(node.RTT, it.Nodes[0].RTT...) + } + hops = hops[:i] + break + } + return hops, nil +} + +func ChinaTrace(ch chan Result, i int) { + hops, err := Trace(net.ParseIP(ChinaIPS[i])) + if err != nil { + s := fmt.Sprintf("%v %-15s %v", names[i], ChinaIPS[i], err) + ch <- Result{i, s} + return + } + + for _, h := range hops { + for _, n := range h.Nodes { + asn := ipAsn(n.IP.String()) + as := m[asn] + switch asn { + case "": + continue + case "AS9929": + as = color.SBlue(as) + case "AS4809": + as = color.SMagenta(as) + default: + as = color.SWhite(as) + } + + s := fmt.Sprintf("%v %-15s %-23s", names[i], ChinaIPS[i], as) + ch <- Result{i, s} + return + } + } + s := fmt.Sprintf("%v %-15s %v", names[i], ChinaIPS[i], color.SRed("测试超时")) + ch <- Result{i, s} +} + +func ipAsn(ip string) string { + switch { + case strings.HasPrefix(ip, "59.43"): + return "AS4809" + case strings.HasPrefix(ip, "202.97"): + return "AS4134" + case strings.HasPrefix(ip, "218.105") || strings.HasPrefix(ip, "210.51"): + return "AS9929" + case strings.HasPrefix(ip, "219.158"): + return "AS4837" + case strings.HasPrefix(ip, "223.118") || strings.HasPrefix(ip, "223.119") || strings.HasPrefix(ip, "223.120") || strings.HasPrefix(ip, "223.121"): + return "AS58453" + default: + return "" + } +} diff --git a/internal/pkg/chinaroute/chinaroute_windows.go b/internal/pkg/chinaroute/chinaroute_windows.go new file mode 100644 index 0000000..27d5aa0 --- /dev/null +++ b/internal/pkg/chinaroute/chinaroute_windows.go @@ -0,0 +1,14 @@ +// Copyright (c) 2023 ysicing(ysicing.me, ysicing@ysicing.cloud) All rights reserved. +// Use of this source code is covered by the following dual licenses: +// (1) Y PUBLIC LICENSE 1.0 (YPL 1.0) +// (2) Affero General Public License 3.0 (AGPL 3.0) +// License that can be found in the LICENSE file. + +//go:build windows +// +build windows + +package chinaroute + +func ChinaTrace(ch chan Result, i int) { + +}