v1.0.0: first working version

This commit is contained in:
AJ ONeal 2018-08-06 16:55:45 -06:00
commit 224d005a90
4 changed files with 308 additions and 0 deletions

41
LICENSE Normal file
View File

@ -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.

81
README.md Normal file
View File

@ -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:
* <https://golang.org/doc/install#install>
```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 <remote> <local> [-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
```

82
sclient-cli.go Normal file
View File

@ -0,0 +1,82 @@
package main
import (
"flag"
"fmt"
"os"
"strconv"
"strings"
)
func usage() {
fmt.Fprintf(os.Stderr, "\nusage: go run sclient*.go <remote> <local>\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)
}
}

104
sclient.go Normal file
View File

@ -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)
}
}