From 224d005a907939a3ff05989cb40a39cd57a0652d Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Mon, 6 Aug 2018 16:55:45 -0600 Subject: [PATCH] v1.0.0: first working version --- LICENSE | 41 +++++++++++++++++++ README.md | 81 ++++++++++++++++++++++++++++++++++++++ sclient-cli.go | 82 ++++++++++++++++++++++++++++++++++++++ sclient.go | 104 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 308 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 sclient-cli.go create mode 100644 sclient.go diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..693448a --- /dev/null +++ b/LICENSE @@ -0,0 +1,41 @@ +Copyright 2018 AJ ONeal + +This is open source software; you can redistribute it and/or modify it under the +terms of either: + + a) the "MIT License" + b) the "Apache-2.0 License" + +MIT License + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + +Apache-2.0 License Summary + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..03627cd --- /dev/null +++ b/README.md @@ -0,0 +1,81 @@ +sclient.go +========== + +Secure Client for exposing TLS (aka SSL) secured services as plain-text connections locally. + +Also ideal for multiplexing a single port with multiple protocols using SNI. + +Unwrap a TLS connection: + +```bash +$ sclient whatever.com:443 localhost:3000 +> [listening] telebit.cloud:443 <= localhost:3000 +``` + +Connect via Telnet + +```bash +$ telnet localhost 3000 +``` + +Connect via netcat (nc) + +```bash +$ nc localhost 3000 +``` + +A poor man's (or Windows user's) makeshift replacement for `openssl s_client`, `stunnel`, or `socat`. + +Install +======= + +### macOS, Linux, Windows + +For the moment you'll have to install go and compile `sclient` yourself: + +* + +```bash +git clone +go build sclient*.go +rsync -av sclient-cli /usr/local/bin/sclient +``` + +```bash +go run sclient*.go example.com:443 localhost:3000 +``` + +Usage +===== + +```bash +sclient [-k | --insecure] +``` + +* remote + * must have servername (i.e. example.com) + * port is optional (default is 443) +* local + * address is optional (default is localhost) + * must have port (i.e. 3000) + +Examples +======== + +Bridge between `telebit.cloud` and local port `3000`. + +```bash +sclient telebit.cloud 3000 +``` + +Same as above, but more explicit + +```bash +sclient telebit.cloud:443 localhost:3000 +``` + +Ignore a bad TLS/SSL/HTTPS certificate and connect anyway. + +```bash +sclient badtls.telebit.cloud:443 localhost:3000 -k +``` diff --git a/sclient-cli.go b/sclient-cli.go new file mode 100644 index 0000000..d566cdc --- /dev/null +++ b/sclient-cli.go @@ -0,0 +1,82 @@ +package main + +import ( + "flag" + "fmt" + "os" + "strconv" + "strings" +) + +func usage() { + fmt.Fprintf(os.Stderr, "\nusage: go run sclient*.go \n"+ + "\n"+ + " ex: sclient example.com 3000\n"+ + " (sclient example.com:443 localhost:3000)\n"+ + "\n"+ + " ex: sclient example.com:8443 0.0.0.0:4080\n"+ + "\n") + flag.PrintDefaults() + fmt.Println() +} + +func main() { + flag.Usage = usage + insecure := flag.Bool("k", false, "ignore bad TLS/SSL/HTTPS certificates") + flag.BoolVar(insecure, "insecure", false, "ignore bad TLS/SSL/HTTPS certificates") + flag.Parse() + + // NArg, Arg, Args + i := flag.NArg() + if 2 != i { + usage() + os.Exit(0) + } + + opts := &SclientOpts{} + opts.RemotePort = 443 + opts.LocalAddress = "localhost" + opts.InsecureSkipVerify = *insecure + + remote := strings.Split(flag.Arg(0), ":") + //remoteAddr, remotePort, err := net.SplitHostPort(flag.Arg(0)) + if 2 == len(remote) { + rport, err := strconv.Atoi(remote[1]) + if nil != err { + usage() + os.Exit(0) + } + opts.RemotePort = rport + } else if 1 != len(remote) { + usage() + os.Exit(0) + } + opts.RemoteAddress = remote[0] + + local := strings.Split(flag.Arg(1), ":") + //localAddr, localPort, err := net.SplitHostPort(flag.Arg(0)) + + if 1 == len(local) { + lport, err := strconv.Atoi(local[0]) + if nil != err { + usage() + os.Exit(0) + } + opts.LocalPort = lport + } else { + lport, err := strconv.Atoi(local[1]) + if nil != err { + usage() + os.Exit(0) + } + opts.LocalAddress = local[0] + opts.LocalPort = lport + } + + sclient := &Sclient{} + err := sclient.DialAndListen(opts) + if nil != err { + usage() + os.Exit(0) + } +} diff --git a/sclient.go b/sclient.go new file mode 100644 index 0000000..cb4f19c --- /dev/null +++ b/sclient.go @@ -0,0 +1,104 @@ +package main + +import ( + "crypto/tls" + "fmt" + "io" + "net" + "os" + "strconv" + "strings" +) + +type SclientOpts struct { + RemoteAddress string + RemotePort int + LocalAddress string + LocalPort int + InsecureSkipVerify bool +} + +type Sclient struct{} + +func pipe(r net.Conn, w net.Conn, t string) { + buffer := make([]byte, 2048) + for { + done := false + // NOTE: count may be > 0 even if there's an err + count, err := r.Read(buffer) + //fmt.Fprintf(os.Stdout, "[debug] (%s) reading\n", t) + if nil != err { + //fmt.Fprintf(os.Stdout, "[debug] (%s:%d) error reading %s\n", t, count, err) + if io.EOF != err { + fmt.Fprintf(os.Stderr, "[read error] (%s:%s) %s\n", t, count, err) + } + r.Close() + //w.Close() + done = true + } + if 0 == count { + break + } + _, err = w.Write(buffer[:count]) + if nil != err { + //fmt.Fprintf(os.Stdout, "[debug] %s error writing\n", t) + if io.EOF != err { + fmt.Fprintf(os.Stderr, "[write error] (%s) %s\n", t, err) + } + // TODO handle error closing? + r.Close() + //w.Close() + done = true + } + if done { + break + } + } +} + +func handleConnection(remote string, conn net.Conn, opts *SclientOpts) { + sclient, err := tls.Dial("tcp", remote, + &tls.Config{InsecureSkipVerify: opts.InsecureSkipVerify}) + + if err != nil { + fmt.Fprintf(os.Stderr, "[error] (remote) %s\n", err) + conn.Close() + return + } + + fmt.Fprintf(os.Stdout, "[connect] %s => %s:%d\n", + strings.Replace(conn.RemoteAddr().String(), "[::1]:", "localhost:", 1), opts.RemoteAddress, opts.RemotePort) + + go pipe(conn, sclient, "local") + pipe(sclient, conn, "remote") +} + +func (*Sclient) DialAndListen(opts *SclientOpts) error { + remote := opts.RemoteAddress + ":" + strconv.Itoa(opts.RemotePort) + conn, err := tls.Dial("tcp", remote, + &tls.Config{InsecureSkipVerify: opts.InsecureSkipVerify}) + + if err != nil { + fmt.Fprintf(os.Stderr, "[warn] '%s' may not be accepting connections: %s\n", remote, err) + } else { + conn.Close() + } + + local := opts.LocalAddress + ":" + strconv.Itoa(opts.LocalPort) + ln, err := net.Listen("tcp", local) + if err != nil { + return err + } + + fmt.Fprintf(os.Stdout, "[listening] %s:%d <= %s:%d\n", + opts.RemoteAddress, opts.RemotePort, opts.LocalAddress, opts.LocalPort) + + for { + conn, err := ln.Accept() + if nil != err { + fmt.Fprintf(os.Stderr, "[error] %s\n", err) + continue + } + go handleConnection(remote, conn, opts) + } +}