├── LICENSE ├── README.md ├── example_conf.json └── serve2d.go /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Kenny Levinsen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # serve2d [![Go Report Card](https://goreportcard.com/badge/kennylevinsen/serve2d)](https://goreportcard.com/report/kennylevinsen/serve2d) 2 | 3 | A protocol detecting server, based off the https://github.com/kennylevinsen/serve2 library. Scroll down for installation and usage info. 4 | 5 | You don't like having to have to decide what port to use for a service? Maybe you're annoyed by a firewall that only allows traffic to port 80? Or even a packet inspecting one that only allows real TLS traffic on port 443, but you want to SSH through none the less? 6 | 7 | Welcome to serve2, a protocol recognizing and stacking server/dispatcher. 8 | 9 | serve2 allows you to serve multiple protocols on a single socket. Example handlers include proxy, HTTP, TLS (through which HTTPS is handled), ECHO and DISCARD. More can easily be added, as long as the protocol sends some data that can be recognized. The proxy handler allows you to redirect the connection to external services, such as OpenSSH or Nginx, in case you don't want or can't use a Go implementation. In most cases, proxy will be sufficient. 10 | 11 | So, what does this mean? Well, it means that if you run serve2 for port 22, 80 and 443 (or all the ports, although I would suggest just having your firewall redirect things in that case, rather than having 65535 listening sockets), you could ask for HTTP(S) on port 22, SSH on port 80, and SSH over TLS (Meaning undetectable without a MITM attack) on port 443! You have to admit that it's kind of neat. 12 | 13 | All bytes read by serve2 are of course fed into whatever ends up having to handle the protocol. For more details on the protocol matching, look at the serve2 library directly. 14 | 15 | # Installation 16 | serve2d can either be installed from source (super simple with Go), or by downloading a prepackaged binary release. 17 | To download source (requires Go 1.4.2 or above): 18 | 19 | go get github.com/kennylevinsen/serve2d 20 | 21 | It can be run with: 22 | 23 | cd $GOPATH/src/github.com/kennylevinsen/serve2d 24 | go build 25 | ./serve2d example_conf.json 26 | 27 | Arch Linux also has serve2d in the AUR: https://aur.archlinux.org/packages/serve2d/ 28 | 29 | Or, use the unstable version: https://aur.archlinux.org/packages/serve2d-git/ 30 | 31 | # Limitations 32 | serve2, and by extension, serve2d, can only detect protocols initiated by the client. That is, protocols where the client starts out by blindly sending a unique blob that can be used to identify the protocol. 33 | 34 | # What's up with the name? 35 | I called the first toy version "serve", and needed to call the new directory in my development folder something else, so it became serve2. 'd' was added to this configurable front-end (daemon), to distinguish it from the library. 36 | 37 | # Usage 38 | Due to potentially large amounts of parameters, serve2d consumes a json configuration file. The only parameter taken by serve2d is the name of this file. The format is as follows: 39 | 40 | ``` 41 | { 42 | // Listening address as given directly to net.Listen. 43 | "address": ":80", 44 | 45 | // Maximum read size for protocol detection before fallback or failure. 46 | // Defaults to 128. 47 | "maxRead": 10, 48 | 49 | // Logging to stdout. 50 | // Defaults to false. 51 | "logStdout": true, 52 | 53 | // Logging to file. Note that only one logging destination can be 54 | // enabled at a given time. 55 | // Defaults to empty string, meaning disabled. 56 | "logFile": "serve2d.log", 57 | 58 | // The enabled ProtocolHandlers. 59 | "protocols": [ 60 | { 61 | // Name of the ProtocolHandler. 62 | "kind": "proxy", 63 | 64 | // Setting this flag to true means that this ProtocolHandler 65 | // will not be used in protocol detection, but instead be used 66 | // as a fallback in case of failed detection. 67 | // Defaults to false. 68 | "default": false, 69 | 70 | // Protocol-specific configuration. 71 | // Defaults to empty. 72 | "conf": { 73 | "magic": "SSH", 74 | "target": "localhost:22" 75 | } 76 | } 77 | ] 78 | } 79 | ``` 80 | 81 | # ProtocolHandlers 82 | 83 | ## proxy 84 | Simply dials another service to handle the protocol. Matches protocol using the user-defined string. If an array of strings is provided, then MultiProxy will be used instead of Proxy internally, which will try to match any of the provided strings, from shortest to longest, progressively requesting more data as necessary. 85 | 86 | * magic (string or []string): The bytes to look for in order to identify the protocol. Example: "SSH" or ["GET", "POST", "HEAD"] 87 | * target (string): The address as given directly to net.Dial to call the real service. Example: "localhost:22". 88 | 89 | ## tls 90 | Looks for a TLS1.0-1.3 ClientHello handshake, and feeds it into Go's TLS handling. The resulting net.Conn is fed back through the protocol detection, allowing for any other supported protocol to go over TLS. 91 | The certificates required can be generated with http://golang.org/src/crypto/tls/generate_cert.go. 92 | 93 | * cert (string): The certificate PEM file path to use for the server. Example: "cert.pem". 94 | * key (string): The key PEM file path to use for the server. Example: "key.pem". 95 | * protos ([]string): The protocols the TLS server will advertise support for in the handshake. Example: ["http/1.1", "ssh"] 96 | 97 | As tls works as a transport, it can be used for anything, not just HTTP. tls + proxy handler for SSH would make it possible to do the following to grant you stealthy SSH over TLS, which would be indistinguishable from HTTPS traffic: 98 | 99 | ssh -oProxyCommand="openssl s_client -connect %h:%p -tls1 -quiet" -p443 serve2dhost 100 | 101 | Alternatively, using http://github.com/kennylevinsen/tunnel, one can do: 102 | 103 | ssh -oProxyCommand="tunnel - tls:%h:%p" -p443 serve2dhost 104 | 105 | ## tlsmatcher 106 | Looks for already established TLS transports, allowing for checks against some of the connection properties, such as SNI. 107 | 108 | * negotiatedProtocols (string): The protocol to look for. Defaults to no check. Example: ["h2", "h2-14"]. 109 | * negotiatedProtocolIsMutual (bool): Check if the protocol was one that was advertised or not.Defaults to no check. Example: true. 110 | * serverNames (string): The SNI server name to look for. Defaults to no check. Example: ["http2.golang.org"]. 111 | * target (string): The target to dial upon a match. Example: "http2.golang.org:443". 112 | * dialTLS (bool): Whether or not to use TLS when dialing. This also copies servername and protocol. Example: true. 113 | 114 | ## http 115 | Simple file-server without directory listing (might change in the future). It guards against navigating out of the directory with some simple path magic. It identifies HTTP traffic by checking for possible methods ("GET", "PUT", "HEAD", "POST", "TRACE", "PATCH", "DELETE", "OPTIONS", "CONNECT"). Forwarding to another HTTP server can be done by just putting this list of methods in as magics for a "proxy" handler. 116 | 117 | * path (string): Path to serve from. Example: "/srv/http/" 118 | * defaultFile (string, optional): File to serve for /. Example: "index.html" 119 | * notFoundMsg (string, optional): 404 body. Example: "Not Found" 120 | * notFoundFile (string, optional): 404 file. Example: "404.html" 121 | 122 | ## echo 123 | A test protocol. Requires that the client starts out by sending "ECHO" (which will by echoed by itself, of course). No configuration. 124 | 125 | ## discard 126 | Same as DISCARD, start by sending "DISCARD". No configuration. If you feel silly, try DISCARD over TLS! 127 | 128 | # More info 129 | For more details about this project, see the underlying library: http://github.com/kennylevinsen/serve2 130 | -------------------------------------------------------------------------------- /example_conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "address": ":8080", 3 | "maxRead": 32, 4 | "logStdout": true, 5 | "protocols": [ 6 | { 7 | "kind": "echo" 8 | }, 9 | { 10 | "kind": "discard", 11 | "default": true 12 | }, 13 | { 14 | "kind": "proxy", 15 | "conf": { 16 | "magic": "SSH", 17 | "target": "localhost:22" 18 | } 19 | }, 20 | { 21 | "kind": "http", 22 | "conf": { 23 | "path": "." 24 | } 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /serve2d.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "log" 9 | "net" 10 | "net/http" 11 | "os" 12 | "path" 13 | 14 | "github.com/kennylevinsen/serve2" 15 | "github.com/kennylevinsen/serve2/proto" 16 | "github.com/kennylevinsen/serve2/utils" 17 | ) 18 | 19 | var ( 20 | server *serve2.Server 21 | conf Config 22 | confReady bool 23 | logger func(string, ...interface{}) 24 | ) 25 | 26 | // Config is the top-level config 27 | type Config struct { 28 | Address string 29 | LogStdout bool `json:"logStdout,omitempty"` 30 | LogFile string `json:"logFile,omitempty"` 31 | MaxRead int `json:"maxRead,omitempty"` 32 | Protocols []Protocol 33 | } 34 | 35 | // Protocol is the part of config defining individual protocols 36 | type Protocol struct { 37 | Kind string 38 | AsDefault bool `json:"default,omitempty"` 39 | Conf map[string]interface{} `json:"conf,omitempty"` 40 | } 41 | 42 | func logit(format string, msg ...interface{}) { 43 | defer func() { 44 | if r := recover(); r != nil { 45 | println("Log failed: ", r) 46 | panic(r) 47 | } 48 | }() 49 | 50 | if logger != nil || !confReady { 51 | log.Printf(format, msg...) 52 | } 53 | } 54 | 55 | type httpHandler struct { 56 | path, defaultFile, notFoundMsg string 57 | } 58 | 59 | func (h httpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 60 | if r.Method == "OPTIONS" { 61 | return 62 | } 63 | 64 | // We add the "./" to make things relative 65 | p := "." + path.Clean(r.URL.Path) 66 | 67 | if p == "./" { 68 | p += h.defaultFile 69 | } 70 | // We then put the origin on there 71 | p = path.Join(h.path, p) 72 | 73 | content, err := ioutil.ReadFile(p) 74 | if err != nil { 75 | logit("http could not read file %s: %v", p, err) 76 | w.WriteHeader(404) 77 | fmt.Fprintf(w, "%s", h.notFoundMsg) 78 | return 79 | } 80 | 81 | fmt.Fprintf(w, "%s", content) 82 | } 83 | 84 | func main() { 85 | defer func() { 86 | if err := recover(); err != nil { 87 | logit("Panicked: %s", err) 88 | } 89 | }() 90 | 91 | if len(os.Args) <= 1 { 92 | panic("Missing configuration path") 93 | } 94 | 95 | path := os.Args[1] 96 | 97 | bytes, err := ioutil.ReadFile(path) 98 | if err != nil { 99 | logit("Reading configuration failed") 100 | panic(err) 101 | } 102 | 103 | err = json.Unmarshal(bytes, &conf) 104 | if err != nil { 105 | logit("Parsing configuration failed") 106 | panic(err) 107 | } 108 | 109 | confReady = true 110 | 111 | server = serve2.New() 112 | 113 | if conf.LogStdout && conf.LogFile != "" { 114 | panic("Unable to both log to stdout and to logfile") 115 | } 116 | 117 | if conf.LogStdout || conf.LogFile != "" { 118 | if conf.LogFile != "" { 119 | file, err := os.OpenFile(conf.LogFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) 120 | if err != nil { 121 | logit("Failed to open logfile: %s", conf.LogFile) 122 | panic(err) 123 | } 124 | log.SetOutput(file) 125 | } 126 | 127 | logger = log.Printf 128 | server.Logger = log.Printf 129 | } 130 | 131 | if conf.MaxRead != 0 { 132 | server.BytesToCheck = conf.MaxRead 133 | } 134 | 135 | logit("Maximum buffer size: %d", server.BytesToCheck) 136 | 137 | l, err := net.Listen("tcp", conf.Address) 138 | if err != nil { 139 | logit("Listen on [%s] failed", conf.Address) 140 | panic(err) 141 | } 142 | 143 | logit("Listening on: %s", conf.Address) 144 | 145 | for _, v := range conf.Protocols { 146 | var ( 147 | handler serve2.ProtocolHandler 148 | err error 149 | ) 150 | switch v.Kind { 151 | case "proxy": 152 | magic, mok := v.Conf["magic"].(string) 153 | magicSlice, sok := v.Conf["magic"].([]interface{}) 154 | if !mok && !sok { 155 | panic("Proxy declaration is missing valid magic") 156 | } 157 | 158 | target, ok := v.Conf["target"].(string) 159 | if !ok { 160 | panic("Proxy declaration is missing valid target") 161 | } 162 | 163 | if mok { 164 | handler = proto.NewProxy([]byte(magic), "tcp", target) 165 | } else { 166 | magics := make([][]byte, len(magicSlice)) 167 | for i := range magicSlice { 168 | magic, ok := magicSlice[i].(string) 169 | if !ok { 170 | panic("magic declaration invalid") 171 | } 172 | magics[i] = []byte(magic) 173 | } 174 | handler = proto.NewMultiProxy(magics, "tcp", target) 175 | } 176 | case "tls": 177 | cert, ok := v.Conf["cert"].(string) 178 | if !ok { 179 | panic("TLS declaration is missing valid certificate") 180 | } 181 | 182 | key, ok := v.Conf["key"].(string) 183 | if !ok { 184 | panic("TLS declaration is missing valid key") 185 | } 186 | 187 | var protos []string 188 | y, ok := v.Conf["protos"].([]interface{}) 189 | if !ok { 190 | panic("TLS protos declaration invalid") 191 | } 192 | 193 | for _, x := range y { 194 | proto, ok := x.(string) 195 | if !ok { 196 | panic("TLS protos declaration invalid") 197 | } 198 | protos = append(protos, proto) 199 | } 200 | 201 | handler, err = proto.NewTLS(protos, cert, key) 202 | if err != nil { 203 | logit("TLS configuration failed") 204 | panic(err) 205 | } 206 | case "tlsmatcher": 207 | target, ok := v.Conf["target"].(string) 208 | if !ok { 209 | panic("TLSMatcher declaration is missing valid target") 210 | } 211 | 212 | var cb func(net.Conn) (net.Conn, error) 213 | dialTLS, ok := v.Conf["dialTLS"].(bool) 214 | if !ok || !dialTLS { 215 | cb = func(c net.Conn) (net.Conn, error) { 216 | return nil, utils.DialAndProxy(c, "tcp", target) 217 | } 218 | } else { 219 | cb = func(c net.Conn) (net.Conn, error) { 220 | serverName := "" 221 | proto := "" 222 | hints := utils.GetHints(c) 223 | if len(hints) > 0 { 224 | if tc, ok := hints[len(hints)-1].(*tls.Conn); ok { 225 | cs := tc.ConnectionState() 226 | serverName = cs.ServerName 227 | proto = cs.NegotiatedProtocol 228 | } 229 | } 230 | 231 | return nil, utils.DialAndProxyTLS(c, "tcp", target, &tls.Config{ 232 | ServerName: serverName, 233 | NextProtos: []string{proto}, 234 | InsecureSkipVerify: true, 235 | }) 236 | } 237 | } 238 | 239 | t := proto.NewTLSMatcher(cb) 240 | 241 | var checks proto.TLSMatcherChecks 242 | if sn, ok := v.Conf["serverNames"].([]interface{}); ok { 243 | checks |= proto.TLSCheckServerName 244 | t.ServerNames = make([]string, len(sn)) 245 | for i, x := range sn { 246 | s, ok := x.(string) 247 | if !ok { 248 | panic("TLSMatcher serverNames declaration invalid") 249 | } 250 | t.ServerNames[i] = s 251 | } 252 | } 253 | 254 | if np, ok := v.Conf["negotiatedProtocols"].([]interface{}); ok { 255 | checks |= proto.TLSCheckNegotiatedProtocol 256 | t.NegotiatedProtocols = make([]string, len(np)) 257 | for i, x := range np { 258 | n, ok := x.(string) 259 | if !ok { 260 | panic("TLSMatcher negotiatedProtocols declaration invalid") 261 | } 262 | t.NegotiatedProtocols[i] = n 263 | } 264 | } 265 | 266 | if npm, ok := v.Conf["negotiatedProtocolIsMutual"].(bool); ok { 267 | checks |= proto.TLSCheckNegotiatedProtocolIsMutual 268 | t.NegotiatedProtocolIsMutual = npm 269 | } 270 | 271 | t.Checks = checks 272 | t.Description = fmt.Sprintf("TLSMatcher [dest: %s]", target) 273 | handler = t 274 | case "http": 275 | h := httpHandler{} 276 | msg, msgOk := v.Conf["notFoundMsg"] 277 | filename, fileOk := v.Conf["notFoundFile"] 278 | if fileOk && msgOk { 279 | panic("HTTP notFoundMsg and notFoundFile declared simultaneously") 280 | } 281 | 282 | if !msgOk && !fileOk { 283 | h.notFoundMsg = "

