diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6b45014 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/go-rvpn-server +/m +/debug + + diff --git a/README.md b/README.md index 74625a2..af9ef55 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,241 @@ # RVPN Server +## branch: passing-traffic + +- code now passes traffic using just daplie tools +- this will require serve-https and node-tunnel-client to work + + +### Build RVPN + +```bash +hcamacho@Hanks-MBP:go-rvpn-server $ go get +hcamacho@Hanks-MBP:go-rvpn-server $ go build +``` +### Setup Some Entries + +```bash +127.0.0.1 tunnel.example.com rvpn.daplie.invalid hfc2.daplie.me hfc.daplie.me +``` + +### Start Up Webserver +```bash +hcamacho@Hanks-MBP:tmp $ cd /tmp +hcamacho@Hanks-MBP:tmp $ vi index.html --- Place some index content +hcamacho@Hanks-MBP:tmp $ serve-https -p 8080 -d /tmp --servername hfc.daplie.me --agree-tos --email henry.f.camacho@gmail.com +``` + +### Start Tunnel Client +```bash +hcamacho@Hanks-MBP:node-tunnel-client $ bin/stunnel.js --locals http://hfc.daplie.me:8080,http://test1.hfc.daplie.me:8080 --stunneld wss://localhost.daplie.me:8443 --secret abc123 +``` + +### Execute RVPN + +```bash +hcamacho@Hanks-MBP:go-rvpn-server $ ./go-rvpn-server +INFO: packer: 2017/03/02 19:16:52.652109 run.go:47: startup +-=-=-=-=-=-=-=-=-=-= +INFO: genericlistener: 2017/03/02 19:16:52.652777 manager.go:77: ConnectionTable starting +INFO: genericlistener: 2017/03/02 19:16:52.652806 connection_table.go:67: ConnectionTable starting +INFO: genericlistener: 2017/03/02 19:16:52.652826 manager.go:84: &{map[] 0xc420072420 0xc420072480} +INFO: genericlistener: 2017/03/02 19:16:52.652832 connection_table.go:50: Reaper waiting for 300 seconds +INFO: genericlistener: 2017/03/02 19:16:52.652856 manager.go:100: register fired 8443 +INFO: genericlistener: 2017/03/02 19:16:52.652862 manager.go:110: listener starting up 8443 +INFO: genericlistener: 2017/03/02 19:16:52.652868 manager.go:111: &{map[] 0xc420072420 0xc420072480} +INFO: genericlistener: 2017/03/02 19:16:52.652869 conn_tracking.go:25: Tracking Running +``` + +### Browse via tunnel + +https://hfc.daplie.me:8443 + +- You'll notice that the browser is redirected to 8080 after accepting the cert. I see a meta-refresh coming back from the serve-https +- The traffic is getting back to the client. + +```bash + +INFO: genericlistener: 2017/03/02 21:24:48.472312 connection.go:207: 00000000 fe 1d 49 50 76 34 2c 31 32 37 2e 30 2e 30 2e 31 |..IPv4,127.0.0.1| +00000010 2c 35 33 35 35 39 2c 33 36 38 2c 68 74 74 70 48 |,53559,368,httpH| +00000020 54 54 50 2f 31 2e 31 20 32 30 30 20 4f 4b 0d 0a |TTP/1.1 200 OK..| +00000030 43 6f 6e 74 65 6e 74 2d 54 79 70 65 3a 20 74 65 |Content-Type: te| +00000040 78 74 2f 68 74 6d 6c 3b 20 63 68 61 72 73 65 74 |xt/html; charset| +00000050 3d 75 74 66 2d 38 0d 0a 44 61 74 65 3a 20 46 72 |=utf-8..Date: Fr| +00000060 69 2c 20 30 33 20 4d 61 72 20 32 30 31 37 20 30 |i, 03 Mar 2017 0| +00000070 33 3a 32 34 3a 34 38 20 47 4d 54 0d 0a 43 6f 6e |3:24:48 GMT..Con| +00000080 6e 65 63 74 69 6f 6e 3a 20 6b 65 65 70 2d 61 6c |nection: keep-al| +00000090 69 76 65 0d 0a 43 6f 6e 74 65 6e 74 2d 4c 65 6e |ive..Content-Len| +000000a0 67 74 68 3a 20 32 32 37 0d 0a 0d 0a 3c 68 74 6d |gth: 227...... ....<| +00000180 2f 62 6f 64 79 3e 0a 3c 2f 68 74 6d 6c 3e 0a |/body>..| + +``` + +- this set of code works great if I am running the node-tunnel-client on a different machine with apache as a web server. +- need to work through why serve-https thinks the traffic is inecure. + + + + + + + + + + + + + + + + + + + + +## restructured-http + +- connection handling has been totally rewritten. +- on a specific port RVPN can determine the following: + - if a connection is encrypted or not encrypted + - if a request is a wss_client + - if a request is an admin/api request + - if a request is a plain (to be forwarded) http request + - or if a request is a different protocol (perhaps SSH) + +To accomplish the above RVPN uses raw TCP sockets, buffered readers, and a custom Listener. This allows protocol detection (multiple services on one port) +If we expose 443 and 80 to maximize the ability for tunnel clients and south bound traffic, the RVPN is able to deal with this traffic on a limited number of ports, and the most popular ports. +It is possible now to meter any point of the connection (not Interface Level, rather TCP) + +There is now a connection manager that dynamically allows new GenericListeners to start on different ports when needed.... + +```go + newListener := NewListenerRegistration(initialPort) + gl.register <- newListener +``` + +A new listener is created by sending a NewListenerRegistration on the channel. + +```go + + ln, err := net.ListenTCP("tcp", listenAddr) + if err != nil { + loginfo.Println("unable to bind", err) + listenerRegistration.status = listenerFault + listenerRegistration.err = err + listenerRegistration.commCh <- listenerRegistration + return + } + + listenerRegistration.status = listenerAdded + listenerRegistration.commCh <- listenerRegistration + +``` + +Once the lister is fired up, I sends back a regisration status to the manager along with the new Listener and status. + + +### Build + +```bash + +hcamacho@Hanks-MBP:go-rvpn-server $ go get +hcamacho@Hanks-MBP:go-rvpn-server $ go build + +``` + +### Execute RVPN + +```bash + +hcamacho@Hanks-MBP:go-rvpn-server $ ./go-rvpn-server +INFO: packer: 2017/02/26 12:43:53.978133 run.go:48: startup +-=-=-=-=-=-=-=-=-=-= +INFO: connection: 2017/02/26 12:43:53.978959 connection_table.go:67: ConnectionTable starting +INFO: connection: 2017/02/26 12:43:53.979000 connection_table.go:50: Reaper waiting for 300 seconds + + +``` + +### Connect Tunnel client + +```bash + +hcamacho@Hanks-MBP:node-tunnel-client $ bin/stunnel.js --locals http://hfc.daplie.me:8443,http://test.hfc.daplie.me:3001,http://127.0.0.1:8080 --stunneld wss://localhost.daplie.me:8443 --secret abc123 +[local proxy] http://hfc.daplie.me:8443 +[local proxy] http://test.hfc.daplie.me:3001 +[local proxy] http://127.0.0.1:8080 +[connect] 'wss://localhost.daplie.me:8443' +[open] connected to 'wss://localhost.daplie.me:8443' + +``` + +### Connect Admin + +- add a host entry + +``` + +127.0.0.1 tunnel.example.com rvpn.daplie.invalid + +``` + +```bash +browse https://rvpn.daplie.invalid:8443 + +``` + +### Send some traffic + +- run the RVPN (as above) +- run the tunnel client (as above) +- browse http://127.0.0.1:8443 && https://127.0.0.1:8443 +- observe + +```bash + +hcamacho@Hanks-MBP:node-tunnel-client $ bin/stunnel.js --locals http://hfc.daplie.me:8443,http://test.hfc.daplie.me:3001,http://127.0.0.1:8080 --stunneld wss://localhost.daplie.me:8443 --secret abc123 +[local proxy] http://hfc.daplie.me:8443 +[local proxy] http://test.hfc.daplie.me:3001 +[local proxy] http://127.0.0.1:8080 +[connect] 'wss://localhost.daplie.me:8443' +[open] connected to 'wss://localhost.daplie.me:8443' +hello +fe1c495076342c3132372e302e302e312c383038302c3431332c68747470474554202f20485454502f312e310d0a486f73743a203132372e302e302e313a383434330d0a436f6e6e656374696f6e3a206b6565702d616c6976650d0a43616368652d436f6e74726f6c3a206d61782d6167653d300d0a557067726164652d496e7365637572652d52657175657374733a20310d0a557365722d4167656e743a204d6f7a696c6c612f352e3020284d6163696e746f73683b20496e74656c204d6163204f5320582031305f31325f3329204170706c655765624b69742f3533372e333620284b48544d4c2c206c696b65204765636b6f29204368726f6d652f35362e302e323932342e3837205361666172692f3533372e33360d0a4163636570743a20746578742f68746d6c2c6170706c69636174696f6e2f7868746d6c2b786d6c2c6170706c69636174696f6e2f786d6c3b713d302e392c696d6167652f776562702c2a2f2a3b713d302e380d0a4163636570742d456e636f64696e673a20677a69702c206465666c6174652c20736463682c2062720d0a4163636570742d4c616e67756167653a20656e2d55532c656e3b713d302e380d0a0d0a +�IPv4,127.0.0.1,8080,413,httpGET / HTTP/1.1 +Host: 127.0.0.1:8443 +Connection: keep-alive +Cache-Control: max-age=0 +Upgrade-Insecure-Requests: 1 +User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36 +Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 +Accept-Encoding: gzip, deflate, sdch, br +Accept-Language: en-US,en;q=0.8 + + +[exit] loop closed 0 + +``` + +Looks like it aborts for some reaon. I have this problem on on a new installation as well. + + + + +-=-=-=-=-=-= + A Poor Man's Reverse VPN written in Go Context diff --git a/admin/html/index.html b/admin/html/index.html new file mode 100644 index 0000000..d9cf2b7 --- /dev/null +++ b/admin/html/index.html @@ -0,0 +1 @@ +Hank diff --git a/admin/static/test.html b/admin/static/test.html new file mode 100644 index 0000000..896227b --- /dev/null +++ b/admin/static/test.html @@ -0,0 +1 @@ +Test.html diff --git a/d b/d new file mode 100755 index 0000000..dd7956a --- /dev/null +++ b/d @@ -0,0 +1,4 @@ +godebug build -instrument git.daplie.com/Daplie/go-rvpn-server/rvpn/connection,\ +git.daplie.com/Daplie/go-rvpn-server/rvpn/connection \ +-o debug main.go + diff --git a/go-rvpn-server.yaml b/go-rvpn-server.yaml new file mode 100644 index 0000000..d0f88e1 --- /dev/null +++ b/go-rvpn-server.yaml @@ -0,0 +1,16 @@ +rvpn: + wssdomain: localhost.daplie.me + admindomain: rvpn.daplie.invalid + genericlistener: 9999 + deadtime: + dwell: 600 + idle: 60 + cancelcheck: 10 + domains: + test.daplie.me: + secret: abc123 + test2.daplie.me: + secret: abc123 + test3.daplie.me: + secret: abc123 + diff --git a/html/admin.html b/html/admin.html new file mode 100755 index 0000000..3f376e7 --- /dev/null +++ b/html/admin.html @@ -0,0 +1,110 @@ + + + +Websock VPN Instrumentation + + + +
+
VPN Instrumentation
+
+
+
Control Plane
+
+ +
+
+
+ + + + +
+
+
+ +
+ +
+
+
+ + + + +
+
+
+
+
+
+
Data
+
+

