├── LICENSE ├── README.md ├── addresspool.go ├── client.go ├── interface_unix.go ├── main.go └── server.go /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017, Dominik Süß 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY 10 | SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION 12 | OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN 13 | CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wpn [![Go Report Card](https://goreportcard.com/badge/github.com/theSuess/wpn)](https://goreportcard.com/report/github.com/theSuess/wpn) 2 | Easy to use websocket VPN 3 | 4 | ### WTF is this? 5 | wpn is a VPN solution using websockets as transport layer. Usefull when running behind a reverse proxy like nginx. 6 | There are some other websocket-vpns but wpn aims for usability. 7 | ### Setup 8 | 9 | ```.bash 10 | # On both 11 | go get -u github.com/theSuess/wpn 12 | 13 | # On the server 14 | wpn --debug server -l 0.0.0.0:6969 --client-network 192.168.69.0/24 --range 192.168.69.150-192.168.69.160 15 | 16 | # On the client 17 | wpn --debug -r :6969 18 | ``` 19 | Now the client can reach every device in the `192.168.69.0/24` network. 20 | 21 | ### Platform support 22 | wpn is only available for linux at the moment. Contributions to add support for Windows,Mac or BSD are welcomed. 23 | 24 | ### How secure is this? 25 | When not using secure websockets (wss) the security is basically nil. Otherwise the encryption of the same strength as HTTPS. 26 | Authorization can be set with the `--secret` parameter and allows for some kind of access restriction. 27 | 28 | #### Features 29 | - [x] Basic connectivity 30 | - [x] SSL 31 | - [x] Authorization 32 | - [ ] status reports 33 | - [ ] isolated VPN pool 34 | - [ ] Access lists 35 | - [ ] MacOS Support 36 | - [ ] Windows Support 37 | -------------------------------------------------------------------------------- /addresspool.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/binary" 5 | "net" 6 | "strings" 7 | 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | // AddressPool stores and manages active connections 12 | type AddressPool struct { 13 | tunName string 14 | pool map[*net.IP]bool 15 | } 16 | 17 | // Setup initializes the address pool 18 | func (a *AddressPool) Setup(r string, i string) { 19 | a.tunName = i 20 | a.pool = make(map[*net.IP]bool) 21 | adrs := strings.Split(r, "-") 22 | beg, end := net.ParseIP(adrs[0]), net.ParseIP(adrs[1]) 23 | bg := ip2int(beg) 24 | e := ip2int(end) 25 | for i := bg; i < e; i++ { 26 | ip := int2ip(i) 27 | a.pool[&ip] = false 28 | } 29 | } 30 | 31 | // Remove frees the address 32 | func (a *AddressPool) Remove(ip *net.IP) { 33 | a.pool[ip] = false 34 | err := removeDevRoute(ip.String(), a.tunName) 35 | if err != nil { 36 | log.Error(err) 37 | } 38 | } 39 | 40 | // Get retrieves a free address 41 | func (a *AddressPool) Get() *net.IP { 42 | for ip, u := range a.pool { 43 | if !u { 44 | // Inject IP route 45 | err := addDevRoute(ip.String(), a.tunName) 46 | if err != nil { 47 | log.Error(err) 48 | return nil 49 | } 50 | return ip 51 | } 52 | } 53 | return nil 54 | } 55 | 56 | // IP to int code from here: https://gist.github.com/ammario/649d4c0da650162efd404af23e25b86b 57 | func ip2int(ip net.IP) uint32 { 58 | if len(ip) == 16 { 59 | return binary.BigEndian.Uint32(ip[12:16]) 60 | } 61 | return binary.BigEndian.Uint32(ip) 62 | } 63 | 64 | func int2ip(nn uint32) net.IP { 65 | ip := make(net.IP, 4) 66 | binary.BigEndian.PutUint32(ip, nn) 67 | return ip 68 | } 69 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | "net/url" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | 10 | "github.com/gorilla/websocket" 11 | "github.com/jasonlvhit/gocron" 12 | log "github.com/sirupsen/logrus" 13 | "github.com/songgao/water" 14 | "github.com/urfave/cli" 15 | ) 16 | 17 | // ClientConfiguration tells the client how to configure the tun device 18 | type ClientConfiguration struct { 19 | IP string 20 | Network string 21 | } 22 | 23 | // Client encapsulates a tun interface 24 | type Client struct { 25 | tun *water.Interface 26 | } 27 | 28 | // NewClient creates a new client 29 | func NewClient(tun *water.Interface) *Client { 30 | return &Client{tun: tun} 31 | } 32 | 33 | // Run starts the Client 34 | func (cl *Client) Run(c *cli.Context) error { 35 | log.WithField("Interface", c.GlobalString("interface")).Info("Starting Client") 36 | 37 | if c.String("remote") == "" { 38 | log.Error("Remote cannot be empty") 39 | return nil 40 | } 41 | proto := "ws" 42 | if c.Bool("secure") { 43 | proto = "wss" 44 | } 45 | u := url.URL{Scheme: proto, Host: c.String("remote"), Path: "/vpn"} 46 | log.Infof("Connecting to %s", u.String()) 47 | headers := http.Header{} 48 | headers.Add("X-WPN-Secret", c.String("secret")) 49 | w, _, err := websocket.DefaultDialer.Dial(u.String(), headers) 50 | if err != nil { 51 | log.Fatal(err) 52 | } 53 | conf := &ClientConfiguration{} 54 | err = w.ReadJSON(&conf) 55 | if err != nil { 56 | log.Fatal(err) 57 | } 58 | 59 | log.WithFields(log.Fields{ 60 | "IP": conf.IP, 61 | "Network": conf.Network, 62 | }).Info("Received Configuration") 63 | 64 | // Set IP on interface 65 | err = addIPAddress(conf.IP, cl.tun.Name()) 66 | if err != nil { 67 | log.Fatal(err) 68 | } 69 | 70 | // Inject IP route 71 | err = addDevRoute(conf.Network, cl.tun.Name()) 72 | if err != nil { 73 | log.Fatal(err) 74 | } 75 | 76 | wsout := make(chan []byte, 1024) 77 | tout := make(chan []byte, 1024) 78 | go wsListener(w, wsout, nil) 79 | rm := &routeManager{isClient: true} 80 | go rm.tunListener(cl.tun, tout) 81 | errors := make(chan error) 82 | go func() { 83 | for { 84 | select { 85 | case packet := <-tout: 86 | err = w.WriteMessage(2, packet) 87 | if err != nil { 88 | log.Error(err) 89 | errors <- err 90 | } 91 | case packet := <-wsout: 92 | _, err := cl.tun.Write(packet) 93 | if err != nil { 94 | log.Error(err) 95 | errors <- err 96 | } 97 | } 98 | } 99 | }() 100 | sigs := make(chan os.Signal, 1) 101 | signal.Notify(sigs, syscall.SIGINT, syscall.SIGKILL, syscall.SIGQUIT) 102 | log.Info("Established connection") 103 | 104 | gocron.Every(15).Seconds().Do(func() { 105 | log.Info("Sending Keepalive") 106 | err := w.WriteMessage(websocket.PingMessage, []byte("KEEPALIVE")) 107 | if err != nil { 108 | errors <- err 109 | return 110 | } 111 | }) 112 | _ = gocron.Start() 113 | select { 114 | case _ = <-sigs: 115 | return nil 116 | case err := <-errors: 117 | return err 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /interface_unix.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os/exec" 5 | ) 6 | 7 | func addDevRoute(src, dev string) error { 8 | cmd := exec.Command("/sbin/ip", "route", "add", src, "dev", dev) 9 | return cmd.Run() 10 | } 11 | 12 | func removeDevRoute(src, dev string) error { 13 | cmd := exec.Command("/sbin/ip", "route", "del", src, "dev", dev) 14 | return cmd.Run() 15 | } 16 | 17 | func addIPAddress(ip, dev string) error { 18 | cmd := exec.Command("/sbin/ip", "addr", "add", ip, "dev", dev) 19 | return cmd.Run() 20 | } 21 | 22 | func setDevUp(dev string) error { 23 | cmd := exec.Command("/sbin/ip", "link", "set", "dev", dev, "up") 24 | return cmd.Run() 25 | } 26 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "golang.org/x/net/ipv4" 7 | 8 | "github.com/gorilla/websocket" 9 | log "github.com/sirupsen/logrus" 10 | "github.com/songgao/water" 11 | "github.com/urfave/cli" 12 | ) 13 | 14 | const ( 15 | // BUFFERSIZE is the size of received and sent packets 16 | BUFFERSIZE = 1500 17 | ) 18 | 19 | func main() { 20 | app := cli.NewApp() 21 | app.Name = "wpn" 22 | app.Version = "0.0.1" 23 | app.Usage = "WebSocket based VPN" 24 | app.Flags = []cli.Flag{ 25 | cli.StringFlag{ 26 | Name: "interface, i", 27 | Value: "wpn1", 28 | Usage: "name of the tun device", 29 | }, 30 | cli.BoolFlag{ 31 | Name: "debug", 32 | Usage: "Show debug messages", 33 | }, 34 | } 35 | var tun *water.Interface 36 | app.Before = func(c *cli.Context) error { 37 | if c.Args().Get(1) == "--help" { 38 | return nil 39 | } 40 | if c.Bool("debug") { 41 | log.SetLevel(log.DebugLevel) 42 | log.Debug("Debug messages activated") 43 | } 44 | // Create the TUN Device 45 | i := c.GlobalString("interface") 46 | if i == "" { 47 | log.Fatal("Interface can't be empty") 48 | } 49 | conf := water.Config{ 50 | DeviceType: water.TUN, 51 | } 52 | conf.Name = i 53 | t, err := water.New(conf) 54 | if err != nil { 55 | log.Error(err) 56 | } 57 | tun = t 58 | 59 | // Bring interface up 60 | err = setDevUp(tun.Name()) 61 | if err != nil { 62 | log.Fatal(err) 63 | } 64 | return nil 65 | } 66 | app.After = func(c *cli.Context) error { 67 | if c.Args().Get(1) == "--help" { 68 | return nil 69 | } 70 | log.WithField("interface", tun.Name()).Info("Tearing down tun device") 71 | return tun.Close() 72 | } 73 | app.Commands = []cli.Command{ 74 | { 75 | Name: "server", 76 | Aliases: []string{"s"}, 77 | Usage: "Start a wpn server", 78 | Flags: []cli.Flag{ 79 | cli.StringFlag{ 80 | Name: "listen, l", 81 | Usage: "Address to listen for remote connections", 82 | Value: "0.0.0.0:6969", 83 | }, 84 | cli.StringFlag{ 85 | Name: "client-network", 86 | Usage: "Network to place clients into", 87 | Value: "10.69.69.0/24", 88 | }, 89 | cli.StringFlag{ 90 | Name: "range", 91 | Usage: "IP Range to distribute to clients", 92 | Value: "10.69.69.100-10.69.69.150", 93 | }, 94 | cli.StringFlag{ 95 | Name: "secret", 96 | Usage: "shared secret between server and client. Used for authorization", 97 | Value: "WPN", 98 | }, 99 | cli.StringFlag{ 100 | Name: "certfile", 101 | Usage: "location of the SSL Certificate", 102 | Value: "", 103 | }, 104 | cli.StringFlag{ 105 | Name: "keyfile", 106 | Usage: "location of the SSL Key", 107 | Value: "", 108 | }, 109 | }, 110 | Action: func(c *cli.Context) error { 111 | s := NewServer(tun) 112 | return s.Run(c) 113 | }, 114 | }, 115 | { 116 | Name: "client", 117 | Aliases: []string{"c"}, 118 | Usage: "Connect to a wpn server", 119 | Flags: []cli.Flag{ 120 | cli.StringFlag{ 121 | Name: "remote, r", 122 | Usage: "Address of the remote server", 123 | Value: "127.0.0.1:6969", 124 | }, 125 | cli.BoolFlag{ 126 | Name: "secure, s", 127 | Usage: "use wss instead of ws", 128 | }, 129 | cli.StringFlag{ 130 | Name: "secret", 131 | Usage: "shared secret between server and client. Used for authorization", 132 | Value: "WPN", 133 | }, 134 | }, 135 | Action: func(c *cli.Context) error { 136 | cl := NewClient(tun) 137 | return cl.Run(c) 138 | }, 139 | }, 140 | } 141 | err := app.Run(os.Args) 142 | if err != nil { 143 | log.Error(err) 144 | } 145 | } 146 | 147 | func tunWriter(tun *water.Interface, in <-chan []byte) { 148 | for packet := range in { 149 | _, err := tun.Write(packet) 150 | if err != nil { 151 | log.Error(err) 152 | } 153 | } 154 | } 155 | 156 | func wsListener(ws *websocket.Conn, out chan []byte, fail chan error) { 157 | for { 158 | mt, packet, err := ws.ReadMessage() 159 | if err != nil { 160 | log.Error(err) 161 | fail <- err 162 | return 163 | } 164 | if mt == 9 { 165 | continue 166 | } 167 | if mt != 2 { 168 | log.Error("Received invalid message type.") 169 | fail <- err 170 | return 171 | } 172 | header, err := ipv4.ParseHeader(packet) 173 | if err != nil { 174 | log.Error(err) 175 | fail <- err 176 | break 177 | } 178 | log.WithFields(log.Fields{ 179 | "Source": header.Src.String(), 180 | "Dest": header.Dst.String(), 181 | "SourceChan": "ws", 182 | }).Debug("Received Packet") 183 | out <- packet 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net" 5 | "net/http" 6 | 7 | "golang.org/x/net/ipv4" 8 | 9 | "github.com/gorilla/websocket" 10 | log "github.com/sirupsen/logrus" 11 | "github.com/songgao/water" 12 | "github.com/urfave/cli" 13 | ) 14 | 15 | type routeManager struct { 16 | isClient bool 17 | routes map[string]chan []byte 18 | } 19 | 20 | // Server encapsulates a tun interface 21 | type Server struct { 22 | tun *water.Interface 23 | } 24 | 25 | // NewServer creates a new server 26 | func NewServer(tun *water.Interface) *Server { 27 | return &Server{tun: tun} 28 | } 29 | 30 | // Run starts the Server 31 | func (s *Server) Run(c *cli.Context) error { 32 | log.WithField("Interface", c.GlobalString("interface")).Info("Starting Server") 33 | tin := make(chan []byte, 1024) 34 | go tunWriter(s.tun, tin) 35 | rm := &routeManager{isClient: false, routes: make(map[string]chan []byte)} 36 | go rm.tunListener(s.tun, nil) 37 | p := AddressPool{} 38 | p.Setup(c.String("range"), s.tun.Name()) 39 | conf := ClientConfiguration{Network: c.String("client-network")} 40 | 41 | http.HandleFunc("/vpn", func(w http.ResponseWriter, r *http.Request) { 42 | if r.Header.Get("X-WPN-Secret") != c.String("secret") { 43 | w.WriteHeader(http.StatusForbidden) 44 | _, _ = w.Write([]byte("Please authenticate yourself")) 45 | } 46 | cip := p.Get() 47 | if cip == nil { 48 | log.Error("Could not open new connection: No Address available") 49 | _, _ = w.Write([]byte("No Address available")) 50 | return 51 | } 52 | log.WithFields(log.Fields{ 53 | "remote": r.RemoteAddr, 54 | "clientip": cip, 55 | }).Info("New Client connected") 56 | conf.IP = cip.String() 57 | upgrader := websocket.Upgrader{} 58 | c, err := upgrader.Upgrade(w, r, nil) 59 | if err != nil { 60 | log.Fatal(err) 61 | } 62 | defer func() { _ = c.Close() }() 63 | err = c.WriteJSON(conf) 64 | if err != nil { 65 | log.Fatal(err) 66 | } 67 | fail := make(chan error, 16) 68 | tout := make(chan []byte, 1024) 69 | go wsListener(c, tin, fail) 70 | rm.Add(cip, tout) 71 | for { 72 | select { 73 | case packet := <-tout: 74 | err := c.WriteMessage(2, packet) 75 | if err != nil { 76 | log.Fatal(err) 77 | } 78 | case err := <-fail: 79 | log.Error(err) 80 | p.Remove(cip) 81 | return 82 | } 83 | } 84 | }) 85 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 86 | http.Redirect(w, r, "https://github.com/theSuess/wpn", http.StatusTemporaryRedirect) 87 | }) 88 | if c.String("certfile") != "" && c.String("keyfile") != "" { 89 | log.Fatal(http.ListenAndServeTLS(c.String("listen"), c.String("certfile"), c.String("keyfile"), nil)) 90 | } 91 | log.Fatal(http.ListenAndServe(c.String("listen"), nil)) 92 | return nil 93 | } 94 | 95 | func (r *routeManager) Add(ip *net.IP, rec chan []byte) { 96 | r.routes[ip.String()] = rec 97 | } 98 | 99 | func (r *routeManager) tunListener(tun *water.Interface, out chan []byte) { 100 | packet := make([]byte, BUFFERSIZE) 101 | for { 102 | plen, err := tun.Read(packet) 103 | if err != nil { 104 | log.Error(err) 105 | continue 106 | } 107 | header, err := ipv4.ParseHeader(packet[:plen]) 108 | if err != nil { 109 | log.Error(err) 110 | continue 111 | } 112 | log.WithFields(log.Fields{ 113 | "Source": header.Src.String(), 114 | "Dest": header.Dst.String(), 115 | "SourceChan": "tun", 116 | }).Debug("Received Packet") 117 | if r.isClient { 118 | out <- packet[:plen] 119 | } else { 120 | log.Debugf("Pushing Packet to %s", header.Dst.String()) 121 | c := r.routes[header.Dst.String()] 122 | if c != nil { 123 | c <- packet[:plen] 124 | } 125 | } 126 | } 127 | } 128 | --------------------------------------------------------------------------------