├── LICENSE.md ├── README.md └── proxy.go /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Giles Thomas 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | stupid-proxy 2 | ============ 3 | 4 | A simple routing proxy in Go. Accepts incoming connections on ports 80 and 443. 5 | 6 | * Connections on port 80 are assumed to be HTTP. A hostname is extracted from each using 7 | the HTTP "Host" header. 8 | * Connections on port 443 are assumed to be TLS. A hostname is extracted from the 9 | server name indication in the ClientHello bytes. Currently non-TLS SSL connections 10 | and TLS connections without SNIs are dropped messily. 11 | 12 | Once a hostname has been extracted from the incoming connection, the proxy looks up 13 | a set of backends on a redis server, which is assumed to be running on 127.0.0.1:6379. 14 | The key for the set is `hostnames::backends`. 15 | If there is no set stored in redis for the backend, it will check 16 | `hostnames:httpDefault:backends` for HTTP connections, or `hostnames:httpsDefault:backends` 17 | for HTTPS. If these latter two lookups fail or return empty sets, it will drop 18 | the connection. 19 | 20 | A backend is then selected at random from the list that was supplied by redis, and 21 | the whole client connection is sent down to the appropriate port on that backend. The 22 | proxy will keep proxying data back and forth until one of the endpoints closes the 23 | connection. 24 | 25 | Uses Juhani Åhman's radix client to access redis: https://github.com/fzzy/radix 26 | 27 | 28 | MIT licensed, in case you're crazy enough to want to use it for something :-) 29 | -------------------------------------------------------------------------------- /proxy.go: -------------------------------------------------------------------------------- 1 | /* 2 | A simple routing proxy in Go. Accepts incoming connections on ports 80 and 443. 3 | 4 | Connections on port 80 are assumed to be HTTP. A hostname is extracted from each using 5 | the HTTP "Host" header. 6 | Connections on port 443 are assumed to be TLS. A hostname is extracted from the 7 | server name indication in the ClientHello bytes. Currently non-TLS SSL connections 8 | and TLS connections without SNIs are dropped messily. 9 | 10 | Once a hostname has been extracted from the incoming connection, the proxy looks up 11 | a set of backends on a redis server, which is assumed to be running on 127.0.0.1:6379. 12 | The key for the set is hostnames::backends. 13 | If there is no set stored in redis for the backend, it will check 14 | hostnames:httpDefault:backends for HTTP connections, or hostnames:httpsDefault:backends 15 | for HTTPS. If these latter two lookups fail or return empty sets, it will drop 16 | the connection. 17 | 18 | A backend is then selected at random from the list that was supplied by redis, and 19 | the whole client connection is sent down to the appropriate port on that backend. 20 | The proxy will keep proxying data back and forth until one of the endpoints closes 21 | the connection. 22 | */ 23 | 24 | package main 25 | 26 | import ( 27 | "bufio" 28 | "container/list" 29 | "errors" 30 | "fmt" 31 | "github.com/fzzy/radix/redis" 32 | "io" 33 | "log" 34 | "math/rand" 35 | "net" 36 | "os" 37 | "strconv" 38 | "strings" 39 | ) 40 | 41 | func getBackend(hostname string, defaultBackendType string, redisClient *redis.Client) (string, error) { 42 | fmt.Println("Looking up", hostname) 43 | 44 | backends, error := redisClient.Cmd("smembers", "hostnames:"+hostname+":backends").List() 45 | if error != nil { 46 | fmt.Println("Error in redis lookup for hostname backend", error) 47 | return "", error 48 | } 49 | 50 | if len(backends) == 0 { 51 | backends, error = redisClient.Cmd("smembers", "hostnames:"+defaultBackendType+":backends").List() 52 | if error != nil { 53 | fmt.Println("Error in redis lookup for default backend", error) 54 | return "", error 55 | } 56 | if len(backends) == 0 { 57 | fmt.Println("No default backend of type", defaultBackendType) 58 | return "", errors.New("Could not find default backend of type " + defaultBackendType) 59 | } 60 | } 61 | 62 | fmt.Println("Found backends:", backends) 63 | backend := backends[int(rand.Float32()*float32(len(backends)))] 64 | return backend, nil 65 | } 66 | 67 | func copyAndClose(dst io.WriteCloser, src io.Reader) { 68 | io.Copy(dst, src) 69 | dst.Close() 70 | } 71 | 72 | func handleHTTPConnection(downstream net.Conn, redisClient *redis.Client) { 73 | reader := bufio.NewReader(downstream) 74 | hostname := "" 75 | readLines := list.New() 76 | for hostname == "" { 77 | bytes, _, error := reader.ReadLine() 78 | if error != nil { 79 | fmt.Println("Error reading", error) 80 | downstream.Close() 81 | return 82 | } 83 | line := string(bytes) 84 | readLines.PushBack(line) 85 | if line == "" { 86 | // End of HTTP headers 87 | break 88 | } 89 | if strings.HasPrefix(line, "Host: ") { 90 | hostname = strings.TrimPrefix(line, "Host: ") 91 | break 92 | } 93 | } 94 | backendAddress, error := getBackend(hostname, "httpDefault", redisClient) 95 | if error != nil { 96 | fmt.Println("Couldn't get backend for ", hostname, "-- got error", error) 97 | downstream.Close() 98 | return 99 | } 100 | 101 | upstream, error := net.Dial("tcp", backendAddress+":80") 102 | if error != nil { 103 | fmt.Println("Couldn't connect to backend", error) 104 | downstream.Close() 105 | return 106 | } 107 | 108 | for element := readLines.Front(); element != nil; element = element.Next() { 109 | line := element.Value.(string) 110 | upstream.Write([]byte(line)) 111 | upstream.Write([]byte("\n")) 112 | } 113 | 114 | go copyAndClose(upstream, reader) 115 | go copyAndClose(downstream, upstream) 116 | } 117 | 118 | func handleHTTPSConnection(downstream net.Conn, redisClient *redis.Client) { 119 | firstByte := make([]byte, 1) 120 | _, error := downstream.Read(firstByte) 121 | if error != nil { 122 | fmt.Println("Couldn't read first byte :-(") 123 | return 124 | } 125 | if firstByte[0] != 0x16 { 126 | fmt.Println("Not TLS :-(") 127 | } 128 | 129 | versionBytes := make([]byte, 2) 130 | _, error = downstream.Read(versionBytes) 131 | if error != nil { 132 | fmt.Println("Couldn't read version bytes :-(") 133 | return 134 | } 135 | if versionBytes[0] < 3 || (versionBytes[0] == 3 && versionBytes[1] < 1) { 136 | fmt.Println("SSL < 3.1 so it's still not TLS") 137 | return 138 | } 139 | 140 | restLengthBytes := make([]byte, 2) 141 | _, error = downstream.Read(restLengthBytes) 142 | if error != nil { 143 | fmt.Println("Couldn't read restLength bytes :-(") 144 | return 145 | } 146 | restLength := (int(restLengthBytes[0]) << 8) + int(restLengthBytes[1]) 147 | 148 | rest := make([]byte, restLength) 149 | _, error = downstream.Read(rest) 150 | if error != nil { 151 | fmt.Println("Couldn't read rest of bytes") 152 | return 153 | } 154 | 155 | current := 0 156 | 157 | handshakeType := rest[0] 158 | current += 1 159 | if handshakeType != 0x1 { 160 | fmt.Println("Not a ClientHello") 161 | return 162 | } 163 | 164 | // Skip over another length 165 | current += 3 166 | // Skip over protocolversion 167 | current += 2 168 | // Skip over random number 169 | current += 4 + 28 170 | // Skip over session ID 171 | sessionIDLength := int(rest[current]) 172 | current += 1 173 | current += sessionIDLength 174 | 175 | cipherSuiteLength := (int(rest[current]) << 8) + int(rest[current+1]) 176 | current += 2 177 | current += cipherSuiteLength 178 | 179 | compressionMethodLength := int(rest[current]) 180 | current += 1 181 | current += compressionMethodLength 182 | 183 | if current > restLength { 184 | fmt.Println("no extensions") 185 | return 186 | } 187 | 188 | // Skip over extensionsLength 189 | // extensionsLength := (int(rest[current]) << 8) + int(rest[current + 1]) 190 | current += 2 191 | 192 | hostname := "" 193 | for current < restLength && hostname == "" { 194 | extensionType := (int(rest[current]) << 8) + int(rest[current+1]) 195 | current += 2 196 | 197 | extensionDataLength := (int(rest[current]) << 8) + int(rest[current+1]) 198 | current += 2 199 | 200 | if extensionType == 0 { 201 | 202 | // Skip over number of names as we're assuming there's just one 203 | current += 2 204 | 205 | nameType := rest[current] 206 | current += 1 207 | if nameType != 0 { 208 | fmt.Println("Not a hostname") 209 | return 210 | } 211 | nameLen := (int(rest[current]) << 8) + int(rest[current+1]) 212 | current += 2 213 | hostname = string(rest[current : current+nameLen]) 214 | } 215 | 216 | current += extensionDataLength 217 | } 218 | if hostname == "" { 219 | fmt.Println("No hostname") 220 | return 221 | } 222 | 223 | backendAddress, error := getBackend(hostname, "httpsDefault", redisClient) 224 | if error != nil { 225 | fmt.Println("Couldn't get backend for ", hostname, "-- got error", error) 226 | return 227 | } 228 | 229 | upstream, error := net.Dial("tcp", backendAddress+":443") 230 | if error != nil { 231 | log.Fatal(error) 232 | return 233 | } 234 | 235 | upstream.Write(firstByte) 236 | upstream.Write(versionBytes) 237 | upstream.Write(restLengthBytes) 238 | upstream.Write(rest) 239 | 240 | go copyAndClose(upstream, downstream) 241 | go copyAndClose(downstream, upstream) 242 | } 243 | 244 | func reportDone(done chan int) { 245 | done <- 1 246 | } 247 | 248 | func doProxy(done chan int, port int, handle func(net.Conn, *redis.Client), redisClient *redis.Client) { 249 | defer reportDone(done) 250 | 251 | listener, error := net.Listen("tcp", "0.0.0.0:"+strconv.Itoa(port)) 252 | if error != nil { 253 | fmt.Println("Couldn't start listening", error) 254 | return 255 | } 256 | fmt.Println("Started proxy on", port, "-- listening...") 257 | for { 258 | connection, error := listener.Accept() 259 | if error != nil { 260 | fmt.Println("Accept error", error) 261 | return 262 | } 263 | 264 | go handle(connection, redisClient) 265 | } 266 | } 267 | 268 | func main() { 269 | redisClient, error := redis.Dial("tcp", "127.0.0.1:6379") 270 | if error != nil { 271 | fmt.Println("Error connecting to redis", error) 272 | os.Exit(1) 273 | } 274 | 275 | httpDone := make(chan int) 276 | go doProxy(httpDone, 80, handleHTTPConnection, redisClient) 277 | 278 | httpsDone := make(chan int) 279 | go doProxy(httpsDone, 443, handleHTTPSConnection, redisClient) 280 | 281 | <-httpDone 282 | <-httpsDone 283 | } 284 | --------------------------------------------------------------------------------