{[{text}]}

+
+
+
+
+ + + + + + + + + + + \ No newline at end of file diff --git a/html/client.html b/html/client.html new file mode 100755 index 0000000..cfbdc39 --- /dev/null +++ b/html/client.html @@ -0,0 +1,132 @@ + + + +Websock VPN Test Client + + + +
+
WebSocket Client Test
+
+
+
Control Plane
+
+ +
+
+
+ + + + +
+
+
+ +
+ +
+
+
+ + + + +
+
+
+ +
+ +
+
+
+ + + + +
+
+
+ + +
+
+
+
Messages
+
+

{[{text}]}

+
+
+
+
+ + + + + + + + + + + diff --git a/main.go b/main.go new file mode 100644 index 0000000..21ca4b9 --- /dev/null +++ b/main.go @@ -0,0 +1,100 @@ +package main + +import ( + "crypto/tls" + "flag" + "fmt" + "log" + "os" + "time" + + "github.com/spf13/viper" + + "context" + + "git.daplie.com/Daplie/go-rvpn-server/rvpn/genericlistener" +) + +var ( + loginfo *log.Logger + logdebug *log.Logger + logFlags = log.Ldate | log.Lmicroseconds | log.Lshortfile + argWssClientListener string + argGenericBinding int + argServerBinding string + argServerAdminBinding string + argServerExternalBinding string + argDeadTime int + connectionTable *genericlistener.Table + secretKey = "abc123" + wssHostName = "localhost.daplie.me" + adminHostName = "rvpn.daplie.invalid" + idle int + dwell int + cancelcheck int +) + +func init() { + +} + +//Main -- main entry point +func main() { + flag.Parse() + loginfo = log.New(os.Stdout, "INFO: main: ", logFlags) + logdebug = log.New(os.Stdout, "DEBUG: main:", logFlags) + viper.SetConfigName("go-rvpn-server") + viper.AddConfigPath("./") + err := viper.ReadInConfig() + if err != nil { + panic(fmt.Errorf("Fatal error config file: %s \n", err)) + } + + flag.IntVar(&argDeadTime, "dead-time-counter", 5, "deadtime counter in seconds") + + wssHostName = viper.Get("rvpn.wssdomain").(string) + adminHostName = viper.Get("rvpn.admindomain").(string) + argGenericBinding = viper.GetInt("rvpn.genericlistener") + deadtime := viper.Get("rvpn.deadtime") + idle = deadtime.(map[string]interface{})["idle"].(int) + dwell = deadtime.(map[string]interface{})["dwell"].(int) + cancelcheck = deadtime.(map[string]interface{})["cancelcheck"].(int) + + loginfo.Println("startup") + + loginfo.Println(viper.Get("rvpn.genericlisteners")) + loginfo.Println(viper.Get("rvpn.domains")) + + fmt.Println("-=-=-=-=-=-=-=-=-=-=") + + certbundle, err := tls.LoadX509KeyPair("certs/fullchain.pem", "certs/privkey.pem") + if err != nil { + loginfo.Println(err) + return + } + + ctx, cancelContext := context.WithCancel(context.Background()) + defer cancelContext() + + // Setup for GenericListenServe. + // - establish context for the generic listener + // - startup listener + // - accept with peek buffer. + // - peek at the 1st 30 bytes. + // - check for tls + // - if tls, establish, protocol peek buffer, else decrypted + // - match protocol + + connectionTracking := genericlistener.NewTracking() + go connectionTracking.Run(ctx) + + connectionTable = genericlistener.NewTable(dwell, idle) + go connectionTable.Run(ctx) + + genericListeners := genericlistener.NewGenerListeners(ctx, connectionTable, connectionTracking, secretKey, certbundle, wssHostName, adminHostName, cancelcheck) + go genericListeners.Run(ctx, argGenericBinding) + + //Run for 10 minutes and then shutdown cleanly + time.Sleep(600 * time.Second) + cancelContext() +} diff --git a/rvpn-docker/docker-compose.yml b/rvpn-docker/docker-compose.yml new file mode 100644 index 0000000..1dbc5ec --- /dev/null +++ b/rvpn-docker/docker-compose.yml @@ -0,0 +1,15 @@ +version: '2.0' +services: + rvpn: + build: rvpn/. + ports: + - "8443:8443" + entrypoint: + - ./start.sh + expose: + - "8843" + volumes: + - $GOPATH/:/go + - ../:/go-rvpn-server + + diff --git a/rvpn-docker/readme.md b/rvpn-docker/readme.md new file mode 100644 index 0000000..11b4f7e --- /dev/null +++ b/rvpn-docker/readme.md @@ -0,0 +1,92 @@ +# Docker Deployment for RVPN + +- install docker 1.13, or the latest stable CE release (Testing on MAC using 17.03.0-ce-mac1 (15583)) +- validate installation + +```bash +hcamacho@Hanks-MBP:rvpn-docker $ docker-compose --version +docker-compose version 1.11.2, build dfed245 + +hcamacho@Hanks-MBP:rvpn-docker $ docker --version +Docker version 17.03.0-ce, build 60ccb22 +``` +- checkout code into gopath + +```bash +cd $GOPATH/src/git.daplie.com/Daplie +git clone git@git.daplie.com:Daplie/go-rvpn-server.git + +cd go-rvpn-server +go get + +``` + +## Execute Container Deployment + +- prep + +```bash +cd rvpn-docker +hcamacho@Hanks-MBP:rvpn-docker $ docker-compose build +Building rvpn +Step 1/3 : FROM golang:1.7.5 + ---> 5cfb16b630ef +Step 2/3 : LABEL maintainer "henry.f.camacho@gmail.com" + ---> Running in 5cdffef8e33d + ---> f7e09c097612 +Removing intermediate container 5cdffef8e33d +Step 3/3 : WORKDIR "/go-rvpn-server" + ---> 182aa9c814f2 +Removing intermediate container f136550d6d48 +Successfully built 182aa9c814f2 + +``` + +- execute container + +```bash +hcamacho@Hanks-MBP:rvpn-docker $ docker-compose up +Creating network "rvpndocker_default" with the default driver +Creating rvpndocker_rvpn_1 +Attaching to rvpndocker_rvpn_1 +rvpn_1 | INFO: packer: 2017/03/04 18:13:00.994955 main.go:47: startup +rvpn_1 | -=-=-=-=-=-=-=-=-=-= +rvpn_1 | INFO: genericlistener: 2017/03/04 18:13:01.000063 conn_tracking.go:25: Tracking Running +rvpn_1 | INFO: genericlistener: 2017/03/04 18:13:01.000067 connection_table.go:67: ConnectionTable starting +rvpn_1 | INFO: genericlistener: 2017/03/04 18:13:01.000214 connection_table.go:50: Reaper waiting for 300 seconds +rvpn_1 | INFO: genericlistener: 2017/03/04 18:13:00.999757 manager.go:77: ConnectionTable starting +rvpn_1 | INFO: genericlistener: 2017/03/04 18:13:01.000453 manager.go:84: &{map[] 0xc4200124e0 0xc420012540} +rvpn_1 | INFO: genericlistener: 2017/03/04 18:13:01.000505 manager.go:100: register fired 8443 +rvpn_1 | INFO: genericlistener: 2017/03/04 18:13:01.000613 manager.go:110: listener starting up 8443 +rvpn_1 | INFO: genericlistener: 2017/03/04 18:13:01.000638 manager.go:111: &{map[] 0xc4200124e0 0xc420012540} +rvpn_1 | INFO: genericlistener: 2017/03/04 18:13:01.000696 listener_generic.go:55: :⃻ +rvpn_1 | INFO: genericlistener: 2017/03/04 18:13:02.242287 listener_generic.go:87: Deadtime reached +rvpn_1 | INFO: genericlistener: 2017/03/04 18:13:02.242596 listener_generic.go:114: conn &{0xc420120000 0xc42011e000} 172.18.0.2:8443 172.18.0.1:38148 +rvpn_1 | INFO: genericlistener: 2017/03/04 18:13:02.242627 listener_generic.go:131: TLS10 +rvpn_1 | INFO: genericlistener: 2017/03/04 18:13:02.242641 listener_generic.go:148: Handle Encryption +rvpn_1 | INFO: genericlistener: 2017/03/04 18:13:02.242654 one_conn.go:22: Accept 172.18.0.2:8443 172.18.0.1:38148 +rvpn_1 | INFO: genericlistener: 2017/03/04 18:13:02.242699 listener_generic.go:177: handle Stream +rvpn_1 | INFO: genericlistener: 2017/03/04 18:13:02.242722 listener_generic.go:178: conn &{0xc420120060 0xc420126000} 172.18.0.2:8443 172.18.0.1:38148 +rvpn_1 | INFO: genericlistener: 2017/03/04 18:13:02.266803 listener_generic.go:191: identifed HTTP +rvpn_1 | INFO: genericlistener: 2017/03/04 18:13:02.267797 listener_generic.go:207: Valid WSS dected...sending to handler +rvpn_1 | INFO: genericlistener: 2017/03/04 18:13:02.267926 one_conn.go:32: addr +rvpn_1 | INFO: genericlistener: 2017/03/04 18:13:02.267947 one_conn.go:22: Accept 172.18.0.2:8443 172.18.0.1:38148 +rvpn_1 | INFO: genericlistener: 2017/03/04 18:13:02.268045 one_conn.go:17: Accept +rvpn_1 | INFO: genericlistener: 2017/03/04 18:13:02.268062 one_conn.go:27: close +rvpn_1 | INFO: genericlistener: 2017/03/04 18:13:02.268066 listener_generic.go:421: Serve error: EOF +rvpn_1 | INFO: genericlistener: 2017/03/04 18:13:02.268707 listener_generic.go:366: HandleFunc / +rvpn_1 | INFO: genericlistener: 2017/03/04 18:13:02.268727 listener_generic.go:369: websocket opening 172.18.0.1:38148 localhost.daplie.me:8443 +rvpn_1 | INFO: genericlistener: 2017/03/04 18:13:02.269264 listener_generic.go:397: before connection table +rvpn_1 | INFO: genericlistener: 2017/03/04 18:13:02.269321 connection_table.go:79: register fired +rvpn_1 | INFO: genericlistener: 2017/03/04 18:13:02.269523 connection_table.go:90: adding domain hfc.daplie.me to connection 172.18.0.1:38148 +rvpn_1 | INFO: genericlistener: 2017/03/04 18:13:02.269602 connection_table.go:90: adding domain test1.hfc.daplie.me to connection 172.18.0.1:38148 +rvpn_1 | INFO: genericlistener: 2017/03/04 18:13:02.269821 listener_generic.go:410: connection registration accepted &{0xc42012af00 172.18.0.1:38148 0xc420120ea0 [hfc.daplie.me test1.hfc.daplie.me] 0xc4200f49c0} +rvpn_1 | INFO: genericlistener: 2017/03/04 18:13:02.270168 connection.go:200: Reader Start &{0xc420104990 0xc420077560 map[hfc.daplie.me:0xc4201ee7a0 test1.hfc.daplie.me:0xc4201ee7c0] 0xc42012af00 0xc420120f00 172.18.0.1:38148 0 0 {63624247982 269492963 0x8392a0} {0 0 } [hfc.daplie.me test1.hfc.daplie.me] 0xc4200f49c0 true} +rvpn_1 | INFO: genericlistener: 2017/03/04 18:13:02.270281 connection.go:242: Writer Start &{0xc420104990 0xc420077560 map[hfc.daplie.me:0xc4201ee7a0 test1.hfc.daplie.me:0xc4201ee7c0] 0xc42012af00 0xc420120f00 172.18.0.1:38148 0 0 {63624247982 269492963 0x8392a0} {0 0 } [hfc.daplie.me test1.hfc.daplie.me] 0xc4200f49c0 true} + +``` + +The line "Connection Registration Accepted indicates a client WSS registered, was authenticated and registered its domains with the RVPN + + + diff --git a/rvpn-docker/rvpn/Dockerfile b/rvpn-docker/rvpn/Dockerfile new file mode 100644 index 0000000..839adf8 --- /dev/null +++ b/rvpn-docker/rvpn/Dockerfile @@ -0,0 +1,5 @@ +FROM golang:1.7.5 +LABEL maintainer "henry.f.camacho@gmail.com" + + +WORKDIR "/go-rvpn-server" diff --git a/rvpn-docker/rvpn/run.sh b/rvpn-docker/rvpn/run.sh new file mode 100755 index 0000000..3c14662 --- /dev/null +++ b/rvpn-docker/rvpn/run.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +docker-compose run --entrypoint "/bin/bash" rvpn + diff --git a/rvpn/genericlistener/conn_tracking.go b/rvpn/genericlistener/conn_tracking.go new file mode 100644 index 0000000..87e129c --- /dev/null +++ b/rvpn/genericlistener/conn_tracking.go @@ -0,0 +1,81 @@ +package genericlistener + +import "net" +import "context" +import "fmt" + +//Track -- used to track connection + domain +type Track struct { + conn net.Conn + domain string +} + +//NewTrack -- Constructor +func NewTrack(conn net.Conn, domain string) (p *Track) { + p = new(Track) + p.conn = conn + p.domain = domain + return +} + +//Tracking -- +type Tracking struct { + connections map[string]*Track + register chan *Track + unregister chan net.Conn +} + +//NewTracking -- Constructor +func NewTracking() (p *Tracking) { + p = new(Tracking) + p.connections = make(map[string]*Track) + p.register = make(chan *Track) + p.unregister = make(chan net.Conn) + return +} + +//Run - +func (p *Tracking) Run(ctx context.Context) { + loginfo.Println("Tracking Running") + + for { + select { + + case <-ctx.Done(): + loginfo.Println("Cancel signal hit") + return + + case connection := <-p.register: + key := connection.conn.RemoteAddr().String() + loginfo.Println("register fired", key) + p.connections[key] = connection + p.list() + + case connection := <-p.unregister: + key := connection.RemoteAddr().String() + loginfo.Println("unregister fired", key) + if _, ok := p.connections[key]; ok { + delete(p.connections, key) + } + p.list() + } + } +} + +func (p *Tracking) list() { + for c := range p.connections { + loginfo.Println(c) + } +} + +//Lookup -- +// - get connection from key +func (p *Tracking) Lookup(key string) (c *Track, err error) { + if _, ok := p.connections[key]; ok { + c = p.connections[key] + } else { + err = fmt.Errorf("Lookup failed for %s", key) + c = nil + } + return +} diff --git a/rvpn/genericlistener/conn_wedge.go b/rvpn/genericlistener/conn_wedge.go new file mode 100644 index 0000000..579db3c --- /dev/null +++ b/rvpn/genericlistener/conn_wedge.go @@ -0,0 +1,68 @@ +package genericlistener + +import ( + "bufio" + "net" +) + +//WedgeConn -- A buffered IO infront of a connection allowing peeking, and switching connections. +type WedgeConn struct { + reader *bufio.Reader + net.Conn +} + +//NewWedgeConn -- Constructor +func NewWedgeConn(c net.Conn) (p *WedgeConn) { + p = new(WedgeConn) + p.reader = bufio.NewReader(c) + p.Conn = c + return +} + +//NewWedgeConnSize -- Constructor +func NewWedgeConnSize(c net.Conn, size int) (p *WedgeConn) { + p = new(WedgeConn) + p.reader = bufio.NewReaderSize(c, size) + p.Conn = c + return +} + +//Discard - discard a number of bytes, perhaps after peeking at the +func (w *WedgeConn) Discard(n int) (discarded int, err error) { + return w.reader.Discard(n) +} + +//Peek - Get a number of bytes outof the buffer, but allow the buffer to be replayed once read +func (w *WedgeConn) Peek(n int) ([]byte, error) { + return w.reader.Peek(n) +} + +//ReadByte -- A normal reader. +func (w *WedgeConn) ReadByte() (byte, error) { + return w.reader.ReadByte() +} + +//Read -- A normal reader. +func (w *WedgeConn) Read(p []byte) (int, error) { + cnt, err := w.reader.Read(p) + return cnt, err +} + +//Buffered -- +func (w *WedgeConn) Buffered() int { + return w.reader.Buffered() +} + +//PeekAll -- +// - get all the chars available +// - pass then back +func (w *WedgeConn) PeekAll() (buf []byte, err error) { + + _, err = w.Peek(1) + if err != nil { + return nil, err + } + + buf, err = w.Peek(w.Buffered()) + return +} diff --git a/rvpn/genericlistener/connection.go b/rvpn/genericlistener/connection.go new file mode 100755 index 0000000..6112e28 --- /dev/null +++ b/rvpn/genericlistener/connection.go @@ -0,0 +1,285 @@ +package genericlistener + +import ( + "strconv" + "time" + + "git.daplie.com/Daplie/go-rvpn-server/rvpn/packer" + + "sync" + + "io" + + "context" + + "encoding/hex" + + "github.com/gorilla/websocket" +) + +var upgrader = websocket.Upgrader{ + ReadBufferSize: 4096, + WriteBufferSize: 4096, +} + +// Connection track websocket and faciliates in and out data +type Connection struct { + mutex *sync.Mutex + + // The main connection table (should be just one of these created at startup) + connectionTable *Table + + //used to track traffic for a domain. Not use for lookup or validation only for tracking + DomainTrack map[string]*DomainTrack + + // The websocket connection. + conn *websocket.Conn + + // Buffered channel of outbound messages. + send chan *SendTrack + + // WssState channel + // Must check state via channel before xmit + + // Address of the Remote End Point + source string + + // bytes in + bytesIn int64 + + // bytes out + bytesOut int64 + + // Connect Time + connectTime time.Time + + //lastUpdate + lastUpdate time.Time + + //initialDomains - a list of domains from the JWT + initialDomains []interface{} + + connectionTrack *Tracking + + ///wssState tracks a highlevel status of the connection, false means do nothing. + wssState bool +} + +//NewConnection -- Constructor +func NewConnection(connectionTable *Table, conn *websocket.Conn, remoteAddress string, initialDomains []interface{}, connectionTrack *Tracking) (p *Connection) { + p = new(Connection) + p.mutex = &sync.Mutex{} + p.connectionTable = connectionTable + p.conn = conn + p.source = remoteAddress + p.bytesIn = 0 + p.bytesOut = 0 + p.send = make(chan *SendTrack) + p.connectTime = time.Now() + p.initialDomains = initialDomains + p.connectionTrack = connectionTrack + p.DomainTrack = make(map[string]*DomainTrack) + + for _, domain := range initialDomains { + p.AddTrackedDomain(string(domain.(string))) + } + + p.State(true) + return +} + +//AddTrackedDomain -- Add a tracked domain +func (c *Connection) AddTrackedDomain(domain string) { + p := new(DomainTrack) + p.DomainName = domain + c.DomainTrack[domain] = p +} + +//InitialDomains -- Property +func (c *Connection) InitialDomains() (i []interface{}) { + i = c.initialDomains + return +} + +//ConnectTime -- Property +func (c *Connection) ConnectTime() (t time.Time) { + t = c.connectTime + return +} + +//BytesIn -- Property +func (c *Connection) BytesIn() (b int64) { + b = c.bytesIn + return +} + +//BytesOut -- Property +func (c *Connection) BytesOut() (b int64) { + b = c.bytesOut + return +} + +//SendCh -- property to sending channel +func (c *Connection) SendCh() chan *SendTrack { + return c.send +} + +func (c *Connection) addIn(num int64) { + c.bytesIn = c.bytesIn + num +} + +func (c *Connection) addOut(num int64) { + c.bytesOut = c.bytesOut + num +} + +//ConnectionTable -- property +func (c *Connection) ConnectionTable() (table *Table) { + table = c.connectionTable + return +} + +//GetState -- Get state of Socket...this is a high level state. +func (c *Connection) GetState() bool { + defer func() { + c.mutex.Unlock() + }() + c.mutex.Lock() + return c.wssState +} + +//State -- Set the set of the high level connection +func (c *Connection) State(state bool) { + defer func() { + c.mutex.Unlock() + }() + + c.mutex.Lock() + c.wssState = state +} + +//Update -- updates the lastUpdate property tracking idle time +func (c *Connection) Update() { + defer func() { + c.mutex.Unlock() + }() + + c.mutex.Lock() + c.lastUpdate = time.Now() +} + +//NextWriter -- Wrapper to allow a high level state check before offering NextWriter +//The libary failes if client abends during write-cycle. a fast moving write is not caught before socket state bubbles up +//A synchronised state is maintained +func (c Connection) NextWriter(wssMessageType int) (w io.WriteCloser, err error) { + if c.GetState() == true { + w, err = c.conn.NextWriter(wssMessageType) + } else { + loginfo.Println("NextWriter aborted, state is not true") + } + return +} + +//Write -- Wrapper to allow a high level state check before allowing a write to the socket. +func (c *Connection) Write(w io.WriteCloser, message []byte) (cnt int, err error) { + if c.GetState() == true { + cnt, err = w.Write(message) + } + return +} + +//Reader -- export the reader function +func (c *Connection) Reader(ctx context.Context) { + connectionTrack := c.connectionTrack + + defer func() { + c.connectionTable.unregister <- c + c.conn.Close() + loginfo.Println("reader defer", c) + }() + + loginfo.Println("Reader Start ", c) + + c.conn.SetReadLimit(65535) + for { + msgType, message, err := c.conn.ReadMessage() + + loginfo.Println("ReadMessage", msgType, err) + + c.Update() + + if err != nil { + if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway) { + c.State(false) + loginfo.Printf("error: %v", err) + } + break + } + + // unpack the message. + p, err := packer.ReadMessage(message) + key := p.Header.Address().String() + ":" + strconv.Itoa(p.Header.Port) + track, err := connectionTrack.Lookup(key) + + loginfo.Println(hex.Dump(p.Data.Data())) + + if err != nil { + loginfo.Println("Unable to locate Tracking for ", key) + continue + } + + //Support for tracking outbound traffic based on domain. + if domainTrack, ok := c.DomainTrack[track.domain]; ok { + //if ok then add to structure, else warn there is something wrong + domainTrack.AddIn(int64(len(message))) + } + + track.conn.Write(p.Data.Data()) + + c.addIn(int64(len(message))) + loginfo.Println("end of read") + } +} + +//Writer -- expoer the writer function +func (c *Connection) Writer() { + defer func() { + c.conn.Close() + }() + + loginfo.Println("Writer Start ", c) + + for { + select { + + case message := <-c.send: + w, err := c.NextWriter(websocket.BinaryMessage) + loginfo.Println("next writer ", w) + if err != nil { + return + } + + c.Update() + + _, err = c.Write(w, message.data) + + if err := w.Close(); err != nil { + return + } + + messageLen := int64(len(message.data)) + + c.addOut(messageLen) + + //Support for tracking outbound traffic based on domain. + if domainTrack, ok := c.DomainTrack[message.domain]; ok { + //if ok then add to structure, else warn there is something wrong + domainTrack.AddOut(messageLen) + loginfo.Println("adding ", messageLen, " to ", message.domain) + } else { + logdebug.Println("attempting to add bytes to ", message.domain, "it does not exist") + logdebug.Println(c.DomainTrack) + } + loginfo.Println(c) + } + } +} diff --git a/rvpn/genericlistener/connection_registration.go b/rvpn/genericlistener/connection_registration.go new file mode 100644 index 0000000..974874c --- /dev/null +++ b/rvpn/genericlistener/connection_registration.go @@ -0,0 +1,38 @@ +package genericlistener + +import "github.com/gorilla/websocket" + +//Registration -- A connection registration structure used to bring up a connection +//connection table will then handle additing and sdtarting up the various readers +//else error. +type Registration struct { + // The websocket connection. + conn *websocket.Conn + + // Address of the Remote End Point + source string + + // communications channel between go routines + commCh chan bool + + //initialDomains - a list of domains from the JWT + initialDomains []interface{} + + connectionTrack *Tracking +} + +//NewRegistration -- Constructor +func NewRegistration(conn *websocket.Conn, remoteAddress string, initialDomains []interface{}, connectionTrack *Tracking) (p *Registration) { + p = new(Registration) + p.conn = conn + p.source = remoteAddress + p.commCh = make(chan bool) + p.initialDomains = initialDomains + p.connectionTrack = connectionTrack + return +} + +//CommCh -- Property +func (c *Registration) CommCh() chan bool { + return c.commCh +} diff --git a/rvpn/genericlistener/connection_table.go b/rvpn/genericlistener/connection_table.go new file mode 100755 index 0000000..ff57042 --- /dev/null +++ b/rvpn/genericlistener/connection_table.go @@ -0,0 +1,135 @@ +package genericlistener + +import "fmt" +import "time" +import "context" + +const ( + initialDomains = 0 + incrementDomains = 0 +) + +//Table maintains the set of connections +type Table struct { + connections map[*Connection][]string + domains map[string]*Connection + register chan *Registration + unregister chan *Connection + domainAnnounce chan *DomainMapping + domainRevoke chan *DomainMapping + dwell int + idle int +} + +//NewTable -- consructor +func NewTable(dwell int, idle int) (p *Table) { + p = new(Table) + p.connections = make(map[*Connection][]string) + p.domains = make(map[string]*Connection) + p.register = make(chan *Registration) + p.unregister = make(chan *Connection) + p.domainAnnounce = make(chan *DomainMapping) + p.domainRevoke = make(chan *DomainMapping) + p.dwell = dwell + p.idle = idle + return +} + +//Connections Property +func (c *Table) Connections() (table map[*Connection][]string) { + table = c.connections + return +} + +//ConnByDomain -- Obtains a connection from a domain announcement. +func (c *Table) ConnByDomain(domain string) (conn *Connection, ok bool) { + conn, ok = c.domains[domain] + return +} + +//reaper -- +func (c *Table) reaper(delay int, idle int) { + _ = "breakpoint" + for { + loginfo.Println("Reaper waiting for ", delay, " seconds") + time.Sleep(time.Duration(delay) * time.Second) + + loginfo.Println("Running scanning ", len(c.connections)) + for d := range c.connections { + if d.GetState() == false { + if time.Since(d.lastUpdate).Seconds() > float64(idle) { + loginfo.Println("reaper removing ", d.lastUpdate, time.Since(d.lastUpdate).Seconds()) + delete(c.connections, d) + } + } + } + } +} + +//Run -- Execute +func (c *Table) Run(ctx context.Context) { + loginfo.Println("ConnectionTable starting") + + go c.reaper(c.dwell, c.idle) + + for { + select { + + case <-ctx.Done(): + loginfo.Println("Cancel signal hit") + return + + case registration := <-c.register: + loginfo.Println("register fired") + + connection := NewConnection(c, registration.conn, registration.source, registration.initialDomains, registration.connectionTrack) + c.connections[connection] = make([]string, initialDomains) + registration.commCh <- true + + // handle initial domain additions + for _, domain := range connection.initialDomains { + // add to the domains regirstation + + newDomain := string(domain.(string)) + loginfo.Println("adding domain ", newDomain, " to connection ", connection.conn.RemoteAddr().String()) + c.domains[newDomain] = connection + + // add to the connection domain list + s := c.connections[connection] + c.connections[connection] = append(s, newDomain) + } + go connection.Writer() + go connection.Reader(ctx) + + case connection := <-c.unregister: + loginfo.Println("closing connection ", connection.conn.RemoteAddr().String()) + if _, ok := c.connections[connection]; ok { + for _, domain := range c.connections[connection] { + fmt.Println("removing domain ", domain) + if _, ok := c.domains[domain]; ok { + delete(c.domains, domain) + } + } + + //delete(c.connections, connection) + //close(connection.send) + } + + case domainMapping := <-c.domainAnnounce: + loginfo.Println("domainMapping fired ", domainMapping) + //check to make sure connection is already regiered, you can no register a domain without an apporved connection + //if connection, ok := connections[domainMapping.connection]; ok { + + //} else { + + //} + + } + } +} + +//Register -- Property +func (c *Table) Register() (r chan *Registration) { + r = c.register + return +} diff --git a/rvpn/genericlistener/domain_api.go b/rvpn/genericlistener/domain_api.go new file mode 100644 index 0000000..bcbf522 --- /dev/null +++ b/rvpn/genericlistener/domain_api.go @@ -0,0 +1,29 @@ +package genericlistener + +//DomainAPI -- Structure to hold the domain tracking for JSON +type DomainAPI struct { + Domain string `json:"domain"` + BytesIn int64 `json:"bytes_in"` + BytesOut int64 `json:"bytes_out"` +} + +//NewDomainAPI - Constructor +func NewDomainAPI(domain string, bytesin int64, bytesout int64) (d *DomainAPI) { + d = new(DomainAPI) + d.Domain = domain + d.BytesIn = bytesin + d.BytesOut = bytesout + return +} + +// //DomainAPIContainer -- +// type DomainAPIContainer struct { +// Domains []*DomainAPI +// } + +// //NewDomainAPIContainer -- Constructor +// func NewDomainAPIContainer() (p *DomainAPIContainer) { +// p = new(DomainAPIContainer) +// p.Domains = make([]*DomainAPI, 0) +// return p +// } diff --git a/rvpn/genericlistener/domain_mapping.go b/rvpn/genericlistener/domain_mapping.go new file mode 100644 index 0000000..7c3b49b --- /dev/null +++ b/rvpn/genericlistener/domain_mapping.go @@ -0,0 +1,24 @@ +package genericlistener + +//DomainMapping -- +type DomainMapping struct { + connection *Connection + domainName string + err int + connCh chan bool +} + +//ConnCh -- Property +func (c *DomainMapping) ConnCh() chan bool { + return c.connCh +} + +//NewDomainMapping -- Constructor +func NewDomainMapping(connection *Connection, domain string) (p *DomainMapping) { + p = new(DomainMapping) + p.connection = connection + p.domainName = domain + p.err = -1 + p.connCh = make(chan bool) + return +} diff --git a/rvpn/genericlistener/domain_track.go b/rvpn/genericlistener/domain_track.go new file mode 100644 index 0000000..2378434 --- /dev/null +++ b/rvpn/genericlistener/domain_track.go @@ -0,0 +1,39 @@ +package genericlistener + +//DomainTrack -- Tracking specifics for domains +type DomainTrack struct { + DomainName string + bytesIn int64 + bytesOut int64 +} + +//NewDomainTrack -- Constructor +func NewDomainTrack(domainName string) (p *DomainTrack) { + p = new(DomainTrack) + p.DomainName = domainName + p.bytesIn = 0 + p.bytesOut = 0 + return +} + +//BytesIn -- Property +func (c *DomainTrack) BytesIn() (b int64) { + b = c.bytesIn + return +} + +//BytesOut -- Property +func (c *DomainTrack) BytesOut() (b int64) { + b = c.bytesOut + return +} + +//AddIn - Porperty +func (c *DomainTrack) AddIn(num int64) { + c.bytesIn = c.bytesIn + num +} + +//AddOut -- Property +func (c *DomainTrack) AddOut(num int64) { + c.bytesOut = c.bytesOut + num +} diff --git a/rvpn/genericlistener/listener_generic.go b/rvpn/genericlistener/listener_generic.go new file mode 100644 index 0000000..07baf22 --- /dev/null +++ b/rvpn/genericlistener/listener_generic.go @@ -0,0 +1,471 @@ +package genericlistener + +import ( + "bytes" + "context" + "crypto/tls" + "encoding/hex" + "encoding/json" + "fmt" + "log" + "net" + "strconv" + "strings" + "time" + + jwt "github.com/dgrijalva/jwt-go" + "github.com/gorilla/mux" + "github.com/gorilla/websocket" + + "net/http" + + "bufio" + + "git.daplie.com/Daplie/go-rvpn-server/rvpn/packer" +) + +type contextKey string + +//CtxConnectionTrack +const ( + ctxSecretKey contextKey = "secretKey" + ctxConnectionTable contextKey = "connectionTable" + ctxConfig contextKey = "config" + ctxListenerRegistration contextKey = "listenerRegistration" + ctxConnectionTrack contextKey = "connectionTrack" + ctxWssHostName contextKey = "wsshostname" + ctxAdminHostName contextKey = "adminHostName" + ctxCancelCheck contextKey = "cancelcheck" +) + +const ( + encryptNone int = iota + encryptSSLV2 + encryptSSLV3 + encryptTLS10 + encryptTLS11 + encryptTLS12 +) + +//GenericListenAndServe -- used to lisen for any https traffic on 443 (8443) +// - setup generic TCP listener, unencrypted TCP, with a Deadtime out +// - leaverage the wedgeConn to peek into the buffer. +// - if TLS, consume connection with TLS certbundle, pass to request identifier +// - else, just pass to the request identififer +func GenericListenAndServe(ctx context.Context, listenerRegistration *ListenerRegistration) { + loginfo.Println(":" + string(listenerRegistration.port)) + cancelCheck := ctx.Value(ctxCancelCheck).(int) + + listenAddr, err := net.ResolveTCPAddr("tcp", ":"+strconv.Itoa(listenerRegistration.port)) + + if nil != err { + loginfo.Println(err) + return + } + + ln, err := net.ListenTCP("tcp", listenAddr) + if err != nil { + loginfo.Println("unable to bind", err) + listenerRegistration.status = listenerFault + listenerRegistration.err = err + listenerRegistration.commCh <- listenerRegistration + return + } + + listenerRegistration.status = listenerAdded + listenerRegistration.commCh <- listenerRegistration + + for { + select { + case <-ctx.Done(): + loginfo.Println("Cancel signal hit") + return + default: + ln.SetDeadline(time.Now().Add(time.Duration(cancelCheck) * time.Second)) + + conn, err := ln.Accept() + + if nil != err { + if opErr, ok := err.(*net.OpError); ok && opErr.Timeout() { + continue + } + log.Println(err) + return + } + + wedgeConn := NewWedgeConn(conn) + go handleConnection(ctx, wedgeConn) + } + } +} + +//handleConnection - +// - accept a wedgeConnection along with all the other required attritvues +// - peek into the buffer, determine TLS or unencrypted +// - if TSL, then terminate with a TLS endpoint, pass to handleStream +// - if clearText, pass to handleStream +func handleConnection(ctx context.Context, wConn *WedgeConn) { + defer wConn.Close() + peekCnt := 10 + + encryptMode := encryptNone + + loginfo.Println("conn", wConn, wConn.LocalAddr().String(), wConn.RemoteAddr().String()) + peek, err := wConn.Peek(peekCnt) + + if err != nil { + loginfo.Println("error while peeking") + return + } + + //take a look for a TLS header. + if bytes.Contains(peek[0:0], []byte{0x80}) && bytes.Contains(peek[2:4], []byte{0x01, 0x03}) { + encryptMode = encryptSSLV2 + + } else if bytes.Contains(peek[0:3], []byte{0x16, 0x03, 0x00}) { + encryptMode = encryptSSLV3 + + } else if bytes.Contains(peek[0:3], []byte{0x16, 0x03, 0x01}) { + encryptMode = encryptTLS10 + loginfo.Println("TLS10") + + } else if bytes.Contains(peek[0:3], []byte{0x16, 0x03, 0x02}) { + encryptMode = encryptTLS11 + + } else if bytes.Contains(peek[0:3], []byte{0x16, 0x03, 0x03}) { + encryptMode = encryptTLS12 + } + + oneConn := &oneConnListener{wConn} + config := ctx.Value(ctxConfig).(*tls.Config) + + if encryptMode == encryptSSLV2 { + loginfo.Println("SSLv2 is not accepted") + return + + } else if encryptMode != encryptNone { + loginfo.Println("Handle Encryption") + + // check SNI heading + // if matched, then looks like a WSS connection + // else external don't pull off TLS. + + peek, err := wConn.PeekAll() + if err != nil { + loginfo.Println("error while peeking") + loginfo.Println(hex.Dump(peek[0:])) + return + } + + wssHostName := ctx.Value(ctxWssHostName).(string) + adminHostName := ctx.Value(ctxAdminHostName).(string) + + sniHostName, err := getHello(peek) + if err != nil { + loginfo.Println(err) + return + } + + loginfo.Println("sni:", sniHostName) + + if sniHostName == wssHostName { + //handle WSS Path + tlsListener := tls.NewListener(oneConn, config) + + conn, err := tlsListener.Accept() + if err != nil { + loginfo.Println(err) + return + } + + tlsWedgeConn := NewWedgeConn(conn) + handleStream(ctx, tlsWedgeConn) + return + + } else if sniHostName == adminHostName { + // handle admin path + tlsListener := tls.NewListener(oneConn, config) + + conn, err := tlsListener.Accept() + if err != nil { + loginfo.Println(err) + return + } + + tlsWedgeConn := NewWedgeConn(conn) + handleStream(ctx, tlsWedgeConn) + return + + } else { + //traffic not terminating on the rvpn do not decrypt + loginfo.Println("processing non terminating traffic", wssHostName, sniHostName) + handleExternalHTTPRequest(ctx, wConn, sniHostName, "https") + } + } + + loginfo.Println("Handle Unencrypted") + handleStream(ctx, wConn) + + return +} + +//handleStream -- +// - we have an unencrypted stream connection with the ability to peek +// - attempt to identify HTTP +// - handle http +// - attempt to identify as WSS session +// - attempt to identify as ADMIN/API session +// - else handle as raw http +// - handle other? +func handleStream(ctx context.Context, wConn *WedgeConn) { + loginfo.Println("handle Stream") + loginfo.Println("conn", wConn.LocalAddr().String(), wConn.RemoteAddr().String()) + + peek, err := wConn.PeekAll() + if err != nil { + loginfo.Println("error while peeking", err) + loginfo.Println(hex.Dump(peek[0:])) + return + } + + // HTTP Identifcation + if bytes.Contains(peek[:], []byte{0x0d, 0x0a}) { + //string protocol + if bytes.ContainsAny(peek[:], "HTTP/") { + loginfo.Println("identifed HTTP") + + r, err := http.ReadRequest(bufio.NewReader(bytes.NewReader(peek))) + if err != nil { + loginfo.Println("identifed as HTTP, failed request parsing", err) + return + } + + // do we have a valid wss_client? + secretKey := ctx.Value(ctxSecretKey).(string) + tokenString := r.URL.Query().Get("access_token") + result, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + return []byte(secretKey), nil + }) + + if err == nil && result.Valid { + loginfo.Println("Valid WSS dected...sending to handler") + oneConn := &oneConnListener{wConn} + handleWssClient(ctx, oneConn) + + //do we have a invalid domain indicating Admin? + //if yes, prep the oneConn and send it to the handler + } else if strings.Contains(r.Host, "rvpn.daplie.invalid") { + loginfo.Println("admin") + oneConn := &oneConnListener{wConn} + handleAdminClient(ctx, oneConn) + return + + } else { + loginfo.Println("unsupported") + loginfo.Println(hex.Dump(peek)) + return + } + } + } +} + +//handleExternalHTTPRequest - +// - get a wConn and start processing requests +func handleExternalHTTPRequest(ctx context.Context, extConn *WedgeConn, hostname string, service string) { + connectionTracking := ctx.Value(ctxConnectionTrack).(*Tracking) + + defer func() { + connectionTracking.unregister <- extConn + extConn.Close() + }() + + connectionTable := ctx.Value(ctxConnectionTable).(*Table) + //find the connection by domain name + conn, ok := connectionTable.ConnByDomain(hostname) + if !ok { + //matching connection can not be found based on ConnByDomain + loginfo.Println("unable to match ", hostname, " to an existing connection") + //http.Error(, "Domain not supported", http.StatusBadRequest) + return + } + + track := NewTrack(extConn, hostname) + connectionTracking.register <- track + + loginfo.Println("Domain Accepted", hostname, extConn.RemoteAddr().String()) + + rAddr, rPort, err := net.SplitHostPort(extConn.RemoteAddr().String()) + if err != nil { + loginfo.Println("unable to decode hostport", extConn.RemoteAddr().String()) + return + } + + for { + buffer, err := extConn.PeekAll() + if err != nil { + loginfo.Println("unable to peekAll", err) + return + } + + loginfo.Println("Before Packer", hex.Dump(buffer)) + + cnt := len(buffer) + + p := packer.NewPacker() + p.Header.SetAddress(rAddr) + p.Header.Port, err = strconv.Atoi(rPort) + if err != nil { + loginfo.Println("Unable to set Remote port", err) + return + } + + p.Header.Service = service + p.Data.AppendBytes(buffer[0:cnt]) + buf := p.PackV1() + + loginfo.Println(hex.Dump(buf.Bytes())) + + sendTrack := NewSendTrack(buf.Bytes(), hostname) + conn.SendCh() <- sendTrack + + _, err = extConn.Discard(cnt) + if err != nil { + loginfo.Println("unable to discard", cnt, err) + return + } + + } +} + +//handleAdminClient - +// - expecting an existing oneConnListener with a qualified wss client connected. +// - auth will happen again since we were just peeking at the token. +func handleAdminClient(ctx context.Context, oneConn *oneConnListener) { + connectionTable := ctx.Value(ctxConnectionTable).(*Table) + + router := mux.NewRouter().StrictSlash(true) + + router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + loginfo.Println("HandleFunc /") + switch url := r.URL.Path; url { + case "/": + // check to see if we are using the administrative Host + if strings.Contains(r.Host, "rvpn.daplie.invalid") { + http.Redirect(w, r, "/admin", 301) + } + + default: + http.Error(w, "Not Found", 404) + } + }) + + router.HandleFunc("/admin", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + fmt.Fprintln(w, "Welcome..press Servers to access stats") + }) + + router.HandleFunc("/api/servers", func(w http.ResponseWriter, r *http.Request) { + fmt.Println("here") + serverContainer := NewServerAPIContainer() + + for c := range connectionTable.Connections() { + serverAPI := NewServerAPI(c) + serverContainer.Servers = append(serverContainer.Servers, serverAPI) + + } + + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + json.NewEncoder(w).Encode(serverContainer) + + }) + + s := &http.Server{ + Addr: ":80", + Handler: router, + } + + err := s.Serve(oneConn) + if err != nil { + loginfo.Println("Serve error: ", err) + } + + select { + case <-ctx.Done(): + loginfo.Println("Cancel signal hit") + return + } +} + +//handleWssClient - +// - expecting an existing oneConnListener with a qualified wss client connected. +// - auth will happen again since we were just peeking at the token. +func handleWssClient(ctx context.Context, oneConn *oneConnListener) { + secretKey := ctx.Value(ctxSecretKey).(string) + connectionTable := ctx.Value(ctxConnectionTable).(*Table) + + router := mux.NewRouter().StrictSlash(true) + router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + loginfo.Println("HandleFunc /") + switch url := r.URL.Path; url { + case "/": + loginfo.Println("websocket opening ", r.RemoteAddr, " ", r.Host) + + tokenString := r.URL.Query().Get("access_token") + result, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + return []byte(secretKey), nil + }) + + if err != nil || !result.Valid { + w.WriteHeader(http.StatusForbidden) + w.Write([]byte("Not Authorized")) + loginfo.Println("access_token invalid...closing connection") + return + } + + claims := result.Claims.(jwt.MapClaims) + domains, ok := claims["domains"].([]interface{}) + + var upgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + } + + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + loginfo.Println("WebSocket upgrade failed", err) + return + } + + loginfo.Println("before connection table") + + //newConnection := connection.NewConnection(connectionTable, conn, r.RemoteAddr, domains) + + connectionTrack := ctx.Value(ctxConnectionTrack).(*Tracking) + newRegistration := NewRegistration(conn, r.RemoteAddr, domains, connectionTrack) + connectionTable.Register() <- newRegistration + ok = <-newRegistration.CommCh() + if !ok { + loginfo.Println("connection registration failed ", newRegistration) + return + } + + loginfo.Println("connection registration accepted ", newRegistration) + } + }) + + s := &http.Server{ + Addr: ":80", + Handler: router, + } + + err := s.Serve(oneConn) + if err != nil { + loginfo.Println("Serve error: ", err) + } + + select { + case <-ctx.Done(): + loginfo.Println("Cancel signal hit") + return + } +} diff --git a/rvpn/genericlistener/manager.go b/rvpn/genericlistener/manager.go new file mode 100644 index 0000000..c05c58f --- /dev/null +++ b/rvpn/genericlistener/manager.go @@ -0,0 +1,133 @@ +package genericlistener + +import ( + "context" + "crypto/tls" + "net" +) + +//ListenerRegistrationStatus - post registration status +type ListenerRegistrationStatus int + +const ( + listenerAdded ListenerRegistrationStatus = iota + listenerExists + listenerFault +) + +//ListenerRegistration -- A connection registration structure used to bring up a connection +//connection table will then handle additing and sdtarting up the various readers +//else error. +type ListenerRegistration struct { + // The websocket connection. + listener *net.Listener + + // The listener port + port int + + // The status + status ListenerRegistrationStatus + + // The error + err error + + // communications channel between go routines + commCh chan *ListenerRegistration +} + +//NewListenerRegistration -- Constructor +func NewListenerRegistration(port int) (p *ListenerRegistration) { + p = new(ListenerRegistration) + p.port = port + p.commCh = make(chan *ListenerRegistration) + return +} + +//GenericListeners - +type GenericListeners struct { + listeners map[*net.Listener]int + ctx context.Context + connnectionTable *Table + connectionTracking *Tracking + secretKey string + certbundle tls.Certificate + register chan *ListenerRegistration + genericListeners *GenericListeners + wssHostName string + adminHostName string + cancelCheck int +} + +//NewGenerListeners -- +func NewGenerListeners(ctx context.Context, connectionTable *Table, connectionTrack *Tracking, secretKey string, certbundle tls.Certificate, wssHostName string, adminHostName string, cancelCheck int) (p *GenericListeners) { + p = new(GenericListeners) + p.listeners = make(map[*net.Listener]int) + p.ctx = ctx + p.connnectionTable = connectionTable + p.connectionTracking = connectionTrack + p.secretKey = secretKey + p.certbundle = certbundle + p.register = make(chan *ListenerRegistration) + p.wssHostName = wssHostName + p.adminHostName = adminHostName + p.cancelCheck = cancelCheck + return +} + +//Run -- Execute +// - execute the GenericLister +// - pass initial port, we'll announce that +func (gl *GenericListeners) Run(ctx context.Context, initialPort int) { + loginfo.Println("ConnectionTable starting") + + config := &tls.Config{Certificates: []tls.Certificate{gl.certbundle}} + + ctx = context.WithValue(ctx, ctxSecretKey, gl.secretKey) + ctx = context.WithValue(ctx, ctxConnectionTable, gl.connnectionTable) + + loginfo.Println(gl.connectionTracking) + + ctx = context.WithValue(ctx, ctxConnectionTrack, gl.connectionTracking) + ctx = context.WithValue(ctx, ctxConfig, config) + ctx = context.WithValue(ctx, ctxListenerRegistration, gl.register) + ctx = context.WithValue(ctx, ctxWssHostName, gl.wssHostName) + ctx = context.WithValue(ctx, ctxAdminHostName, gl.adminHostName) + ctx = context.WithValue(ctx, ctxCancelCheck, gl.cancelCheck) + + go func(ctx context.Context) { + for { + select { + + case <-ctx.Done(): + loginfo.Println("Cancel signal hit") + return + + case registration := <-gl.register: + loginfo.Println("register fired", registration.port) + + // check to see if port is already running + for listener := range gl.listeners { + if gl.listeners[listener] == registration.port { + loginfo.Println("listener already running", registration.port) + registration.status = listenerExists + registration.commCh <- registration + } + } + loginfo.Println("listener starting up ", registration.port) + loginfo.Println(ctx.Value(ctxConnectionTrack).(*Tracking)) + go GenericListenAndServe(ctx, registration) + + status := <-registration.commCh + if status.status == listenerAdded { + gl.listeners[status.listener] = status.port + } else if status.status == listenerFault { + loginfo.Println("Unable to create a new listerer", registration.port) + } + } + } + + }(ctx) + + newListener := NewListenerRegistration(initialPort) + gl.register <- newListener +} diff --git a/rvpn/genericlistener/one_conn.go b/rvpn/genericlistener/one_conn.go new file mode 100644 index 0000000..1e9b104 --- /dev/null +++ b/rvpn/genericlistener/one_conn.go @@ -0,0 +1,34 @@ +package genericlistener + +import ( + "io" + "net" +) + +type oneConnListener struct { + conn net.Conn +} + +func (l *oneConnListener) Accept() (c net.Conn, err error) { + c = l.conn + + if c == nil { + err = io.EOF + loginfo.Println("Accept") + return + } + err = nil + l.conn = nil + loginfo.Println("Accept", c.LocalAddr().String(), c.RemoteAddr().String()) + return +} + +func (l *oneConnListener) Close() error { + loginfo.Println("close") + return nil +} + +func (l *oneConnListener) Addr() net.Addr { + loginfo.Println("addr") + return nil +} diff --git a/rvpn/genericlistener/send_track.go b/rvpn/genericlistener/send_track.go new file mode 100644 index 0000000..817a179 --- /dev/null +++ b/rvpn/genericlistener/send_track.go @@ -0,0 +1,16 @@ +package genericlistener + +//SendTrack -- Used as a channel communication to id domain asssociated to domain for outbound WSS +type SendTrack struct { + data []byte + domain string +} + +//NewSendTrack -- Constructor +func NewSendTrack(data []byte, domain string) (p *SendTrack) { + p = new(SendTrack) + p.data = data + p.domain = domain + return + +} diff --git a/rvpn/genericlistener/server_api.go b/rvpn/genericlistener/server_api.go new file mode 100644 index 0000000..2f84e27 --- /dev/null +++ b/rvpn/genericlistener/server_api.go @@ -0,0 +1,44 @@ +package genericlistener + +import ( + "fmt" + "time" +) + +//ServerAPI -- Structure to support the server API +type ServerAPI struct { + ServerName string `json:"server_name"` + Domains []*DomainAPI `json:"domains"` + Duration float64 `json:"duration"` + BytesIn int64 `json:"bytes_in"` + BytesOut int64 `json:"bytes_out"` +} + +//NewServerAPI - Constructor +func NewServerAPI(c *Connection) (s *ServerAPI) { + s = new(ServerAPI) + s.ServerName = fmt.Sprintf("%p", c) + s.Domains = make([]*DomainAPI, 0) + s.Duration = time.Since(c.ConnectTime()).Seconds() + s.BytesIn = c.BytesIn() + s.BytesOut = c.BytesOut() + + for d := range c.DomainTrack { + dt := c.DomainTrack[d] + domainAPI := NewDomainAPI(dt.DomainName, dt.BytesIn(), dt.BytesOut()) + s.Domains = append(s.Domains, domainAPI) + } + return +} + +//ServerAPIContainer -- Holder for all the Servers +type ServerAPIContainer struct { + Servers []*ServerAPI `json:"servers"` +} + +//NewServerAPIContainer -- Constructor +func NewServerAPIContainer() (p *ServerAPIContainer) { + p = new(ServerAPIContainer) + p.Servers = make([]*ServerAPI, 0) + return p +} diff --git a/rvpn/genericlistener/setup.go b/rvpn/genericlistener/setup.go new file mode 100644 index 0000000..79d6113 --- /dev/null +++ b/rvpn/genericlistener/setup.go @@ -0,0 +1,17 @@ +package genericlistener + +import ( + "log" + "os" +) + +var ( + loginfo *log.Logger + logdebug *log.Logger + logFlags = log.Ldate | log.Lmicroseconds | log.Lshortfile +) + +func init() { + loginfo = log.New(os.Stdout, "INFO: genericlistener: ", logFlags) + logdebug = log.New(os.Stdout, "DEBUG: genericlistener:", logFlags) +} diff --git a/rvpn/genericlistener/tls_get_hello.go b/rvpn/genericlistener/tls_get_hello.go new file mode 100644 index 0000000..0ddb051 --- /dev/null +++ b/rvpn/genericlistener/tls_get_hello.go @@ -0,0 +1,69 @@ +package genericlistener + +import "errors" + +func getHello(b []byte) (string, error) { + rest := b[5:] + current := 0 + handshakeType := rest[0] + current++ + if handshakeType != 0x1 { + return "", errors.New("Not a ClientHello") + } + + // Skip over another length + current += 3 + // Skip over protocolversion + current += 2 + // Skip over random number + current += 4 + 28 + // Skip over session ID + sessionIDLength := int(rest[current]) + current++ + current += sessionIDLength + + cipherSuiteLength := (int(rest[current]) << 8) + int(rest[current+1]) + current += 2 + current += cipherSuiteLength + + compressionMethodLength := int(rest[current]) + current++ + current += compressionMethodLength + + if current > len(rest) { + return "", errors.New("no extensions") + } + + current += 2 + + hostname := "" + for current < len(rest) && hostname == "" { + extensionType := (int(rest[current]) << 8) + int(rest[current+1]) + current += 2 + + extensionDataLength := (int(rest[current]) << 8) + int(rest[current+1]) + current += 2 + + if extensionType == 0 { + + // Skip over number of names as we're assuming there's just one + current += 2 + + nameType := rest[current] + current++ + if nameType != 0 { + return "", errors.New("Not a hostname") + } + nameLen := (int(rest[current]) << 8) + int(rest[current+1]) + current += 2 + hostname = string(rest[current : current+nameLen]) + } + + current += extensionDataLength + } + if hostname == "" { + return "", errors.New("No hostname") + } + return hostname, nil + +} diff --git a/rvpn/packer/packer.go b/rvpn/packer/packer.go new file mode 100644 index 0000000..2c6f6db --- /dev/null +++ b/rvpn/packer/packer.go @@ -0,0 +1,146 @@ +package packer + +import ( + "bytes" + "fmt" + "net" + "strconv" +) + +const ( + packerV1 byte = 255 - 1 + packerV2 byte = 255 - 2 +) + +//Packer -- contains both header and data +type Packer struct { + Header *packerHeader + Data *packerData +} + +//NewPacker -- Structre +func NewPacker() (p *Packer) { + p = new(Packer) + p.Header = newPackerHeader() + p.Data = newPackerData() + return +} + +//ReadMessage - +func ReadMessage(b []byte) (p *Packer, err error) { + fmt.Println("ReadMessage") + var pos int + + err = nil + // detect protocol in use + if b[0] == packerV1 { + p = NewPacker() + + // Handle Header Length + pos = pos + 1 + p.Header.HeaderLen = b[pos] + + //handle address family + pos = pos + 1 + end := bytes.IndexAny(b[pos:], ",") + if end == -1 { + err = fmt.Errorf("missing , while parsing address family") + return nil, err + } + + bAddrFamily := b[pos : pos+end] + if bytes.ContainsAny(bAddrFamily, addressFamilyText[FamilyIPv4]) { + p.Header.family = FamilyIPv4 + } else if bytes.ContainsAny(bAddrFamily, addressFamilyText[FamilyIPv6]) { + p.Header.family = FamilyIPv6 + } else { + err = fmt.Errorf("Address family not supported %d", bAddrFamily) + } + + //handle address + pos = pos + end + 1 + end = bytes.IndexAny(b[pos:], ",") + if end == -1 { + err = fmt.Errorf("missing , while parsing address") + return nil, err + } + p.Header.address = net.ParseIP(string(b[pos : pos+end])) + + //handle import + pos = pos + end + 1 + end = bytes.IndexAny(b[pos:], ",") + if end == -1 { + err = fmt.Errorf("missing , while parsing address") + return nil, err + } + + p.Header.Port, err = strconv.Atoi(string(b[pos : pos+end])) + if err != nil { + err = fmt.Errorf("error converting port %s", err) + } + + //handle data length + pos = pos + end + 1 + end = bytes.IndexAny(b[pos:], ",") + if end == -1 { + err = fmt.Errorf("missing , while parsing address") + return nil, err + } + + p.Data.DataLen, err = strconv.Atoi(string(b[pos : pos+end])) + if err != nil { + err = fmt.Errorf("error converting data length %s", err) + } + + //handle Service + pos = pos + end + 1 + end = pos + int(p.Header.HeaderLen) + p.Header.Service = string(b[pos : p.Header.HeaderLen+2]) + + //handle payload + pos = int(p.Header.HeaderLen + 2) + p.Data.AppendBytes(b[pos:]) + + } else { + err = fmt.Errorf("Version %d not supported", b[0:0]) + } + + return + +} + +//PackV1 -- Outputs version 1 of packer +func (p *Packer) PackV1() (b bytes.Buffer) { + version := packerV1 + + var headerBuf bytes.Buffer + headerBuf.WriteString(p.Header.FamilyText()) + headerBuf.WriteString(",") + headerBuf.Write([]byte(p.Header.Address().String())) + headerBuf.WriteString(",") + headerBuf.WriteString(fmt.Sprintf("%d", p.Header.Port)) + headerBuf.WriteString(",") + headerBuf.WriteString(fmt.Sprintf("%d", p.Data.buffer.Len())) + headerBuf.WriteString(",") + headerBuf.WriteString(p.Header.Service) + + var metaBuf bytes.Buffer + metaBuf.WriteByte(version) + metaBuf.WriteByte(byte(headerBuf.Len())) + + var buf bytes.Buffer + buf.Write(metaBuf.Bytes()) + buf.Write(headerBuf.Bytes()) + buf.Write(p.Data.buffer.Bytes()) + + //fmt.Println("header: ", headerBuf.String()) + //fmt.Println("meta: ", metaBuf) + //fmt.Println("Data: ", p.Data.buffer) + //fmt.Println("Buffer: ", buf.Bytes()) + //fmt.Println("Buffer: ", hex.Dump(buf.Bytes())) + //fmt.Printf("Buffer %s", buf.Bytes()) + + b = buf + + return +} diff --git a/rvpn/packer/packer_data.go b/rvpn/packer/packer_data.go new file mode 100644 index 0000000..fce36c8 --- /dev/null +++ b/rvpn/packer/packer_data.go @@ -0,0 +1,31 @@ +package packer + +import "bytes" + +//packerData -- Contains packer data +type packerData struct { + buffer *bytes.Buffer + DataLen int +} + +func newPackerData() (p *packerData) { + p = new(packerData) + p.buffer = new(bytes.Buffer) + return +} + +func (p *packerData) AppendString(dataString string) (n int, err error) { + n, err = p.buffer.WriteString(dataString) + return +} + +func (p *packerData) AppendBytes(dataBytes []byte) (n int, err error) { + n, err = p.buffer.Write(dataBytes) + return +} + +//Data -- +func (p *packerData) Data() (b []byte) { + b = p.buffer.Bytes() + return +} diff --git a/rvpn/packer/packer_header.go b/rvpn/packer/packer_header.go new file mode 100644 index 0000000..7319622 --- /dev/null +++ b/rvpn/packer/packer_header.go @@ -0,0 +1,81 @@ +package packer + +import "net" +import "fmt" + +type addressFamily int + +// packerHeader structure to hold our header information. +type packerHeader struct { + family addressFamily + address net.IP + Port int + Service string + HeaderLen byte +} + +//Family -- ENUM for Address Family +const ( + FamilyIPv4 addressFamily = iota + FamilyIPv6 +) + +var addressFamilyText = [...]string{ + "IPv4", + "IPv6", +} + +func newPackerHeader() (p *packerHeader) { + p = new(packerHeader) + p.SetAddress("127.0.0.1") + p.Port = 65535 + p.Service = "na" + p.HeaderLen = 0 + return +} + +//SetAddress -- Set Address. which sets address family automatically +func (p *packerHeader) SetAddress(addr string) { + p.address = net.ParseIP(addr) + err := p.address.To4() + + if err != nil { + p.family = FamilyIPv4 + } else { + err := p.address.To16() + if err != nil { + p.family = FamilyIPv6 + } else { + panic(fmt.Sprintf("setAddress does not support %s", addr)) + } + } +} + +func (p *packerHeader) AddressBytes() (b []byte) { + b = make([]byte, 16) + + switch { + case p.address.To4() != nil: + b = make([]byte, 4) + for pos := range b { + b[pos] = p.address[pos+12] + } + return + } + return +} + +func (p *packerHeader) Address() (address net.IP) { + address = p.address + return +} + +func (p *packerHeader) Family() (family addressFamily) { + family = p.family + return +} + +func (p *packerHeader) FamilyText() (familyText string) { + familyText = addressFamilyText[p.family] + return +} diff --git a/rvpn/packer/setup.go b/rvpn/packer/setup.go new file mode 100644 index 0000000..bee01b8 --- /dev/null +++ b/rvpn/packer/setup.go @@ -0,0 +1,15 @@ +package packer + +import "log" +import "os" + +var ( + loginfo *log.Logger + logdebug *log.Logger + logFlags = log.Ldate | log.Lmicroseconds | log.Lshortfile +) + +func init() { + loginfo = log.New(os.Stdout, "INFO: packer: ", logFlags) + logdebug = log.New(os.Stdout, "DEBUG: packer:", logFlags) +} diff --git a/rvpn/xlate/setup.go b/rvpn/xlate/setup.go new file mode 100644 index 0000000..fdc3c8e --- /dev/null +++ b/rvpn/xlate/setup.go @@ -0,0 +1,17 @@ +package xlate + +import ( + "log" + "os" +) + +var ( + loginfo *log.Logger + logdebug *log.Logger + logFlags = log.Ldate | log.Lmicroseconds | log.Lshortfile +) + +func init() { + loginfo = log.New(os.Stdout, "INFO: xlate: ", logFlags) + logdebug = log.New(os.Stdout, "DEBUG: xlate:", logFlags) +} \ No newline at end of file diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..98189e3 --- /dev/null +++ b/start.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +go build && ./go-rvpn-server + +