├── .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 | [![Build Status](https://travis-ci.org/0xcafed00d/tracetcp-go.svg?branch=master)](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 |
4 |
5 | Host: 6 |
7 |
8 | Port: 9 |
10 |
11 | Trace to Source: 12 |
13 |
14 | 15 |
16 |
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 | --------------------------------------------------------------------------------