404

" 284 | } else if msgOk { 285 | h.notFoundMsg, msgOk = msg.(string) 286 | if !msgOk { 287 | panic("HTTP notFoundMsg declaration invalid") 288 | } 289 | } else if fileOk { 290 | f, ok := filename.(string) 291 | if !ok { 292 | panic("HTTP notFoundFile declaration invalid") 293 | } 294 | 295 | x, err := ioutil.ReadFile(f) 296 | if err != nil { 297 | logit("HTTP unable to open notFoundFile") 298 | panic(err) 299 | } 300 | h.notFoundMsg = string(x) 301 | } 302 | 303 | c, ok := v.Conf["defaultFile"] 304 | if !ok { 305 | h.defaultFile = "index.html" 306 | } else { 307 | h.defaultFile, ok = c.(string) 308 | if !ok { 309 | panic("HTTP defaultFile declaration invalid") 310 | } 311 | } 312 | 313 | h.path, ok = v.Conf["path"].(string) 314 | if !ok { 315 | panic("HTTP path declaration invalid") 316 | } 317 | 318 | handler = proto.NewHTTP(h) 319 | case "echo": 320 | handler = proto.NewEcho() 321 | case "discard": 322 | handler = proto.NewDiscard() 323 | default: 324 | panic("Unknown kind: " + v.Kind) 325 | } 326 | 327 | if v.AsDefault { 328 | server.DefaultProtocol = handler 329 | } else { 330 | server.AddHandler(handler) 331 | } 332 | } 333 | 334 | if server.DefaultProtocol != nil { 335 | logit("Default protocol set to: %v", server.DefaultProtocol) 336 | } 337 | 338 | server.Serve(l) 339 | } 340 | --------------------------------------------------------------------------------