├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── cmd
├── tracetcp
│ └── main.go
└── tracetcpserver
│ ├── editcmd.html
│ ├── exec.go
│ ├── main.go
│ ├── tracetcpserve.go
│ └── tracetcpserve_test.go
└── tracetcp
├── connect.go
├── icmp.go
├── jsontracewriter.go
├── socket.go
├── stdtracewriter.go
├── trace.go
├── traceoutputwriter.go
└── utils.go
/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiled Object files, Static and Dynamic libs (Shared Objects)
2 | *.o
3 | *.a
4 | *.so
5 |
6 | # Folders
7 | _obj
8 | _test
9 |
10 | # Architecture specific extensions/prefixes
11 | *.[568vq]
12 | [568vq].out
13 |
14 | *.cgo1.go
15 | *.cgo2.c
16 | _cgo_defun.c
17 | _cgo_gotypes.go
18 | _cgo_export.*
19 |
20 | _testmain.go
21 |
22 | *.exe
23 | *.test
24 | *.prof
25 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: go
2 |
3 | go:
4 | - 1.14
5 |
6 | install:
7 | - go get -v -t github.com/0xcafed00d/tracetcp-go/...
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Lee Witek
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # tracetcp-go
2 | [](https://travis-ci.org/0xcafed00d/tracetcp-go)
3 |
4 | Reimplementation of tracetcp (http://simulatedsimian.github.io/tracetcp.html) in Go.
5 |
6 | ## Installation:
7 | ```bash
8 | $ go get github.com/0xcafed00d/tracetcp-go/cmd/tracetcp
9 | ```
10 | Installs tracetcp executable into $GOPATH/bin
11 |
12 |
13 | ## Configuration:
14 | As tracetcp uses raw sockets it needs to be run as root, using sudo.
15 | To avoid running as root, issue the following command:
16 |
17 | ```bash
18 | sudo setcap cap_net_raw=ep tracetcp
19 | ```
20 |
21 | If tracetcp is rebuilt, setcap will need to be run again.
22 |
23 | ## Usage:
24 | ```bash
25 | ➤ ./tracetcp www.news.com
26 | Tracing route to 64.30.224.82 (phx1-rb-gtm3-tron-xw-lb.cnet.com) on port 80 over a maximum of 30 hops:
27 |
28 | 1 4ms 3ms 3ms Wintermute (192.168.1.1)
29 | 2 10ms 10ms 9ms 10.239.152.1
30 | 3 11ms 9ms 11ms perr-core-2a-ae9-609.network.virginmedia.net (62.252.175.129)
31 | 4 * * *
32 | 5 * * *
33 | 6 * * *
34 | 7 25ms 16ms 13ms brhm-bb-1c-ae0-0.network.virginmedia.net (62.254.42.110)
35 | 8 18ms 17ms 17ms 213.161.65.149
36 | 9 * * *
37 | 10 194ms 161ms 162ms ae-1-8.bar1.Phoenix1.Level3.net (4.69.133.29)
38 | 11 157ms 155ms 156ms CBS-CORPORA.bar1.Phoenix1.Level3.net (4.53.106.166)
39 | 12 158ms 157ms 158ms ae2-0.io-phx1-ex8216-1.cnet.com (64.30.227.54)
40 | 13 Connected to 64.30.224.82 on port 80
41 | ```
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/cmd/tracetcp/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "io/ioutil"
7 | "log"
8 | "os"
9 | "time"
10 |
11 | "github.com/0xcafed00d/tracetcp-go/tracetcp"
12 | )
13 |
14 | type Config struct {
15 | Help bool
16 | Timeout time.Duration
17 | NoLookups bool
18 | StartHop int
19 | EndHop int
20 | Queries int
21 | Verbose bool
22 | OutputWriter string
23 | }
24 |
25 | var config Config
26 |
27 | func init() {
28 | flag.BoolVar(&config.Help, "?", false, "display help")
29 | flag.DurationVar(&config.Timeout, "t", time.Second, "probe reply timeout")
30 | flag.BoolVar(&config.NoLookups, "n", false, "no reverse DNS lookups")
31 | flag.IntVar(&config.StartHop, "h", 1, "start hop")
32 | flag.IntVar(&config.EndHop, "m", 30, "max hops")
33 | flag.IntVar(&config.Queries, "p", 3, "pings per hop")
34 | flag.BoolVar(&config.Verbose, "v", false, "verbose output")
35 | flag.StringVar(&config.OutputWriter, "o", "std", "output format: [std|json]")
36 |
37 | flag.Usage = func() {
38 | fmt.Fprintln(os.Stderr, "Usage: tracetcp-go [options] hostname[:port]")
39 | flag.PrintDefaults()
40 | }
41 | }
42 |
43 | func exitOnError(err error) {
44 | if err != nil {
45 | fmt.Fprintln(os.Stderr, err)
46 | os.Exit(1)
47 | }
48 | }
49 |
50 | // Linux to open raw sockets without running as root: sudo setcap cap_net_raw=ep tracetcp
51 | func main() {
52 | flag.Parse()
53 |
54 | if len(flag.Args()) == 0 && config.Help {
55 | flag.Usage()
56 | os.Exit(1)
57 | }
58 |
59 | if len(flag.Args()) != 1 {
60 | fmt.Fprintln(os.Stderr, "Host not suplied")
61 | fmt.Fprintln(os.Stderr, "")
62 | flag.Usage()
63 | os.Exit(1)
64 | }
65 |
66 | host, port, err := tracetcp.SplitHostAndPort(flag.Args()[0], 80)
67 | exitOnError(err)
68 |
69 | ip, err := tracetcp.LookupAddress(host)
70 | exitOnError(err)
71 |
72 | trace := tracetcp.NewTrace()
73 | trace.BeginTrace(ip, port, config.StartHop, config.EndHop, config.Queries, config.Timeout)
74 |
75 | if !config.Verbose {
76 | log.SetOutput(ioutil.Discard)
77 | }
78 |
79 | writer, err := tracetcp.GetOutputWriter(config.OutputWriter)
80 | exitOnError(err)
81 |
82 | writer.Init(port, config.StartHop, config.EndHop, config.Queries, config.NoLookups, os.Stdout)
83 |
84 | for {
85 | ev := <-trace.Events
86 | writer.Event(ev)
87 |
88 | if config.Verbose {
89 | fmt.Println(ev)
90 | }
91 | if ev.Type == tracetcp.TraceComplete {
92 | break
93 | }
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/cmd/tracetcpserver/editcmd.html:
--------------------------------------------------------------------------------
1 |
enter command:
2 |
3 |
17 |
18 |
--------------------------------------------------------------------------------
/cmd/tracetcpserver/exec.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "errors"
5 | "io"
6 | "os/exec"
7 | "time"
8 | )
9 |
10 | var ErrTimeout = errors.New("exec timeout")
11 |
12 | func execWithTimeout(proc string, args []string, out io.Writer, timeout time.Duration) error {
13 |
14 | cmd := exec.Command(proc, args...)
15 | cmd.Stdout = out
16 | cmd.Stderr = out
17 |
18 | c := make(chan error)
19 | go func(c chan error) {
20 | c <- cmd.Run()
21 | }(c)
22 |
23 | timeoutChan := time.NewTimer(timeout)
24 |
25 | select {
26 | case err := <-c:
27 | return err
28 | case <-timeoutChan.C:
29 | cmd.Process.Kill()
30 | return ErrTimeout
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/cmd/tracetcpserver/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "log"
7 | "net/http"
8 | "os"
9 | "time"
10 | )
11 |
12 | type Config struct {
13 | Help bool
14 | TraceTimeout time.Duration
15 | ListenPort int
16 | ConcurrentTraces int
17 | }
18 |
19 | var mainConfig Config
20 |
21 | func init() {
22 | flag.BoolVar(&mainConfig.Help, "?", false, "display help")
23 | flag.DurationVar(&mainConfig.TraceTimeout, "t", time.Second*30, "max time allowed for a trace")
24 | flag.IntVar(&mainConfig.ListenPort, "p", 80, "http listen port")
25 | flag.IntVar(&mainConfig.ConcurrentTraces, "c", 30, "max concurrent traces")
26 |
27 | flag.Usage = func() {
28 | fmt.Fprintln(os.Stderr, "Usage: tracetcpserver [options]")
29 | flag.PrintDefaults()
30 | }
31 | }
32 |
33 | func exitOnError(err error) {
34 | if err != nil {
35 | fmt.Fprintln(os.Stderr, err)
36 | os.Exit(1)
37 | }
38 | }
39 |
40 | // Linux: to bind to ports <1024: sudo setcap cap_net_bind_service=+ep tracetcpserver
41 | func main() {
42 | flag.Parse()
43 |
44 | if mainConfig.Help {
45 | flag.Usage()
46 | os.Exit(1)
47 | }
48 |
49 | http.HandleFunc("/editcmd/", editCommandHandler)
50 | http.HandleFunc("/exec/", execHandler)
51 | http.HandleFunc("/dotrace/", doTraceHandler)
52 |
53 | log.Printf("Listening on port %d", mainConfig.ListenPort)
54 |
55 | err := http.ListenAndServe(fmt.Sprintf(":%d", mainConfig.ListenPort), nil)
56 | exitOnError(err)
57 | }
58 |
--------------------------------------------------------------------------------
/cmd/tracetcpserver/tracetcpserve.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "io"
6 | "net/http"
7 | "strconv"
8 | "strings"
9 | "time"
10 | "unicode"
11 | )
12 |
13 | type flushWriter struct {
14 | f http.Flusher
15 | w io.Writer
16 | }
17 |
18 | func (fw *flushWriter) Write(p []byte) (n int, err error) {
19 | n, err = fw.w.Write(p)
20 | //log.Printf("%s", p)
21 | if fw.f != nil {
22 | fw.f.Flush()
23 | }
24 | return
25 | }
26 |
27 | func editCommandHandler(w http.ResponseWriter, r *http.Request) {
28 | http.ServeFile(w, r, "editcmd.html")
29 | }
30 |
31 | func validate(s string) bool {
32 | for _, r := range s {
33 | if !unicode.IsDigit(r) && !unicode.IsLetter(r) && r != '.' {
34 | return false
35 | }
36 | }
37 | return true
38 | }
39 |
40 | type traceConfig struct {
41 | host string
42 | port string
43 | starthop int
44 | endhop int
45 | timeout time.Duration
46 | queries int
47 | nolookup bool
48 | }
49 |
50 | var defaultTraceConfig = traceConfig{
51 | host: "",
52 | port: "http",
53 | starthop: 1,
54 | endhop: 30,
55 | timeout: 1 * time.Second,
56 | queries: 3,
57 | nolookup: false,
58 | }
59 |
60 | func doTrace(w http.ResponseWriter, config *traceConfig) {
61 | fw := flushWriter{w: w}
62 | if f, ok := w.(http.Flusher); ok {
63 | fw.f = f
64 | }
65 |
66 | err := execWithTimeout("tracetcp", makeCommandLine(config), &fw, mainConfig.TraceTimeout)
67 |
68 | if err != nil {
69 | w.WriteHeader(http.StatusInternalServerError)
70 | fmt.Fprintf(w, "%s\n", err)
71 | }
72 | }
73 |
74 | func makeCommandLine(config *traceConfig) []string {
75 | args := []string{}
76 |
77 | if config.nolookup {
78 | args = append(args, "-n")
79 | }
80 |
81 | args = append(args, "-h", fmt.Sprint(config.starthop))
82 | args = append(args, "-m", fmt.Sprint(config.endhop))
83 | args = append(args, "-p", fmt.Sprint(config.queries))
84 | args = append(args, "-t", fmt.Sprint(config.timeout))
85 | args = append(args, config.host+":"+config.port)
86 |
87 | return args
88 | }
89 |
90 | func validateConfig(config *traceConfig) error {
91 |
92 | if !validate(config.host) {
93 | return fmt.Errorf("Invalid Host Name")
94 | }
95 |
96 | if !validate(config.port) {
97 | return fmt.Errorf("Invalid Port Number")
98 | }
99 |
100 | if config.starthop < 1 {
101 | return fmt.Errorf("starthop must be > 1")
102 | }
103 |
104 | if config.endhop > 127 {
105 | return fmt.Errorf("endhop must be < 128")
106 | }
107 |
108 | if config.endhop < config.starthop {
109 | return fmt.Errorf("endhop must be >= starthop")
110 | }
111 |
112 | if config.queries < 1 {
113 | return fmt.Errorf("queries must be > 1")
114 | }
115 |
116 | if config.queries > 5 {
117 | return fmt.Errorf("queries must be <= 5")
118 | }
119 |
120 | if config.timeout > time.Second*3 {
121 | return fmt.Errorf("timeout must be <= 3s")
122 | }
123 |
124 | return nil
125 | }
126 |
127 | func parseRequest(config traceConfig, reader func(name string) (string, bool)) (*traceConfig, error) {
128 |
129 | if v, ok := reader("host"); ok {
130 | config.host = v
131 | }
132 |
133 | if v, ok := reader("port"); ok {
134 | config.port = v
135 | }
136 |
137 | var err error
138 |
139 | if v, ok := reader("starthop"); ok {
140 | config.starthop, err = strconv.Atoi(v)
141 | if err != nil {
142 | return nil, fmt.Errorf("Invalid Start Hop: %v", err)
143 | }
144 | }
145 |
146 | if v, ok := reader("endhop"); ok {
147 | config.endhop, err = strconv.Atoi(v)
148 | if err != nil {
149 | return nil, fmt.Errorf("Invalid End Hop: %v", err)
150 | }
151 | }
152 |
153 | if v, ok := reader("timeout"); ok {
154 | config.timeout, err = time.ParseDuration(v)
155 | if err != nil {
156 | return nil, fmt.Errorf("Invalid Timeout Duration: %v", err)
157 | }
158 | }
159 |
160 | if v, ok := reader("queries"); ok {
161 | config.queries, err = strconv.Atoi(v)
162 | if err != nil {
163 | return nil, fmt.Errorf("Invalid Query Count: %v", err)
164 | }
165 | }
166 |
167 | return &config, nil
168 | }
169 |
170 | func doTraceHandler(w http.ResponseWriter, r *http.Request) {
171 |
172 | config, err := parseRequest(defaultTraceConfig, func(name string) (string, bool) {
173 | if v, ok := r.URL.Query()[name]; ok {
174 | return v[0], ok
175 | }
176 | return "", false
177 | })
178 |
179 | if err != nil {
180 | w.WriteHeader(http.StatusBadRequest)
181 | fmt.Fprint(w, err)
182 | return
183 | }
184 |
185 | err = validateConfig(config)
186 | if err != nil {
187 | w.WriteHeader(http.StatusBadRequest)
188 | fmt.Fprint(w, "Error: ", err)
189 | }
190 |
191 | doTrace(w, config)
192 | }
193 |
194 | func execHandler(w http.ResponseWriter, r *http.Request) {
195 |
196 | config := defaultTraceConfig
197 |
198 | config.host = r.FormValue("host")
199 | config.port = r.FormValue("port")
200 |
201 | if r.FormValue("source") == "ok" {
202 | config.host = r.RemoteAddr[:strings.Index(r.RemoteAddr, ":")]
203 | }
204 |
205 | doTrace(w, &config)
206 | }
207 |
--------------------------------------------------------------------------------
/cmd/tracetcpserver/tracetcpserve_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "testing"
5 | "time"
6 |
7 | "github.com/0xcafed00d/assert"
8 | )
9 |
10 | type Values map[string]string
11 |
12 | func (v Values) read(name string) (val string, ok bool) {
13 | val, ok = (v)[name]
14 | return
15 | }
16 |
17 | func (v Values) clone() Values {
18 | dst := Values{}
19 | for key, val := range v {
20 | dst[key] = val
21 | }
22 | return dst
23 | }
24 |
25 | var testConfig = traceConfig{
26 | host: "www.google.com",
27 | port: "https",
28 | starthop: 1,
29 | endhop: 30,
30 | timeout: 1 * time.Second,
31 | queries: 3,
32 | nolookup: false,
33 | }
34 |
35 | func TestParseRequest(t *testing.T) {
36 |
37 | assert := assert.Make(t)
38 |
39 | v := Values{
40 | "host": "www.google.com",
41 | "port": "https",
42 | "starthop": "1",
43 | "endhop": "30",
44 | "queries": "3",
45 | }
46 |
47 | assert(parseRequest(testConfig, Values{}.read)).NoError().Equal(testConfig, nil)
48 | assert(parseRequest(testConfig, v.read)).NoError().Equal(testConfig, nil)
49 |
50 | v2 := v.clone()
51 | v2["starthop"] = "abc"
52 | assert(parseRequest(testConfig, v2.read)).HasError()
53 |
54 | v2 = v.clone()
55 | v2["endhop"] = "abc"
56 | assert(parseRequest(testConfig, v2.read)).HasError()
57 |
58 | v2 = v.clone()
59 | v2["queries"] = "abc"
60 | assert(parseRequest(testConfig, v2.read)).HasError()
61 | }
62 |
63 | func TestValidateConfig(t *testing.T) {
64 |
65 | assert := assert.Make(t)
66 |
67 | assert(validateConfig(&testConfig)).NoError()
68 | assert(validateConfig(&traceConfig{})).HasError()
69 |
70 | cfg := testConfig
71 | cfg.host += "|"
72 | assert(validateConfig(&cfg)).HasError()
73 |
74 | cfg = testConfig
75 | cfg.port += "&"
76 | assert(validateConfig(&cfg)).HasError()
77 |
78 | cfg = testConfig
79 | cfg.starthop = 0
80 | assert(validateConfig(&cfg)).HasError()
81 |
82 | cfg = testConfig
83 | cfg.endhop = 0
84 | assert(validateConfig(&cfg)).HasError()
85 |
86 | cfg = testConfig
87 | cfg.endhop = 128
88 | assert(validateConfig(&cfg)).HasError()
89 |
90 | cfg = testConfig
91 | cfg.endhop = 45
92 | cfg.starthop = 46
93 | assert(validateConfig(&cfg)).HasError()
94 |
95 | cfg = testConfig
96 | cfg.queries = 0
97 | assert(validateConfig(&cfg)).HasError()
98 |
99 | cfg = testConfig
100 | cfg.queries = 6
101 | assert(validateConfig(&cfg)).HasError()
102 |
103 | cfg = testConfig
104 | cfg.timeout = 3*time.Second + 1
105 | assert(validateConfig(&cfg)).HasError()
106 | }
107 |
108 | func TestCommandLine(t *testing.T) {
109 | assert := assert.Make(t)
110 |
111 | cfg := testConfig
112 | assert(makeCommandLine(&cfg)).Equal([]string{"-h", "1", "-m", "30", "-p", "3", "-t", "1s", "www.google.com:https"})
113 | cfg.nolookup = true
114 | assert(makeCommandLine(&cfg)).Equal([]string{"-n", "-h", "1", "-m", "30", "-p", "3", "-t", "1s", "www.google.com:https"})
115 | }
116 |
--------------------------------------------------------------------------------
/tracetcp/connect.go:
--------------------------------------------------------------------------------
1 | package tracetcp
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "net"
7 | "syscall"
8 | "time"
9 | )
10 |
11 | type connectEventType int
12 |
13 | const (
14 | connectNone connectEventType = iota
15 | connectTimedOut
16 | connectConnected
17 | connectRefused
18 | connectUnreachable
19 | connectError
20 | )
21 |
22 | // implementation of fmt.Stinger interface
23 | func (t connectEventType) String() string {
24 | switch t {
25 | case connectNone:
26 | return "none"
27 | case connectTimedOut:
28 | return "timedOut"
29 | case connectConnected:
30 | return "connected"
31 | case connectRefused:
32 | return "connectRefused"
33 | case connectUnreachable:
34 | return "connectUnreachable"
35 | case connectError:
36 | return "errored"
37 | }
38 | return "Invalid implTraceEventType"
39 | }
40 |
41 | type connectEvent struct {
42 | evtype connectEventType
43 | timeStamp time.Time
44 |
45 | localAddr net.IPAddr
46 | localPort int
47 | remoteAddr net.IPAddr
48 | remotePort int
49 | ttl int
50 | query int
51 | err error
52 | }
53 |
54 | // implementation of fmt.Stinger interface
55 | func (e connectEvent) String() string {
56 | return fmt.Sprintf("connectEvent:{type: %v, time: %v, local: %v:%d, remote: %v:%d, ttl: %d, query: %d, err: %v}",
57 | e.evtype.String(), e.timeStamp, e.localAddr, e.localPort, e.remoteAddr, e.remotePort, e.ttl, e.query, e.err)
58 | }
59 |
60 | func makeErrorEvent(event *connectEvent, err error) connectEvent {
61 | event.err = err
62 | event.evtype = connectError
63 | event.timeStamp = time.Now()
64 | return *event
65 | }
66 |
67 | func makeEvent(event *connectEvent, evtype connectEventType) connectEvent {
68 | event.evtype = evtype
69 | event.timeStamp = time.Now()
70 | return *event
71 | }
72 |
73 | func tryConnect(dest net.IPAddr, port, ttl, query int, timeout time.Duration) (result connectEvent) {
74 |
75 | log.Printf("try Connect dest: %v port: %v ttl: %v query: %v timeout: %v",
76 | dest, port, ttl, query, timeout)
77 |
78 | // fill in the event with as much info as we have so far
79 | event := connectEvent{
80 | remoteAddr: dest,
81 | remotePort: port,
82 | ttl: ttl,
83 | query: query,
84 | }
85 |
86 | sock, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_STREAM, syscall.IPPROTO_TCP)
87 | if err != nil {
88 | result = makeErrorEvent(&event, err)
89 | return
90 | }
91 | defer syscall.Close(sock)
92 |
93 | err = syscall.SetsockoptInt(sock, 0x0, syscall.IP_TTL, ttl)
94 | if err != nil {
95 | result = makeErrorEvent(&event, err)
96 | return
97 | }
98 |
99 | err = syscall.SetNonblock(sock, true)
100 | if err != nil {
101 | result = makeErrorEvent(&event, err)
102 | return
103 | }
104 |
105 | // ignore error from connect in non-blocking mode. as it will always return an
106 | // in progress error
107 | _ = syscall.Connect(sock, ToSockaddrInet4(dest, port))
108 |
109 | // get the local ip address and port number
110 | local, err := syscall.Getsockname(sock)
111 | if err != nil {
112 | result = makeErrorEvent(&event, err)
113 | return
114 | }
115 |
116 | // fill in the local endpoint deatils on the event struct
117 | event.localAddr, event.localPort, err = ToIPAddrAndPort(local)
118 | if err != nil {
119 | result = makeErrorEvent(&event, err)
120 | return
121 | }
122 | log.Printf(".... try Connect local endpoint: %v : %v", event.localAddr, event.localPort)
123 |
124 | state, err := waitWithTimeout(sock, timeout)
125 | switch state {
126 | case SocketError:
127 | result = makeErrorEvent(&event, err)
128 | case SocketConnected:
129 | result = makeEvent(&event, connectConnected)
130 | case SocketNotReached:
131 | result = makeEvent(&event, connectUnreachable)
132 | case SocketPortClosed:
133 | result = makeEvent(&event, connectRefused)
134 | case SocketTimedOut:
135 | result = makeEvent(&event, connectTimedOut)
136 | }
137 | return
138 | }
139 |
--------------------------------------------------------------------------------
/tracetcp/icmp.go:
--------------------------------------------------------------------------------
1 | package tracetcp
2 |
3 | import (
4 | "bytes"
5 | "encoding/binary"
6 | "fmt"
7 | "net"
8 | "syscall"
9 | "time"
10 | )
11 |
12 | type icmpEventType int
13 |
14 | const (
15 | icmpNone icmpEventType = iota
16 | icmpTTLExpired
17 | icmpNoRoute
18 | icmpError
19 | )
20 |
21 | // implementation of fmt.Stinger interface
22 | func (t icmpEventType) String() string {
23 | switch t {
24 | case icmpNone:
25 | return "none"
26 | case icmpTTLExpired:
27 | return "ttlExpired"
28 | case icmpNoRoute:
29 | return "noRoute"
30 | case icmpError:
31 | return "error"
32 | }
33 | return "Invalid implTraceEventType"
34 | }
35 |
36 | type icmpEvent struct {
37 | evtype icmpEventType
38 | timeStamp time.Time
39 |
40 | localAddr net.IPAddr
41 | localPort int
42 | remoteAddr net.IPAddr
43 | remotePort int
44 | err error
45 | }
46 |
47 | // implementation of fmt.Stinger interface
48 | func (e icmpEvent) String() string {
49 | return fmt.Sprintf("icmpEvent:{type: %v, time: %v, local: %v:%d, remote: %v:%d, err: %v}",
50 | e.evtype.String(), e.timeStamp, e.localAddr, e.localPort, e.remoteAddr, e.remotePort, e.err)
51 | }
52 |
53 | func makeICMPErrorEvent(event *icmpEvent, err error) icmpEvent {
54 | event.err = err
55 | event.evtype = icmpError
56 | event.timeStamp = time.Now()
57 | return *event
58 | }
59 | func makeICMPEvent(event *icmpEvent, evtype icmpEventType) icmpEvent {
60 | event.evtype = evtype
61 | event.timeStamp = time.Now()
62 | return *event
63 | }
64 |
65 | type IPHeader struct {
66 | VerHdrLen byte
67 | TOS byte
68 | TotalLen uint16
69 | ID uint16
70 | FlagsFragmentOff uint16
71 | TTL byte
72 | Protocol byte
73 | HdrChk uint16
74 | SourceIP [4]byte
75 | DestIP [4]byte
76 | }
77 |
78 | type ICMPHeader struct {
79 | Type byte
80 | Code byte
81 | Chk uint16
82 | Unused uint32
83 | }
84 |
85 | type TCPHeader struct {
86 | SrcPort uint16
87 | DestPort uint16
88 | Sequence uint32
89 | }
90 |
91 | func receiveICMP(result chan icmpEvent) {
92 |
93 | // Set up the socket to receive inbound packets
94 | sock, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_RAW, syscall.IPPROTO_ICMP)
95 | if err != nil {
96 | result <- makeICMPErrorEvent(&icmpEvent{}, fmt.Errorf("%v. Did you forget to run as root?", err))
97 | return
98 | }
99 |
100 | err = syscall.Bind(sock, &syscall.SockaddrInet4{})
101 | if err != nil {
102 | result <- makeICMPErrorEvent(&icmpEvent{}, err)
103 | return
104 | }
105 |
106 | var pkt = make([]byte, 1024)
107 | for {
108 | event := icmpEvent{}
109 | _, from, err := syscall.Recvfrom(sock, pkt, 0)
110 | if err != nil {
111 | result <- makeICMPErrorEvent(&event, err)
112 | return
113 | }
114 | reader := bytes.NewReader(pkt)
115 | var ip IPHeader
116 | var icmp ICMPHeader
117 | var tcp TCPHeader
118 |
119 | err = binary.Read(reader, binary.BigEndian, &ip)
120 | if ip.Protocol != syscall.IPPROTO_ICMP {
121 | break
122 | }
123 |
124 | ipheaderlen := (ip.VerHdrLen & 0xf) * 4
125 | reader = bytes.NewReader(pkt[ipheaderlen:])
126 |
127 | err = binary.Read(reader, binary.BigEndian, &icmp)
128 | if icmp.Type != 11 || icmp.Code != 0 {
129 | break
130 | }
131 |
132 | err = binary.Read(reader, binary.BigEndian, &ip)
133 |
134 | if ip.Protocol != syscall.IPPROTO_TCP {
135 | break
136 | }
137 |
138 | err = binary.Read(reader, binary.BigEndian, &tcp)
139 |
140 | event.localAddr.IP = append(event.localAddr.IP, ip.SourceIP[:]...)
141 | event.localPort = int(tcp.SrcPort)
142 |
143 | // fill in the remote endpoint deatils on the event struct
144 | event.remoteAddr, _, _ = ToIPAddrAndPort(from)
145 | result <- makeICMPEvent(&event, icmpTTLExpired)
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/tracetcp/jsontracewriter.go:
--------------------------------------------------------------------------------
1 | package tracetcp
2 |
3 | import (
4 | "encoding/json"
5 | "io"
6 | "net"
7 | )
8 |
9 | type JSONTraceWriter struct {
10 | port int
11 | hopsFrom int
12 | hopsTo int
13 | queriesPerHop int
14 | noLooups bool
15 | out io.Writer
16 | currentHop int
17 | currentAddr *net.IPAddr
18 |
19 | jsonData []TraceEvent
20 | }
21 |
22 | func (w *JSONTraceWriter) Init(port int, hopsFrom, hopsTo, queriesPerHop int, noLookups bool, out io.Writer) {
23 | w.port = port
24 | w.hopsFrom = hopsFrom
25 | w.hopsTo = hopsTo
26 | w.queriesPerHop = queriesPerHop
27 | w.noLooups = noLookups
28 | w.out = out
29 | w.currentHop = 0
30 | }
31 |
32 | func (w *JSONTraceWriter) Event(e TraceEvent) error {
33 |
34 | w.jsonData = append(w.jsonData, e)
35 |
36 | if e.Type == TraceComplete {
37 | jsonenc := json.NewEncoder(w.out)
38 | jsonenc.Encode(w.jsonData)
39 | }
40 |
41 | return nil
42 | }
43 |
--------------------------------------------------------------------------------
/tracetcp/socket.go:
--------------------------------------------------------------------------------
1 | package tracetcp
2 |
3 | import (
4 | "fmt"
5 | "syscall"
6 | "time"
7 | )
8 |
9 | type SocketState int
10 |
11 | const (
12 | SocketConnected SocketState = iota
13 | SocketNotReached
14 | SocketTimedOut
15 | SocketPortClosed
16 | SocketError
17 | )
18 |
19 | func (s SocketState) String() string {
20 | switch s {
21 | case SocketConnected:
22 | return "SocketConnected"
23 | case SocketNotReached:
24 | return "SocketNotReached"
25 | case SocketTimedOut:
26 | return "SocketTimedOut"
27 | case SocketPortClosed:
28 | return "SocketPortClosed"
29 | case SocketError:
30 | return "SocketError"
31 | }
32 | return "SocketInvlaidState"
33 | }
34 |
35 | func waitWithTimeout(socket int, timeout time.Duration) (state SocketState, err error) {
36 | wfdset := &syscall.FdSet{}
37 |
38 | FD_ZERO(wfdset)
39 | FD_SET(wfdset, socket)
40 |
41 | timeval := syscall.NsecToTimeval(int64(timeout))
42 |
43 | syscall.Select(socket+1, nil, wfdset, nil, &timeval)
44 |
45 | errcode, err := syscall.GetsockoptInt(socket, syscall.SOL_SOCKET, syscall.SO_ERROR)
46 | if err != nil {
47 | state = SocketError
48 | return
49 | }
50 |
51 | if errcode == int(syscall.EHOSTUNREACH) {
52 | state = SocketNotReached
53 | return
54 | }
55 |
56 | if errcode == int(syscall.ECONNREFUSED) {
57 | state = SocketPortClosed
58 | return
59 | }
60 |
61 | if errcode != 0 {
62 | state = SocketError
63 | err = fmt.Errorf("Connect Error: %v", errcode)
64 | return
65 | }
66 |
67 | if FD_ISSET(wfdset, socket) {
68 | state = SocketConnected
69 | } else {
70 | state = SocketTimedOut
71 | }
72 | return
73 | }
74 |
--------------------------------------------------------------------------------
/tracetcp/stdtracewriter.go:
--------------------------------------------------------------------------------
1 | package tracetcp
2 |
3 | import (
4 | "fmt"
5 | "io"
6 | "net"
7 | "time"
8 | )
9 |
10 | type StdTraceWriter struct {
11 | port int
12 | hopsFrom int
13 | hopsTo int
14 | queriesPerHop int
15 | noLooups bool
16 | out io.Writer
17 | currentHop int
18 | currentAddr *net.IPAddr
19 | }
20 |
21 | func (w *StdTraceWriter) Init(port int, hopsFrom, hopsTo, queriesPerHop int, noLookups bool, out io.Writer) {
22 | w.port = port
23 | w.hopsFrom = hopsFrom
24 | w.hopsTo = hopsTo
25 | w.queriesPerHop = queriesPerHop
26 | w.noLooups = noLookups
27 | w.out = out
28 | w.currentHop = 0
29 | }
30 |
31 | func (w *StdTraceWriter) Event(e TraceEvent) error {
32 |
33 | if e.Type == TraceFailed {
34 | fmt.Fprintf(w.out, "Error: %v\n", e.Err)
35 | return e.Err
36 | }
37 |
38 | if e.Hop != 0 && w.currentHop != e.Hop {
39 | w.currentHop = e.Hop
40 | fmt.Fprintf(w.out, "\n%-3v", e.Hop)
41 | w.currentAddr = nil
42 | }
43 |
44 | switch e.Type {
45 | case TraceStarted:
46 | var revhost string
47 | if !w.noLooups {
48 | revhost, _ = ReverseLookup(e.Addr)
49 | }
50 | if revhost != "" {
51 | fmt.Fprintf(w.out, "Tracing route to %v (%v) on port %v over a maximum of %v hops:\n",
52 | e.Addr.IP, revhost, w.port, w.hopsTo)
53 | } else {
54 | fmt.Fprintf(w.out, "Tracing route to %v on port %v over a maximum of %v hops:\n",
55 | e.Addr.IP, w.port, w.hopsTo)
56 | }
57 |
58 | case TimedOut:
59 | fmt.Fprintf(w.out, "%8v", "*")
60 | case TTLExpired:
61 | w.currentAddr = &e.Addr
62 | fmt.Fprintf(w.out, "%8v", (e.Time/time.Millisecond)*time.Millisecond)
63 | case Connected:
64 | fmt.Fprintf(w.out, "Connected to %v on port %v\n", e.Addr.String(), w.port)
65 | case RemoteClosed:
66 | fmt.Fprintf(w.out, "Port %v closed at %v\n", w.port, e.Addr.String())
67 | }
68 |
69 | if e.Query == w.queriesPerHop-1 && w.currentAddr != nil {
70 | name, _ := ReverseLookup(*w.currentAddr)
71 | if name == "" || w.noLooups {
72 | fmt.Fprintf(w.out, "\t%v", w.currentAddr.String())
73 | } else {
74 | fmt.Fprintf(w.out, "\t%v (%v)", name, w.currentAddr.String())
75 | }
76 | }
77 |
78 | return nil
79 | }
80 |
--------------------------------------------------------------------------------
/tracetcp/trace.go:
--------------------------------------------------------------------------------
1 | package tracetcp
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "net"
7 | "reflect"
8 | "time"
9 | )
10 |
11 | type TraceEventType int
12 |
13 | const (
14 | None TraceEventType = iota
15 | TimedOut
16 | TTLExpired
17 | Connected
18 | RemoteClosed
19 | TraceStarted
20 | TraceComplete
21 | TraceAborted
22 | TraceFailed
23 | )
24 |
25 | // implementation of fmt.Stinger interface
26 | func (t TraceEventType) String() string {
27 | switch t {
28 | case None:
29 | return "None"
30 | case TimedOut:
31 | return "TimedOut"
32 | case TTLExpired:
33 | return "TTLExpired"
34 | case Connected:
35 | return "Connected"
36 | case RemoteClosed:
37 | return "RemoteClosed"
38 | case TraceStarted:
39 | return "TraceStarted"
40 | case TraceComplete:
41 | return "TraceComplete"
42 | case TraceAborted:
43 | return "TraceAborted"
44 | case TraceFailed:
45 | return "TraceFailed"
46 | }
47 | return "Invalid TraceEventType"
48 | }
49 |
50 | type TraceEvent struct {
51 | Type TraceEventType
52 | Addr net.IPAddr
53 | Time time.Duration
54 | Hop int
55 | Query int
56 | Err error
57 | }
58 |
59 | // implementation of fmt.Stinger interface
60 | func (e TraceEvent) String() string {
61 | return fmt.Sprintf("TraceEvent:{type: %v, addr: %v, timetaken: %v, hop: %d, query %d, err: %v}",
62 | e.Type, e.Addr, e.Time, e.Hop, e.Query, e.Err)
63 | }
64 |
65 | type Trace struct {
66 | Events chan TraceEvent
67 | TraceRunning AtomicBool
68 | AbortRequested AtomicBool
69 | }
70 |
71 | func NewTrace() *Trace {
72 | t := Trace{}
73 |
74 | t.Events = make(chan TraceEvent, 100)
75 |
76 | return &t
77 | }
78 |
79 | func (t *Trace) BeginTrace(addr *net.IPAddr, port, beginTTL, endTTL, queries int, timeout time.Duration) error {
80 | if !t.TraceRunning.CompareAndSet(false, true) {
81 | return fmt.Errorf("Trace already in progress")
82 | }
83 |
84 | go t.traceImpl(addr, port, beginTTL, endTTL, queries, timeout)
85 | return nil
86 | }
87 |
88 | func (t *Trace) AbortTrace() {
89 | t.AbortRequested.Write(true)
90 | }
91 |
92 | func (t *Trace) traceImpl(addr *net.IPAddr, port, beginTTL, endTTL, queries int, timeout time.Duration) {
93 |
94 | icmpChan := make(chan icmpEvent, 100)
95 | go receiveICMP(icmpChan)
96 |
97 | traceStart := time.Now()
98 | t.Events <- TraceEvent{Addr: *addr, Type: TraceStarted, Time: time.Since(traceStart)}
99 | for ttl := beginTTL; ttl <= endTTL; ttl++ {
100 | for q := 0; q < queries; q++ {
101 | if t.AbortRequested.Read() {
102 | // TODO: abort trace
103 | }
104 | log.Printf("Probe query: %v hops: %v", q, ttl)
105 | queryStart := time.Now()
106 | ev := tryConnect(*addr, port, ttl, q, timeout)
107 | if t.correlateEvents(ev, icmpChan, queryStart) {
108 | t.Events <- TraceEvent{Type: TraceComplete, Time: time.Since(traceStart)}
109 | return
110 | }
111 | }
112 | }
113 | t.Events <- TraceEvent{Type: TraceComplete, Time: time.Since(traceStart)}
114 | t.TraceRunning.Write(false)
115 | }
116 |
117 | func (t *Trace) correlateEvents(ev connectEvent, icmpChan chan icmpEvent, queryStart time.Time) bool {
118 |
119 | icmpev := icmpEvent{}
120 |
121 | // collect all pending icmp events
122 | done := false
123 | for !done {
124 | select {
125 | case iev := <-icmpChan:
126 | if reflect.DeepEqual(iev.localAddr, ev.localAddr) && iev.localPort == ev.localPort {
127 | done = true
128 | icmpev = iev
129 | }
130 |
131 | if iev.evtype == icmpError {
132 | done = true
133 | icmpev = iev
134 | }
135 |
136 | case <-time.After(100 * time.Millisecond):
137 | done = true
138 | }
139 | }
140 | log.Println(ev)
141 | if icmpev.evtype == icmpNone {
142 | log.Println("No matching ICMP event")
143 | } else {
144 | log.Println("matching icmp event", icmpev)
145 | }
146 |
147 | traceEvent := TraceEvent{
148 | Hop: ev.ttl,
149 | Query: ev.query,
150 | Time: time.Since(queryStart),
151 | }
152 |
153 | if ev.evtype == connectError {
154 | traceEvent.Type = TraceFailed
155 | traceEvent.Err = ev.err
156 | t.Events <- traceEvent
157 | return true
158 | }
159 |
160 | if icmpev.evtype == icmpError {
161 | traceEvent.Type = TraceFailed
162 | traceEvent.Err = icmpev.err
163 | t.Events <- traceEvent
164 | return true
165 | }
166 |
167 | if icmpev.evtype == icmpTTLExpired && ev.evtype == connectUnreachable {
168 | traceEvent.Type = TTLExpired
169 | traceEvent.Addr = icmpev.remoteAddr
170 | traceEvent.Time = icmpev.timeStamp.Sub(queryStart)
171 | t.Events <- traceEvent
172 | return false
173 | }
174 |
175 | if ev.evtype == connectConnected {
176 | traceEvent.Type = Connected
177 | traceEvent.Addr = ev.remoteAddr
178 | t.Events <- traceEvent
179 | return true
180 | }
181 |
182 | if ev.evtype == connectTimedOut || ev.evtype == connectUnreachable {
183 | traceEvent.Type = TimedOut
184 | t.Events <- traceEvent
185 | return false
186 | }
187 |
188 | if ev.evtype == connectRefused {
189 | traceEvent.Type = RemoteClosed
190 | traceEvent.Addr = ev.remoteAddr
191 | t.Events <- traceEvent
192 | return true
193 | }
194 |
195 | panic("should not get here???")
196 |
197 | return false
198 | }
199 |
--------------------------------------------------------------------------------
/tracetcp/traceoutputwriter.go:
--------------------------------------------------------------------------------
1 | package tracetcp
2 |
3 | import (
4 | "fmt"
5 | "io"
6 | )
7 |
8 | type TraceOutputWriter interface {
9 | Init(port int, hopsFrom, hopsTo, queriesPerHop int, noLookups bool, out io.Writer)
10 | Event(e TraceEvent) error
11 | }
12 |
13 | var outputWriters = map[string]TraceOutputWriter{
14 | "std": &StdTraceWriter{},
15 | "json": &JSONTraceWriter{},
16 | }
17 |
18 | func GetOutputWriter(name string) (TraceOutputWriter, error) {
19 | if writer, ok := outputWriters[name]; ok {
20 | return writer, nil
21 | }
22 | return nil, fmt.Errorf("Invalid output format name: %v", name)
23 | }
24 |
--------------------------------------------------------------------------------
/tracetcp/utils.go:
--------------------------------------------------------------------------------
1 | package tracetcp
2 |
3 | import (
4 | "fmt"
5 | "io"
6 | "net"
7 | "strconv"
8 | "strings"
9 | "sync/atomic"
10 | "syscall"
11 | "time"
12 | )
13 |
14 | type AtomicBool struct {
15 | val int32
16 | }
17 |
18 | func b2i(b bool) int32 {
19 | if b {
20 | return 1
21 | }
22 | return 0
23 | }
24 |
25 | func (b *AtomicBool) Write(value bool) {
26 | if value {
27 | atomic.StoreInt32(&(b.val), 1)
28 | } else {
29 | atomic.StoreInt32(&(b.val), 0)
30 | }
31 | }
32 |
33 | func (b *AtomicBool) Read() bool {
34 | return atomic.LoadInt32(&(b.val)) != 0
35 | }
36 |
37 | func (b *AtomicBool) CompareAndSet(old, new bool) (setok bool) {
38 | setok = atomic.CompareAndSwapInt32(&(b.val), b2i(old), b2i(new))
39 | return
40 | }
41 |
42 | func HexDump(data []byte, out io.Writer, width int) error {
43 | dataLen := len(data)
44 |
45 | for n := 0; n < dataLen; n++ {
46 |
47 | if n%width == 0 {
48 | if n != 0 {
49 | fmt.Fprintln(out, "")
50 | }
51 |
52 | _, err := fmt.Fprintf(out, "%04x: ", n)
53 | if err != nil {
54 | return err
55 | }
56 | }
57 |
58 | _, err := fmt.Fprintf(out, "%02x ", data[n])
59 | if err != nil {
60 | return err
61 | }
62 | }
63 | fmt.Fprintln(out, "")
64 | return nil
65 | }
66 |
67 | func MakeTimeval(t time.Duration) syscall.Timeval {
68 | return syscall.NsecToTimeval(int64(t))
69 | }
70 |
71 | func FD_SET(p *syscall.FdSet, i int) {
72 | p.Bits[i/64] |= 1 << uint(i) % 64
73 | }
74 |
75 | func FD_ISSET(p *syscall.FdSet, i int) bool {
76 | return (p.Bits[i/64] & (1 << uint(i) % 64)) != 0
77 | }
78 |
79 | func FD_ZERO(p *syscall.FdSet) {
80 | for i := range p.Bits {
81 | p.Bits[i] = 0
82 | }
83 | }
84 |
85 | func SplitHostAndPort(hostAndPort string, defaultPort int) (host string, port int, err error) {
86 | parts := strings.Split(hostAndPort, ":")
87 | if len(parts) == 0 || len(parts) > 2 {
88 | err = fmt.Errorf("%s malformed host and port", hostAndPort)
89 | return
90 | }
91 | port = defaultPort
92 | if len(parts) > 0 {
93 | host = parts[0]
94 | }
95 | if len(parts) > 1 {
96 | port, err = strconv.Atoi(parts[1])
97 | if err != nil {
98 | port, err = net.LookupPort("tcp", parts[1])
99 | }
100 | }
101 | return
102 | }
103 |
104 | func ReverseLookup(ip net.IPAddr) (name string, err error) {
105 | names, err := net.LookupAddr(ip.String())
106 | if err == nil && len(names) > 0 {
107 | name = names[0]
108 | // names seem to have a . at the end. remove it
109 | if name[len(name)-1] == '.' {
110 | name = name[:len(name)-1]
111 | }
112 | }
113 | return
114 | }
115 |
116 | func LookupAddress(host string) (*net.IPAddr, error) {
117 | addresses, err := net.LookupHost(host)
118 | if err != nil {
119 | return &net.IPAddr{}, err
120 | }
121 |
122 | ip, err := net.ResolveIPAddr("ip", addresses[0])
123 | if err != nil {
124 | return &net.IPAddr{}, err
125 | }
126 | return ip, nil
127 | }
128 |
129 | func ToSockaddrInet4(ip net.IPAddr, port int) *syscall.SockaddrInet4 {
130 | var addr [4]byte
131 | copy(addr[:], ip.IP.To4())
132 |
133 | return &syscall.SockaddrInet4{Port: port, Addr: addr}
134 | }
135 |
136 | func ToIPAddrAndPort(saddr syscall.Sockaddr) (addr net.IPAddr, port int, err error) {
137 |
138 | if sa, ok := saddr.(*syscall.SockaddrInet4); ok {
139 | port = sa.Port
140 | addr.IP = append(addr.IP, sa.Addr[:]...)
141 | } else {
142 | err = fmt.Errorf("%s", "ToIPAddrAndPort: syscall.Sockaddr not a syscall.SockaddeInet4")
143 | }
144 |
145 | return
146 | }
147 |
--------------------------------------------------------------------------------