├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── extra └── logo.psd ├── main.go ├── mux.go ├── shovel.go ├── ssh.go └── tcp.go /.gitignore: -------------------------------------------------------------------------------- 1 | switcher 2 | switcher-* 3 | *.tar.gz 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, James Cunningham 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the switcher nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL JAMES CUNNINGHAM BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build release 2 | 3 | build: 4 | go build -o switcher *.go 5 | 6 | release: 7 | GOOS=linux GOARCH=amd64 go build -o switcher-linux-x64 8 | GOOS=darwin GOARCH=amd64 go build -o switcher-darwin-x64 9 | GOOS=windows GOARCH=amd64 go build -o switcher-windows-x64.exe 10 | tar czvf switcher-linux-x64.tar.gz switcher-linux-x64 README.md LICENSE 11 | tar czvf switcher-darwin-x64.tar.gz switcher-darwin-x64 README.md LICENSE 12 | tar czvf switcher-windows-x64.tar.gz switcher-windows-x64.exe README.md LICENSE 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Switcher 2 | 3 | Switcher 4 | ======== 5 | 6 | Switcher is a proxy server which accepts connections and proxies based on which protocol is detected. 7 | 8 | Currently implemented is: 9 | 10 | - SSH 11 | 12 | The use case is running HTTP(S) and SSH on the same port. 13 | 14 | 15 | Usage 16 | ----- 17 | 18 | [Download release](https://github.com/jamescun/switcher/releases) or Build: 19 | 20 | make 21 | 22 | To get help: 23 | 24 | $ ./switcher --help 25 | Switcher 1.0.0 26 | usage: switcher [options] 27 | 28 | Options: 29 | --listen <:80> Server Listen Address 30 | --ssh <127.0.0.1:22> SSH Server Address 31 | --default <127.0.0.1:8080> Default Server Address 32 | 33 | Examples: 34 | To serve SSH(127.0.0.1:22) and HTTP(127.0.0.1:8080) on port 80 35 | $ switcher 36 | 37 | To serve SSH(127.0.0.1:2222) and HTTPS(127.0.0.1:443) on port 443 38 | $ switcher --listen :443 --ssh 127.0.0.1:2222 --default 127.0.0.1:443 39 | 40 | 41 | Example 42 | ------- 43 | 44 | Run switcher on HTTP port 80, proxy to SSH on 127.0.0.1:22 and Nginx on 127.0.0.1:8080 45 | 46 | $ switcher --listen :80 --ssh 127.0.0.1:22 --default 127.0.0.1:8080 47 | 48 | To test HTTP: 49 | 50 | $ curl -I http://my-server.local 51 | HTTP/1.1 200 OK 52 | 53 | To test SSH 54 | 55 | $ ssh james@my-server.local -p 80 56 | Password: 57 | 58 | 59 | Why not sslh 60 | ------------ 61 | 62 | Switcher is heavily influenced by [sslh](https://github.com/yrutschle/sslh). It started out as a learning exercise to discover how sslh worked and attempt an implementation in Go. 63 | 64 | The result is useful in its own right through use of Go's interfaces for protocol matching (making adding new protocols trivial), and lightweight goroutines (instead of forking, which is more CPU intensive under load). 65 | 66 | 67 | License 68 | ------- 69 | 70 | 3-Clause "Modified" BSD Licence. 71 | 72 | [License](LICENSE) 73 | -------------------------------------------------------------------------------- /extra/logo.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamescun/switcher/c9d4705ac0d500b100b94a0f69cfd40d85b29901/extra/logo.psd -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | ) 8 | 9 | var ( 10 | listenAddress = flag.String("listen", ":80", "Server Listen Address") 11 | sshAddress = flag.String("ssh", "127.0.0.1:22", "SSH Server Address") 12 | defaultAddress = flag.String("default", "127.0.0.1:8080", "Default Server Address") 13 | ) 14 | 15 | func usage() { 16 | fmt.Println("Switcher 1.0.1") 17 | fmt.Println("usage: switcher [options]\n") 18 | 19 | fmt.Println("Options:") 20 | fmt.Println(" --listen <:80> Server Listen Address") 21 | fmt.Println(" --ssh <127.0.0.1:22> SSH Server Address") 22 | fmt.Println(" --default <127.0.0.1:8080> Default Server Address\n") 23 | 24 | fmt.Println("Examples:") 25 | fmt.Println(" To serve SSH(127.0.0.1:22) and HTTP(127.0.0.1:8080) on port 80") 26 | fmt.Println(" $ switcher\n") 27 | 28 | fmt.Println(" To serve SSH(127.0.0.1:2222) and HTTPS(127.0.0.1:443) on port 443") 29 | fmt.Println(" $ switcher --listen :443 --ssh 127.0.0.1:2222 --default 127.0.0.1:443") 30 | } 31 | 32 | func main() { 33 | flag.Usage = usage 34 | flag.Parse() 35 | 36 | mux := NewMux() 37 | 38 | mux.Handle(SSH(*sshAddress)) 39 | mux.Handle(TCP(*defaultAddress)) 40 | 41 | log.Printf("[INFO] listen: %s\n", *listenAddress) 42 | err := mux.ListenAndServe(*listenAddress) 43 | if err != nil { 44 | log.Fatalf("[FATAL] listen: %s\n", err) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /mux.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "net" 7 | ) 8 | 9 | type Protocol interface { 10 | // address to proxy to 11 | Address() string 12 | 13 | // identify protocol from header 14 | Identify(header []byte) bool 15 | } 16 | 17 | type Mux struct { 18 | Handlers []Protocol 19 | } 20 | 21 | // create a new Mux assignment 22 | func NewMux() *Mux { 23 | return &Mux{} 24 | } 25 | 26 | // add a protocol to mux handler set 27 | func (m *Mux) Handle(p Protocol) { 28 | m.Handlers = append(m.Handlers, p) 29 | } 30 | 31 | // match protocol to handler 32 | // returns address to proxy to 33 | func (m *Mux) Identify(header []byte) (address string) { 34 | if len(m.Handlers) < 1 { 35 | return "" 36 | } 37 | 38 | for _, handler := range m.Handlers { 39 | if handler.Identify(header) { 40 | return handler.Address() 41 | } 42 | } 43 | 44 | // return address of last handler, default 45 | return m.Handlers[len(m.Handlers)-1].Address() 46 | } 47 | 48 | // create a server on given address and handle incoming connections 49 | func (m *Mux) ListenAndServe(addr string) error { 50 | server, err := net.Listen("tcp", addr) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | for { 56 | conn, err := server.Accept() 57 | if err != nil { 58 | return err 59 | } 60 | 61 | go m.Serve(conn) 62 | } 63 | 64 | return nil 65 | } 66 | 67 | // serve takes an incomming connection, applies configured protocol 68 | // handlers and proxies the connection based on result 69 | func (m *Mux) Serve(conn net.Conn) error { 70 | defer conn.Close() 71 | 72 | // get first 3 bytes of connection as header 73 | header := make([]byte, 3) 74 | if _, err := io.ReadAtLeast(conn, header, 3); err != nil { 75 | return err 76 | } 77 | 78 | // identify protocol from header 79 | address := m.Identify(header) 80 | 81 | log.Printf("[INFO] proxy: from=%s to=%s\n", conn.RemoteAddr(), address) 82 | 83 | // connect to remote 84 | remote, err := net.Dial("tcp", address) 85 | if err != nil { 86 | log.Printf("[ERROR] remote: %s\n", err) 87 | return err 88 | } 89 | defer remote.Close() 90 | 91 | // write header we chopped back to remote 92 | remote.Write(header) 93 | 94 | // proxy between us and remote server 95 | err = Shovel(conn, remote) 96 | if err != nil { 97 | return err 98 | } 99 | 100 | return nil 101 | } 102 | -------------------------------------------------------------------------------- /shovel.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | ) 6 | 7 | // proxy between two sockets 8 | func Shovel(local, remote io.ReadWriteCloser) error { 9 | errch := make(chan error, 1) 10 | 11 | go chanCopy(errch, local, remote) 12 | go chanCopy(errch, remote, local) 13 | 14 | for i := 0; i < 2; i++ { 15 | if err := <-errch; err != nil { 16 | // If this returns early the second func will push into the 17 | // buffer, and the GC will clean up 18 | return err 19 | } 20 | } 21 | return nil 22 | } 23 | 24 | // copy between pipes, sending errors to channel 25 | func chanCopy(e chan error, dst, src io.ReadWriter) { 26 | _, err := io.Copy(dst, src) 27 | e <- err 28 | } 29 | -------------------------------------------------------------------------------- /ssh.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | ) 6 | 7 | type SSH string 8 | 9 | // address to proxy to 10 | func (s SSH) Address() string { 11 | return string(s) 12 | } 13 | 14 | // identify header as one of SSH 15 | func (s SSH) Identify(header []byte) bool { 16 | // first 3 bytes of 1.0/2.0 is literal `SSH` 17 | if bytes.Compare(header, []byte("SSH")) == 0 { 18 | return true 19 | } 20 | 21 | return false 22 | } 23 | -------------------------------------------------------------------------------- /tcp.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type TCP string 4 | 5 | // address to proxy to 6 | func (t TCP) Address() string { 7 | return string(t) 8 | } 9 | 10 | // identify header as one of TCP 11 | func (t TCP) Identify(header []byte) bool { 12 | // this is a dummy protocol handler used for the default 13 | return false 14 | } 15 | --------------------------------------------------------------------------------