├── .gitignore ├── go.mod ├── util.go ├── go.sum ├── proxy.go ├── main.go ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | lecat 2 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jtolio/lecat 2 | 3 | go 1.14 4 | 5 | require golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a 6 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 JT Olds 2 | // See LICENSE for copying information 3 | 4 | package main 5 | 6 | import ( 7 | "net" 8 | "time" 9 | ) 10 | 11 | type tcpKeepAliveListener struct { 12 | *net.TCPListener 13 | } 14 | 15 | func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) { 16 | tc, err := ln.AcceptTCP() 17 | if err != nil { 18 | return nil, err 19 | } 20 | tc.SetKeepAlive(true) 21 | tc.SetKeepAlivePeriod(3 * time.Minute) 22 | return tc, nil 23 | } 24 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 2 | golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM= 3 | golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 4 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= 5 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 6 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 7 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 8 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 9 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 10 | -------------------------------------------------------------------------------- /proxy.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 JT Olds 2 | // See LICENSE for copying information 3 | 4 | package main 5 | 6 | import ( 7 | "crypto/tls" 8 | "io" 9 | "log" 10 | "net" 11 | "syscall" 12 | "time" 13 | 14 | "golang.org/x/crypto/acme" 15 | "golang.org/x/crypto/acme/autocert" 16 | ) 17 | 18 | func handleConn(inc net.Conn) { 19 | defer inc.Close() 20 | 21 | log.Printf("incoming connection from %s", inc.RemoteAddr()) 22 | 23 | outc, err := net.Dial("tcp", *targetAddr) 24 | if err != nil { 25 | log.Println("failed forwarding request:", err) 26 | return 27 | } 28 | defer outc.Close() 29 | 30 | done := make(chan bool, 2) 31 | go proxy(outc, inc, done) 32 | go proxy(inc, outc, done) 33 | <-done 34 | } 35 | 36 | func proxy(outc io.Writer, inc io.Reader, done chan bool) { 37 | _, err := io.Copy(outc, inc) 38 | if err != nil && !isClosedConn(err) { 39 | log.Println("error forwarding stream:", err) 40 | } 41 | done <- true 42 | } 43 | 44 | func isClosedConn(err error) bool { 45 | if err == nil { 46 | return false 47 | } 48 | operr, ok := err.(*net.OpError) 49 | if !ok { 50 | return false 51 | } 52 | if operr.Err == syscall.ECONNRESET { 53 | return true 54 | } 55 | if operr.Err.Error() == "use of closed network connection" { 56 | return true 57 | } 58 | return false 59 | } 60 | 61 | func serve(manager *autocert.Manager) error { 62 | base_l, err := net.Listen("tcp", *listenAddr) 63 | if err != nil { 64 | log.Fatalf("failed listening on %s", *listenAddr) 65 | } 66 | defer base_l.Close() 67 | 68 | log.Printf("listening on %s", base_l.Addr()) 69 | 70 | nextProtos := make([]string, 0, 3) 71 | if *supportHTTP2 { 72 | nextProtos = append(nextProtos, "h2") 73 | } 74 | nextProtos = append(nextProtos, "http/1.1", acme.ALPNProto) 75 | 76 | l := tls.NewListener( 77 | tcpKeepAliveListener{ 78 | TCPListener: base_l.(*net.TCPListener)}, 79 | &tls.Config{ 80 | GetCertificate: manager.GetCertificate, 81 | NextProtos: nextProtos, 82 | }) 83 | 84 | var delay time.Duration 85 | for { 86 | conn, err := l.Accept() 87 | if err != nil { 88 | if nerr, ok := err.(net.Error); ok && nerr.Temporary() { 89 | if delay == 0 { 90 | delay = 5 * time.Millisecond 91 | } else { 92 | delay *= 2 93 | } 94 | if delay > time.Second { 95 | delay = time.Second 96 | } 97 | time.Sleep(delay) 98 | continue 99 | } 100 | return err 101 | } 102 | delay = 0 103 | go handleConn(conn) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 JT Olds 2 | // See LICENSE for copying information 3 | 4 | package main 5 | 6 | import ( 7 | "bufio" 8 | "flag" 9 | "fmt" 10 | "log" 11 | "net/http" 12 | "os" 13 | "runtime" 14 | "time" 15 | 16 | "golang.org/x/crypto/acme/autocert" 17 | ) 18 | 19 | var ( 20 | reachableHost = flag.String("host", "", 21 | "the hostname to get a certificate for. something like "+ 22 | "'yourservice.yourdomain.tld'") 23 | listenAddr = flag.String("addr", ":443", "the address to listen on. "+ 24 | "this almost certainly should be ':443'") 25 | targetAddr = flag.String("target", "", 26 | "the address to forward unencrypted connections to. "+ 27 | "probably something like 'localhost:8080'") 28 | statePath = flag.String("path", os.ExpandEnv("$HOME/.lecat"), 29 | "the path to a folder to keep state.") 30 | redirectAddr = flag.String("redirect-addr", "", 31 | "if set, will listen on this address and redirect unencrypted http "+ 32 | "requests to it to https AND set HSTS. if you want this at "+ 33 | "all, you almost certainly want ':80'") 34 | hstsDuration = flag.Duration("hsts-duration", 24*time.Hour, 35 | "if redirect-addr is set, length of time to trigger hsts") 36 | acceptTOS = flag.Bool("accept-tos", false, "if true, accept lets encrypt TOS") 37 | supportHTTP2 = flag.Bool("support-http2", false, 38 | "if true, indicate http2 support in the ssl handshake") 39 | email = flag.String("email", "", "the email address to use with letsencrypt") 40 | ) 41 | 42 | func AcceptTOS(url string) bool { 43 | if *acceptTOS { 44 | return true 45 | } 46 | fmt.Println("Do you accept the Let's Encrypt terms of service?") 47 | fmt.Println(url) 48 | fmt.Println("Press enter to accept, control-c to reject") 49 | _, err := bufio.NewReader(os.Stdin).ReadString('\n') 50 | return err == nil 51 | } 52 | 53 | func main() { 54 | flag.Parse() 55 | runtime.GOMAXPROCS(runtime.NumCPU() + 1) 56 | 57 | if *reachableHost == "" { 58 | log.Fatal("--host argument required") 59 | } 60 | 61 | if *targetAddr == "" { 62 | log.Fatal("--target argument required") 63 | } 64 | 65 | manager := &autocert.Manager{ 66 | Prompt: AcceptTOS, 67 | HostPolicy: autocert.HostWhitelist(*reachableHost), 68 | Email: *email, 69 | } 70 | 71 | log.Println("loading configuration") 72 | 73 | err := os.MkdirAll(*statePath, 0700) 74 | if err != nil { 75 | log.Fatal("failed to make state path:", err) 76 | } 77 | manager.Cache = autocert.DirCache(*statePath) 78 | 79 | if *redirectAddr != "" { 80 | go func() { 81 | log.Printf("redirecting on %s", *redirectAddr) 82 | panic(http.ListenAndServe(*redirectAddr, http.HandlerFunc( 83 | func(w http.ResponseWriter, req *http.Request) { 84 | log.Printf("redirecting request from %s and setting HSTS", 85 | req.RemoteAddr) 86 | w.Header().Set("Strict-Transport-Security", fmt.Sprintf("max-age=%d", int64(hstsDuration.Seconds()))) 87 | http.Redirect(w, req, 88 | fmt.Sprintf("https://%s%s", *reachableHost, req.RequestURI), 89 | http.StatusMovedPermanently) 90 | }))) 91 | }() 92 | } 93 | 94 | panic(serve(manager)) 95 | } 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lecat 2 | 3 | socat-lite with letsencrypt support 4 | 5 | ## Why? 6 | 7 | I often find myself running web services on unprivileged ports such as 8000, 8 | 8080, etc, and then later decide I want to access these things on port 80. 9 | In these cases, I often run something like 10 | `sudo socat TCP-LISTEN:80,fork,reuseaddr TCP:localhost:8080`. What this does 11 | is start a small process that listens on port 80 and forwards all incoming 12 | connections to my process on port 8080. 13 | 14 | Unfortunately, this isn't HTTPS or SSL. It'd be nice to be able to run a small 15 | binary like socat that listens on 443, does SSL termination, and redirects the 16 | actual unencrypted traffic to localhost:8080. You can do this, too, with 17 | socat, but certs are just such a hassle. OR WERE! 18 | 19 | With the advent of [Let's Encrypt](https://letsencrypt.org/), having a small 20 | binary that actually does the entire process of making a key, getting a 21 | valid certificate, and doing the proxying is now possible! 22 | 23 | `lecat` is this thing. 24 | 25 | ## Example Usage 26 | 27 | All you gotta do is tell lecat the domain your process is visible from and the 28 | local unencrypted port to forward to. 29 | 30 | ``` 31 | lecat --host your.website.tld --target localhost:8080 32 | ``` 33 | 34 | An example session: 35 | 36 | ``` 37 | $ ./my-unprivileged-thing.py --listen localhost:8080 & 38 | $ go get github.com/jtolds/lecat 39 | $ sudo ~/your/gopath/bin/lecat --host your.website.tld --target localhost:8080 40 | 2016/02/07 07:12:25 loading configuration 41 | 2016/02/07 07:12:25 no key found at /root/.lecat/server.key, generating 42 | 2016/02/07 07:12:35 no cert found at /root/.lecat/server.crt, requesting 43 | 2016/02/07 07:12:35 no key found at /root/.lecat/account.key, generating 44 | 2016/02/07 07:12:44 (re)registering account key 45 | 2016/02/07 07:12:44 getting challenges for "your.website.tld" 46 | 2016/02/07 07:12:45 performing sni challenge 47 | 2016/02/07 07:12:46 waiting for challenge 48 | 2016/02/07 07:12:47 making csr 49 | 2016/02/07 07:12:47 getting cert 50 | 2016/02/07 07:12:47 listening on [::]:443 51 | ``` 52 | 53 | Running it again will reload existing keys and certificates: 54 | 55 | ``` 56 | $ sudo ~/your/gopath/bin/lecat --host your.website.tld --target localhost:8080 57 | 2016/02/07 07:19:13 loading configuration 58 | 2016/02/07 07:19:14 listening on [::]:443 59 | ``` 60 | 61 | Lastly, you can also pass `--redirect-addr :80` to have the process start a 62 | small HTTP server listening on port 80 that redirects incoming unencrypted 63 | requests to HTTPS. Be aware that this little HTTP server will set the HSTS 64 | flag on redirected requests, telling incoming browsers to never 65 | try HTTP again for the configured period. If you use this setting and this isn't 66 | the behavior that you want, you'll probably need to clear your domain out of 67 | your browser's HSTS database. Or just keep using SSL. 68 | 69 | ### sudo? 70 | 71 | lecat doesn't really need sudo, it just needs 72 | `setcap 'cap_net_bind_service=+ep' go/bin/lecat`. 73 | 74 | ## LICENSE 75 | 76 | Copyright 2016 JT Olds 77 | 78 | Licensed under the Apache License, Version 2.0 (the "License"); 79 | you may not use this file except in compliance with the License. 80 | You may obtain a copy of the License at 81 | 82 | http://www.apache.org/licenses/LICENSE-2.0 83 | 84 | Unless required by applicable law or agreed to in writing, software 85 | distributed under the License is distributed on an "AS IS" BASIS, 86 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 87 | See the License for the specific language governing permissions and 88 | limitations under the License. 89 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016 JT Olds 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | 15 | 16 | 17 | Apache License 18 | Version 2.0, January 2004 19 | http://www.apache.org/licenses/ 20 | 21 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 22 | 23 | 1. Definitions. 24 | 25 | "License" shall mean the terms and conditions for use, reproduction, 26 | and distribution as defined by Sections 1 through 9 of this document. 27 | 28 | "Licensor" shall mean the copyright owner or entity authorized by 29 | the copyright owner that is granting the License. 30 | 31 | "Legal Entity" shall mean the union of the acting entity and all 32 | other entities that control, are controlled by, or are under common 33 | control with that entity. For the purposes of this definition, 34 | "control" means (i) the power, direct or indirect, to cause the 35 | direction or management of such entity, whether by contract or 36 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 37 | outstanding shares, or (iii) beneficial ownership of such entity. 38 | 39 | "You" (or "Your") shall mean an individual or Legal Entity 40 | exercising permissions granted by this License. 41 | 42 | "Source" form shall mean the preferred form for making modifications, 43 | including but not limited to software source code, documentation 44 | source, and configuration files. 45 | 46 | "Object" form shall mean any form resulting from mechanical 47 | transformation or translation of a Source form, including but 48 | not limited to compiled object code, generated documentation, 49 | and conversions to other media types. 50 | 51 | "Work" shall mean the work of authorship, whether in Source or 52 | Object form, made available under the License, as indicated by a 53 | copyright notice that is included in or attached to the work 54 | (an example is provided in the Appendix below). 55 | 56 | "Derivative Works" shall mean any work, whether in Source or Object 57 | form, that is based on (or derived from) the Work and for which the 58 | editorial revisions, annotations, elaborations, or other modifications 59 | represent, as a whole, an original work of authorship. For the purposes 60 | of this License, Derivative Works shall not include works that remain 61 | separable from, or merely link (or bind by name) to the interfaces of, 62 | the Work and Derivative Works thereof. 63 | 64 | "Contribution" shall mean any work of authorship, including 65 | the original version of the Work and any modifications or additions 66 | to that Work or Derivative Works thereof, that is intentionally 67 | submitted to Licensor for inclusion in the Work by the copyright owner 68 | or by an individual or Legal Entity authorized to submit on behalf of 69 | the copyright owner. For the purposes of this definition, "submitted" 70 | means any form of electronic, verbal, or written communication sent 71 | to the Licensor or its representatives, including but not limited to 72 | communication on electronic mailing lists, source code control systems, 73 | and issue tracking systems that are managed by, or on behalf of, the 74 | Licensor for the purpose of discussing and improving the Work, but 75 | excluding communication that is conspicuously marked or otherwise 76 | designated in writing by the copyright owner as "Not a Contribution." 77 | 78 | "Contributor" shall mean Licensor and any individual or Legal Entity 79 | on behalf of whom a Contribution has been received by Licensor and 80 | subsequently incorporated within the Work. 81 | 82 | 2. Grant of Copyright License. Subject to the terms and conditions of 83 | this License, each Contributor hereby grants to You a perpetual, 84 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 85 | copyright license to reproduce, prepare Derivative Works of, 86 | publicly display, publicly perform, sublicense, and distribute the 87 | Work and such Derivative Works in Source or Object form. 88 | 89 | 3. Grant of Patent License. Subject to the terms and conditions of 90 | this License, each Contributor hereby grants to You a perpetual, 91 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 92 | (except as stated in this section) patent license to make, have made, 93 | use, offer to sell, sell, import, and otherwise transfer the Work, 94 | where such license applies only to those patent claims licensable 95 | by such Contributor that are necessarily infringed by their 96 | Contribution(s) alone or by combination of their Contribution(s) 97 | with the Work to which such Contribution(s) was submitted. If You 98 | institute patent litigation against any entity (including a 99 | cross-claim or counterclaim in a lawsuit) alleging that the Work 100 | or a Contribution incorporated within the Work constitutes direct 101 | or contributory patent infringement, then any patent licenses 102 | granted to You under this License for that Work shall terminate 103 | as of the date such litigation is filed. 104 | 105 | 4. Redistribution. You may reproduce and distribute copies of the 106 | Work or Derivative Works thereof in any medium, with or without 107 | modifications, and in Source or Object form, provided that You 108 | meet the following conditions: 109 | 110 | (a) You must give any other recipients of the Work or 111 | Derivative Works a copy of this License; and 112 | 113 | (b) You must cause any modified files to carry prominent notices 114 | stating that You changed the files; and 115 | 116 | (c) You must retain, in the Source form of any Derivative Works 117 | that You distribute, all copyright, patent, trademark, and 118 | attribution notices from the Source form of the Work, 119 | excluding those notices that do not pertain to any part of 120 | the Derivative Works; and 121 | 122 | (d) If the Work includes a "NOTICE" text file as part of its 123 | distribution, then any Derivative Works that You distribute must 124 | include a readable copy of the attribution notices contained 125 | within such NOTICE file, excluding those notices that do not 126 | pertain to any part of the Derivative Works, in at least one 127 | of the following places: within a NOTICE text file distributed 128 | as part of the Derivative Works; within the Source form or 129 | documentation, if provided along with the Derivative Works; or, 130 | within a display generated by the Derivative Works, if and 131 | wherever such third-party notices normally appear. The contents 132 | of the NOTICE file are for informational purposes only and 133 | do not modify the License. You may add Your own attribution 134 | notices within Derivative Works that You distribute, alongside 135 | or as an addendum to the NOTICE text from the Work, provided 136 | that such additional attribution notices cannot be construed 137 | as modifying the License. 138 | 139 | You may add Your own copyright statement to Your modifications and 140 | may provide additional or different license terms and conditions 141 | for use, reproduction, or distribution of Your modifications, or 142 | for any such Derivative Works as a whole, provided Your use, 143 | reproduction, and distribution of the Work otherwise complies with 144 | the conditions stated in this License. 145 | 146 | 5. Submission of Contributions. Unless You explicitly state otherwise, 147 | any Contribution intentionally submitted for inclusion in the Work 148 | by You to the Licensor shall be under the terms and conditions of 149 | this License, without any additional terms or conditions. 150 | Notwithstanding the above, nothing herein shall supersede or modify 151 | the terms of any separate license agreement you may have executed 152 | with Licensor regarding such Contributions. 153 | 154 | 6. Trademarks. This License does not grant permission to use the trade 155 | names, trademarks, service marks, or product names of the Licensor, 156 | except as required for reasonable and customary use in describing the 157 | origin of the Work and reproducing the content of the NOTICE file. 158 | 159 | 7. Disclaimer of Warranty. Unless required by applicable law or 160 | agreed to in writing, Licensor provides the Work (and each 161 | Contributor provides its Contributions) on an "AS IS" BASIS, 162 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 163 | implied, including, without limitation, any warranties or conditions 164 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 165 | PARTICULAR PURPOSE. You are solely responsible for determining the 166 | appropriateness of using or redistributing the Work and assume any 167 | risks associated with Your exercise of permissions under this License. 168 | 169 | 8. Limitation of Liability. In no event and under no legal theory, 170 | whether in tort (including negligence), contract, or otherwise, 171 | unless required by applicable law (such as deliberate and grossly 172 | negligent acts) or agreed to in writing, shall any Contributor be 173 | liable to You for damages, including any direct, indirect, special, 174 | incidental, or consequential damages of any character arising as a 175 | result of this License or out of the use or inability to use the 176 | Work (including but not limited to damages for loss of goodwill, 177 | work stoppage, computer failure or malfunction, or any and all 178 | other commercial damages or losses), even if such Contributor 179 | has been advised of the possibility of such damages. 180 | 181 | 9. Accepting Warranty or Additional Liability. While redistributing 182 | the Work or Derivative Works thereof, You may choose to offer, 183 | and charge a fee for, acceptance of support, warranty, indemnity, 184 | or other liability obligations and/or rights consistent with this 185 | License. However, in accepting such obligations, You may act only 186 | on Your own behalf and on Your sole responsibility, not on behalf 187 | of any other Contributor, and only if You agree to indemnify, 188 | defend, and hold each Contributor harmless for any liability 189 | incurred by, or claims asserted against, such Contributor by reason 190 | of your accepting any such warranty or additional liability. 191 | 192 | END OF TERMS AND CONDITIONS 193 | --------------------------------------------------------------------------------