├── .gitignore ├── README.md ├── checker └── initcwndcheck.go ├── homepage.html └── server.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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | incwndcheck 2 | =========== 3 | 4 | Test initcwnd for any http endpoint (Linux only). 5 | 6 | What this basically does is: make a tcp connection and send a GET request. From this moment onwards, it stops acknowledging packets from the remote host. This allows us to measure how much data the server is willing to send unacknowledged. 7 | 8 | Read more about initial congestion windows : http://www.cdnplanet.com/blog/tune-tcp-initcwnd-for-optimum-performance/ 9 | 10 | Run the test online: http://www.cdnplanet.com/tools/initcwndcheck/ 11 | 12 | Usage 13 | ----- 14 | 15 | ``` 16 | go get github.com/turbobytes/initcwndcheck 17 | go build github.com/turbobytes/initcwndcheck 18 | wget -O homepage.html https://github.com/turbobytes/initcwndcheck/raw/master/homepage.html 19 | sudo ./initcwndcheck 20 | ``` 21 | 22 | In browser open http://127.0.0.1:8565/ 23 | 24 | JSON API 25 | -------- 26 | 27 | /runtest?url=http%3A//www.cdnplanet.com/ 28 | 29 | Limitations 30 | ----------- 31 | 32 | 1. Works on Linux only, because some iptables commands are hardcoded in the code. In future I will split that to platform specific modules. 33 | 2. We send receive window of 65535 34 | 3. Works on HTTP only. No HTTPS/TLS support at this time. 35 | 4. Server might send a smaller payload than expected (e.g. in case of error). Inspect hexdump to be sure 36 | 37 | Coming Soon 38 | ----------- 39 | 40 | A CLI tool to run one-off tests 41 | -------------------------------------------------------------------------------- /checker/initcwndcheck.go: -------------------------------------------------------------------------------- 1 | package initcwndcheck 2 | 3 | //Originally stollen from https://github.com/kdar/gorawtcpsyn/blob/master/main.go 4 | import ( 5 | "bytes" 6 | "github.com/google/gopacket" 7 | "github.com/google/gopacket/layers" 8 | "errors" 9 | "fmt" 10 | "log" 11 | "net" 12 | "os/exec" 13 | "strconv" 14 | "time" 15 | ) 16 | 17 | // get the local ip and port based on our destination ip 18 | func localIPPort(dstip net.IP) (net.IP, int) { 19 | serverAddr, err := net.ResolveUDPAddr("udp", dstip.String()+":12345") 20 | if err != nil { 21 | log.Fatal(err) 22 | } 23 | 24 | // We don't actually connect to anything, but we can determine 25 | // based on our destination ip what source ip we should use. 26 | if con, err := net.DialUDP("udp", nil, serverAddr); err == nil { 27 | if udpaddr, ok := con.LocalAddr().(*net.UDPAddr); ok { 28 | return udpaddr.IP, udpaddr.Port 29 | } 30 | } 31 | log.Fatal("could not get local ip: " + err.Error()) 32 | return nil, -1 33 | } 34 | 35 | func listenandcount(conn net.PacketConn, dstip string, srcport layers.TCPPort) (pkt_count, payload_size int, fullpayload []byte) { 36 | //Drain the connection without ACKing 37 | detected := make(map[uint32]bool) //Store the detected seq to weed out retransmits 38 | timer := time.NewTicker(10 * time.Second) 39 | for { 40 | select { 41 | case <-timer.C: 42 | //Stop draining when channel pings first time 43 | return 44 | default: 45 | b := make([]byte, 4096) 46 | n, addr, err := conn.ReadFrom(b) 47 | //log.Println(n) 48 | if err != nil { 49 | log.Println("error reading packet: ", err) 50 | return 51 | } else if addr.String() == dstip { 52 | packet := gopacket.NewPacket(b[:n], layers.LayerTypeTCP, gopacket.Default) 53 | if tcpLayer := packet.Layer(layers.LayerTypeTCP); tcpLayer != nil { 54 | tcp, _ := tcpLayer.(*layers.TCP) 55 | ok := detected[tcp.Seq] 56 | if !ok { 57 | //log.Println(packet) 58 | if tcp.DstPort == srcport { 59 | if payloadlayer := packet.Layer(gopacket.LayerTypePayload); payloadlayer != nil { 60 | log.Println(tcp.Seq) 61 | detected[tcp.Seq] = true 62 | pkt_count++ 63 | cnt := payloadlayer.LayerContents() 64 | fullpayload = append(fullpayload, cnt...) 65 | //fmt.Println(string(cnt)) 66 | payload_size += len(cnt) 67 | //log.Println(pkt_count, payload_size) 68 | } 69 | } 70 | } else { 71 | log.Println("retransmit") 72 | } 73 | } 74 | } 75 | } 76 | } 77 | return 78 | } 79 | 80 | func porttoint(port layers.TCPPort) string { 81 | return strconv.Itoa(int(port)) 82 | } 83 | 84 | func getack(conn net.PacketConn, srcport layers.TCPPort, dstip string) (ack uint32, err error) { 85 | for { 86 | b := make([]byte, 4096) 87 | log.Println("reading from conn") 88 | var n int 89 | var addr net.Addr 90 | n, addr, err = conn.ReadFrom(b) 91 | if err != nil { 92 | log.Println("reading..", err) 93 | return 94 | } else if addr.String() == dstip { 95 | // Decode a packet 96 | packet := gopacket.NewPacket(b[:n], layers.LayerTypeTCP, gopacket.Default) 97 | // Get the TCP layer from this packet 98 | if tcpLayer := packet.Layer(layers.LayerTypeTCP); tcpLayer != nil { 99 | tcp, _ := tcpLayer.(*layers.TCP) 100 | if tcp.DstPort == srcport { 101 | if tcp.SYN && tcp.ACK { 102 | ack = tcp.Seq 103 | } else { 104 | err = errors.New("Port is CLOSED") 105 | } 106 | return 107 | } 108 | } 109 | } else { 110 | err = errors.New("Got packet not matching addr") 111 | } 112 | } 113 | return 114 | } 115 | 116 | //Detectinitcwnd attempts to detect the initial congession window of an http endpoint. 117 | //First does a 3 way tcp handshake, sends GET request and then does not ack any response while measuring the packets received. This allows us to see how much data the server can send without acknowledgement. 118 | func Detectinitcwnd(host, url string, dstip net.IP) (pkt_count, payload_size int, fullpayload []byte, err error) { 119 | pldata := []byte(fmt.Sprintf("GET %s HTTP/1.1\r\nHost: %s\r\n\r\n", url, host)) 120 | var dstport layers.TCPPort 121 | 122 | dstport = layers.TCPPort(80) 123 | 124 | srcip, sport := localIPPort(dstip) 125 | srcport := layers.TCPPort(sport) 126 | log.Printf("using srcip: %v", srcip.String()) 127 | log.Printf("using dstip: %v", dstip.String()) 128 | 129 | // Our IP header... not used, but necessary for TCP checksumming. 130 | ip := &layers.IPv4{ 131 | SrcIP: srcip, 132 | DstIP: dstip, 133 | Protocol: layers.IPProtocolTCP, 134 | } 135 | //layers.TCPOption{3, 3, []byte{7}} maybe for window scaling... dunno 136 | tcpopts := []layers.TCPOption{layers.TCPOption{2, 4, []byte{5, 172}}} //Set MSS 1452 137 | // Our TCP header 138 | tcp := &layers.TCP{ 139 | SrcPort: srcport, 140 | DstPort: dstport, 141 | Seq: 1105024978, 142 | SYN: true, 143 | Window: 65535, 144 | Options: tcpopts, 145 | } 146 | tcp.SetNetworkLayerForChecksum(ip) 147 | 148 | // Serialize. Note: we only serialize the TCP layer, because the 149 | // socket we get with net.ListenPacket wraps our data in IPv4 packets 150 | // already. We do still need the IP layer to compute checksums 151 | // correctly, though. 152 | buf := gopacket.NewSerializeBuffer() 153 | opts := gopacket.SerializeOptions{ 154 | ComputeChecksums: true, 155 | FixLengths: true, 156 | } 157 | err = gopacket.SerializeLayers(buf, opts, tcp) 158 | if err != nil { 159 | return 160 | } 161 | var out1 bytes.Buffer 162 | iptset := exec.Command("iptables", "-A", "OUTPUT", "-p", "tcp", "--tcp-flags", "RST", "RST", "-s", srcip.String(), "--sport", porttoint(srcport), "--dport", porttoint(dstport), "-j", "DROP") 163 | iptset.Stderr = &out1 164 | log.Println(iptset) 165 | err = iptset.Run() 166 | if err != nil { 167 | return 168 | } 169 | log.Println(out1.String()) 170 | iptrem := exec.Command("iptables", "-D", "OUTPUT", "-p", "tcp", "--tcp-flags", "RST", "RST", "-s", srcip.String(), "--sport", porttoint(srcport), "--dport", porttoint(dstport), "-j", "DROP") 171 | conn, err := net.ListenPacket("ip4:tcp", "0.0.0.0") 172 | if err != nil { 173 | return 174 | } 175 | defer func() { 176 | fmt.Println(iptrem) 177 | var out bytes.Buffer 178 | iptrem.Stderr = &out 179 | err = iptrem.Run() 180 | if err != nil { 181 | log.Println(err) 182 | } 183 | fmt.Printf(out.String()) 184 | log.Println("Removed iptable rule") 185 | //Now RST should be allowed... send it 186 | rst_pkt := &layers.TCP{ 187 | SrcPort: srcport, 188 | DstPort: dstport, 189 | Seq: 1105024980, 190 | Window: 65535, 191 | RST: true, 192 | } 193 | rst_pkt.SetNetworkLayerForChecksum(ip) 194 | if err := gopacket.SerializeLayers(buf, opts, rst_pkt); err != nil { 195 | //Shadowing err since we dont care 196 | log.Println(err) 197 | } 198 | if _, err := conn.WriteTo(buf.Bytes(), &net.IPAddr{IP: dstip}); err != nil { 199 | //Shadowing err since we dont care 200 | log.Println(err) 201 | } 202 | 203 | }() 204 | log.Println("writing request") 205 | _, err = conn.WriteTo(buf.Bytes(), &net.IPAddr{IP: dstip}) 206 | if err != nil { 207 | return 208 | } 209 | 210 | // Set deadline so we don't wait forever. 211 | err = conn.SetDeadline(time.Now().Add(15 * time.Second)) 212 | if err != nil { 213 | return 214 | } 215 | //Capture synack from our syn, return the ack value 216 | ack, err := getack(conn, srcport, dstip.String()) 217 | if err != nil { 218 | log.Println(err) 219 | return 220 | } else { 221 | //Prepare http request, ack the synack 222 | payload := &layers.TCP{ 223 | SrcPort: srcport, 224 | DstPort: dstport, 225 | Seq: 1105024979, 226 | ACK: true, 227 | Window: 65535, 228 | Ack: ack + 1, 229 | } 230 | payload.SetNetworkLayerForChecksum(ip) 231 | if err := gopacket.SerializeLayers(buf, opts, payload, gopacket.Payload(pldata)); err != nil { 232 | log.Fatal(err) 233 | } 234 | if _, err := conn.WriteTo(buf.Bytes(), &net.IPAddr{IP: dstip}); err != nil { 235 | log.Fatal(err) 236 | } 237 | pkt_count, payload_size, fullpayload = listenandcount(conn, dstip.String(), srcport) 238 | log.Println("Initcwnd: ", pkt_count) 239 | log.Println("Data: ", payload_size) 240 | 241 | return 242 | } 243 | return 244 | } 245 | -------------------------------------------------------------------------------- /homepage.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Initcwnd checker 5 | 6 | 7 | 8 | URL (http only, use > 70kb object) : 9 | (check net panel for progress..) 10 | 11 | 18 |
{{result.PayloadHexDump}} 
19 | 36 | 37 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | //Originally stollen from https://github.com/kdar/gorawtcpsyn/blob/master/main.go 4 | 5 | import ( 6 | "encoding/hex" 7 | "encoding/json" 8 | "fmt" 9 | "github.com/turbobytes/initcwndcheck/checker" 10 | "log" 11 | "net" 12 | "net/http" 13 | "net/http/httputil" 14 | "net/url" 15 | ) 16 | 17 | type Result struct { 18 | PktCount, PayloadSize, TotalPayloadSize int 19 | PayloadHexDump string 20 | Err string 21 | Ip string 22 | } 23 | 24 | func homeHandler(w http.ResponseWriter, r *http.Request) { 25 | http.ServeFile(w, r, "homepage.html") 26 | } 27 | 28 | func serve(w http.ResponseWriter, r *http.Request, res *Result) { 29 | b, _ := json.Marshal(res) 30 | cb := r.FormValue("callback") 31 | if cb == "" { 32 | w.Header().Set("Content-Type", "application/json") 33 | fmt.Fprintf(w, "%s", b) 34 | } else { 35 | w.Header().Set("Content-Type", "application/javascript") 36 | fmt.Fprintf(w, "%s && %s (%s)", cb, cb, b) 37 | } 38 | } 39 | 40 | func getfullbodysize(c chan int, req *http.Request) { 41 | tr := &http.Transport{} 42 | res, _ := tr.RoundTrip(req) 43 | b, _ := httputil.DumpResponse(res, true) 44 | c <- len(b) 45 | } 46 | 47 | func handler(w http.ResponseWriter, r *http.Request) { 48 | u, err := url.Parse(r.FormValue("url")) 49 | if err != nil { 50 | serve(w, r, &Result{Err: err.Error()}) 51 | return 52 | } 53 | hostname := u.Host 54 | if hostname == "" { 55 | serve(w, r, &Result{Err: "Can't get Hostname"}) 56 | return 57 | } 58 | 59 | path := u.Path 60 | if path == "" { 61 | path = "/" 62 | } 63 | if u.RawQuery != "" { 64 | path = fmt.Sprintf("%s?%s", u.Path, u.RawQuery) 65 | } 66 | fmt.Println(hostname) 67 | fmt.Println(path) 68 | endpoint := r.FormValue("endpoint") 69 | 70 | var dstip net.IP 71 | if endpoint == "" { 72 | endpoint = hostname 73 | } 74 | ipstr := r.FormValue("ip") 75 | if ipstr == "" { 76 | dstaddrs, err := net.LookupIP(endpoint) 77 | if err != nil { 78 | serve(w, r, &Result{Err: err.Error()}) 79 | return 80 | } 81 | dstip = dstaddrs[0].To4() 82 | } else { 83 | dstip = net.ParseIP(ipstr) 84 | } 85 | fmt.Println(dstip) 86 | if dstip == nil { 87 | serve(w, r, &Result{Err: "ip error"}) 88 | return 89 | } 90 | c_fullpayload := make(chan int) 91 | req, _ := http.NewRequest("GET", fmt.Sprintf("http://%s%s", dstip.String(), path), nil) 92 | req.Host = hostname 93 | go getfullbodysize(c_fullpayload, req) 94 | pkt_count, payload_size, fullpayload, err := initcwndcheck.Detectinitcwnd(hostname, path, dstip) 95 | fullpayloadsize := <-c_fullpayload 96 | if err != nil { 97 | serve(w, r, &Result{pkt_count, payload_size, fullpayloadsize, hex.Dump(fullpayload), err.Error(), dstip.String()}) 98 | } else { 99 | serve(w, r, &Result{pkt_count, payload_size, fullpayloadsize, hex.Dump(fullpayload), "", dstip.String()}) 100 | } 101 | 102 | } 103 | 104 | func main() { 105 | http.HandleFunc("/", homeHandler) 106 | http.HandleFunc("/runtest", handler) 107 | s := &http.Server{ 108 | Addr: ":8565", 109 | } 110 | log.Fatal(s.ListenAndServe()) 111 | /* 112 | if len(os.Args) != 3 { 113 | log.Printf("Usage: %s \n", os.Args[0]) 114 | os.Exit(-1) 115 | } 116 | 117 | log.Println("starting") 118 | //Prepare http payload 119 | //Resolve destination 120 | dstaddrs, err := net.LookupIP(os.Args[1]) 121 | if err != nil { 122 | log.Fatal(err) 123 | } 124 | // parse the destination host and port from the command line os.Args 125 | dstip := dstaddrs[0].To4() 126 | pkt_count, payload_size, fullpayload, err := detectinitcwnd(os.Args[1], os.Args[2], dstip) 127 | fmt.Println(hex.Dump(fullpayload)) 128 | fmt.Printf("Packet Count: %d\nData downloaded: %d\nErr: %v\n", pkt_count, payload_size, err) 129 | */ 130 | } 131 | --------------------------------------------------------------------------------