mirror of
https://github.com/therootcompany/sclient
synced 2025-10-07 09:28:19 +00:00
Compare commits
7 Commits
Author | SHA1 | Date | |
---|---|---|---|
d806c1853c | |||
3a2792dcd1 | |||
bc4aeb3124 | |||
8783d46849 | |||
b27b32700c | |||
aac3959dc7 | |||
acaa3d3537 |
@ -3,25 +3,34 @@ before:
|
|||||||
- go mod download
|
- go mod download
|
||||||
- go generate ./...
|
- go generate ./...
|
||||||
builds:
|
builds:
|
||||||
- main: ./cmd/sclient/main.go
|
- main: ./cmd/sclient/
|
||||||
env:
|
env:
|
||||||
- CGO_ENABLED=0
|
- CGO_ENABLED=0
|
||||||
goos:
|
goos:
|
||||||
- linux
|
|
||||||
- windows
|
|
||||||
- darwin
|
- darwin
|
||||||
|
- linux
|
||||||
|
- freebsd
|
||||||
|
- windows
|
||||||
|
- js
|
||||||
goarch:
|
goarch:
|
||||||
- 386
|
|
||||||
- amd64
|
- amd64
|
||||||
- arm
|
- arm
|
||||||
- arm64
|
- arm64
|
||||||
|
- wasm
|
||||||
goarm:
|
goarm:
|
||||||
- 6
|
|
||||||
- 7
|
- 7
|
||||||
|
goamd64:
|
||||||
|
- v2
|
||||||
|
ignore:
|
||||||
|
- goos: windows
|
||||||
|
goarch: 386
|
||||||
|
- goos: windows
|
||||||
|
goarm: 6
|
||||||
|
- goos: windows
|
||||||
|
goarm: 7
|
||||||
archives:
|
archives:
|
||||||
- replacements:
|
- id: sclient-binary
|
||||||
386: i386
|
format: tar.xz
|
||||||
amd64: x86_64
|
|
||||||
format_overrides:
|
format_overrides:
|
||||||
- goos: windows
|
- goos: windows
|
||||||
format: zip
|
format: zip
|
||||||
|
8
.prettierrc.json
Normal file
8
.prettierrc.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"printWidth": 80,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"singleQuote": false,
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"proseWrap": "always",
|
||||||
|
"semi": true
|
||||||
|
}
|
38
README.md
38
README.md
@ -1,6 +1,7 @@
|
|||||||
# sclient
|
# sclient
|
||||||
|
|
||||||
Secure Client for exposing TLS (aka SSL) secured services as plain-text connections locally.
|
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.
|
Also ideal for multiplexing a single port with multiple protocols using SNI.
|
||||||
|
|
||||||
@ -30,7 +31,8 @@ cURL
|
|||||||
curl http://localhost:3000 -H 'Host: whatever.com'
|
curl http://localhost:3000 -H 'Host: whatever.com'
|
||||||
```
|
```
|
||||||
|
|
||||||
A poor man's (or Windows user's) makeshift replacement for `openssl s_client`, `stunnel`, or `socat`.
|
A poor man's (or Windows user's) makeshift replacement for `openssl s_client`,
|
||||||
|
`stunnel`, or `socat`.
|
||||||
|
|
||||||
# Table of Contents
|
# Table of Contents
|
||||||
|
|
||||||
@ -53,9 +55,11 @@ curl.exe -A MS https://webinstall.dev/sclient | powershell
|
|||||||
|
|
||||||
### Downloads
|
### Downloads
|
||||||
|
|
||||||
Check the [Github Releases](https://github.com/therootcompany/sclient/releases) for
|
Check the [Github Releases](https://github.com/therootcompany/sclient/releases)
|
||||||
|
for
|
||||||
|
|
||||||
- macOS (x64) Apple Silicon [coming soon](https://github.com/golang/go/issues/39782)
|
- macOS (x64) Apple Silicon
|
||||||
|
[coming soon](https://github.com/golang/go/issues/39782)
|
||||||
- Linux (x64, i386, arm64, arm6, arm7)
|
- Linux (x64, i386, arm64, arm6, arm7)
|
||||||
- Windows 10 (x64, i386)
|
- Windows 10 (x64, i386)
|
||||||
|
|
||||||
@ -66,8 +70,11 @@ sclient [flags] <remote> <local>
|
|||||||
```
|
```
|
||||||
|
|
||||||
- flags
|
- flags
|
||||||
- -k, --insecure ignore invalid TLS (SSL/HTTPS) certificates
|
- `-s`, `--silent` less verbose logging
|
||||||
- --servername <string> spoof SNI (to disable use IP as <remote> and do not use this option)
|
- `-k`, `--insecure` ignore invalid TLS (SSL/HTTPS) certificates
|
||||||
|
- `--servername <domain>` spoof SNI (to disable use IP as <remote> and do
|
||||||
|
not use this option)
|
||||||
|
- `--alpn <protocol-list>`
|
||||||
- remote
|
- remote
|
||||||
- must have servername (i.e. example.com)
|
- must have servername (i.e. example.com)
|
||||||
- port is optional (default is 443)
|
- port is optional (default is 443)
|
||||||
@ -75,6 +82,17 @@ sclient [flags] <remote> <local>
|
|||||||
- address is optional (default is localhost)
|
- address is optional (default is localhost)
|
||||||
- must have port (i.e. 3000)
|
- must have port (i.e. 3000)
|
||||||
|
|
||||||
|
-alpn string
|
||||||
|
acceptable protocols, ex: 'h2,http/1.1' 'http/1.1' (default) 'ssh' (default "http/1.1")
|
||||||
|
-insecure
|
||||||
|
ignore bad TLS/SSL/HTTPS certificates
|
||||||
|
-k alias for --insecure
|
||||||
|
-s alias of --silent
|
||||||
|
-servername string
|
||||||
|
specify a servername different from <remote> (to disable SNI use an IP as <remote> and do use this option)
|
||||||
|
-silent
|
||||||
|
less verbose output
|
||||||
|
|
||||||
# Examples
|
# Examples
|
||||||
|
|
||||||
Bridge between `telebit.cloud` and local port `3000`.
|
Bridge between `telebit.cloud` and local port `3000`.
|
||||||
@ -121,10 +139,14 @@ sclient --servername "Robert'); DROP TABLE Students;" -k example.com localhost:3
|
|||||||
sclient --servername "../../../.hidden/private.txt" -k example.com localhost:3000
|
sclient --servername "../../../.hidden/private.txt" -k example.com localhost:3000
|
||||||
```
|
```
|
||||||
|
|
||||||
|
# API
|
||||||
|
|
||||||
|
See [Go Docs](https://pkg.go.dev/github.com/therootcompany/sclient).
|
||||||
|
|
||||||
# Build from source
|
# Build from source
|
||||||
|
|
||||||
You'll need to install [Go](https://golang.org).
|
You'll need to install [Go](https://golang.org). See
|
||||||
See [webinstall.dev/golang](https://webinstall.dev/golang) for install instructions.
|
[webinstall.dev/golang](https://webinstall.dev/golang) for install instructions.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -sS https://webinstall.dev/golang | bash
|
curl -sS https://webinstall.dev/golang | bash
|
||||||
|
@ -40,27 +40,37 @@ func usage() {
|
|||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if len(os.Args) >= 2 {
|
if len(os.Args) >= 2 {
|
||||||
if "version" == strings.TrimLeft(os.Args[1], "-") {
|
if os.Args[1] == "-V" || strings.TrimLeft(os.Args[1], "-") == "version" {
|
||||||
fmt.Printf("%s\n", ver())
|
fmt.Printf("%s\n", ver())
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var alpnList string
|
||||||
|
var insecure bool
|
||||||
|
var servername string
|
||||||
|
var silent bool
|
||||||
|
|
||||||
flag.Usage = usage
|
flag.Usage = usage
|
||||||
insecure := flag.Bool("k", false, "alias for --insecure")
|
|
||||||
silent := flag.Bool("s", false, "alias of --silent")
|
flag.StringVar(&alpnList, "alpn", "", "acceptable protocols, ex: 'h2,http/1.1' 'http/1.1' 'ssh'")
|
||||||
servername := flag.String("servername", "", "specify a servername different from <remote> (to disable SNI use an IP as <remote> and do use this option)")
|
flag.BoolVar(&insecure, "k", false, "alias for --insecure")
|
||||||
flag.BoolVar(insecure, "insecure", false, "ignore bad TLS/SSL/HTTPS certificates")
|
flag.BoolVar(&silent, "s", false, "alias of --silent")
|
||||||
flag.BoolVar(silent, "silent", false, "less verbose output")
|
flag.StringVar(&servername, "servername", "", "specify a servername different from <remote> (to disable SNI use an IP as <remote> and do not use this option)")
|
||||||
|
flag.BoolVar(&insecure, "insecure", false, "ignore bad TLS/SSL/HTTPS certificates")
|
||||||
|
flag.BoolVar(&silent, "silent", false, "less verbose output")
|
||||||
|
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
|
alpns := parseOptionList(alpnList)
|
||||||
remotestr := flag.Arg(0)
|
remotestr := flag.Arg(0)
|
||||||
localstr := flag.Arg(1)
|
localstr := flag.Arg(1)
|
||||||
|
|
||||||
i := flag.NArg()
|
i := flag.NArg()
|
||||||
if 2 != i {
|
if i != 2 {
|
||||||
// We may omit the second argument if we're going straight to stdin
|
// We may omit the second argument if we're going straight to stdin
|
||||||
if stat, _ := os.Stdin.Stat(); 1 == i && (stat.Mode()&os.ModeCharDevice) == 0 {
|
if stat, _ := os.Stdin.Stat(); i == 1 && (stat.Mode()&os.ModeCharDevice) == 0 {
|
||||||
localstr = "|"
|
localstr = "|"
|
||||||
} else {
|
} else {
|
||||||
usage()
|
usage()
|
||||||
@ -71,27 +81,28 @@ func main() {
|
|||||||
sclient := &sclient.Tunnel{
|
sclient := &sclient.Tunnel{
|
||||||
RemotePort: 443,
|
RemotePort: 443,
|
||||||
LocalAddress: "localhost",
|
LocalAddress: "localhost",
|
||||||
InsecureSkipVerify: *insecure,
|
InsecureSkipVerify: insecure,
|
||||||
ServerName: *servername,
|
ServerName: servername,
|
||||||
Silent: *silent,
|
Silent: silent,
|
||||||
|
NextProtos: alpns,
|
||||||
}
|
}
|
||||||
|
|
||||||
remote := strings.Split(remotestr, ":")
|
remote := strings.Split(remotestr, ":")
|
||||||
//remoteAddr, remotePort, err := net.SplitHostPort(remotestr)
|
//remoteAddr, remotePort, err := net.SplitHostPort(remotestr)
|
||||||
if 2 == len(remote) {
|
if len(remote) == 2 {
|
||||||
rport, err := strconv.Atoi(remote[1])
|
rport, err := strconv.Atoi(remote[1])
|
||||||
if nil != err {
|
if nil != err {
|
||||||
usage()
|
usage()
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
sclient.RemotePort = rport
|
sclient.RemotePort = rport
|
||||||
} else if 1 != len(remote) {
|
} else if len(remote) != 1 {
|
||||||
usage()
|
usage()
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
sclient.RemoteAddress = remote[0]
|
sclient.RemoteAddress = remote[0]
|
||||||
|
|
||||||
if "-" == localstr || "|" == localstr {
|
if localstr == "-" || localstr == "|" {
|
||||||
// User may specify stdin/stdout instead of net
|
// User may specify stdin/stdout instead of net
|
||||||
sclient.LocalAddress = localstr
|
sclient.LocalAddress = localstr
|
||||||
sclient.LocalPort = -1
|
sclient.LocalPort = -1
|
||||||
@ -99,7 +110,7 @@ func main() {
|
|||||||
// Test that argument is a local address
|
// Test that argument is a local address
|
||||||
local := strings.Split(localstr, ":")
|
local := strings.Split(localstr, ":")
|
||||||
|
|
||||||
if 1 == len(local) {
|
if len(local) == 1 {
|
||||||
lport, err := strconv.Atoi(local[0])
|
lport, err := strconv.Atoi(local[0])
|
||||||
if nil != err {
|
if nil != err {
|
||||||
usage()
|
usage()
|
||||||
@ -124,3 +135,17 @@ func main() {
|
|||||||
//os.Exit(6)
|
//os.Exit(6)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parsers "a,b,c" "a b c" and "a, b, c" all the same
|
||||||
|
func parseOptionList(optionList string) []string {
|
||||||
|
optionList = strings.TrimSpace(optionList)
|
||||||
|
|
||||||
|
if len(optionList) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
optionList = strings.ReplaceAll(optionList, ",", " ")
|
||||||
|
options := strings.Fields(optionList)
|
||||||
|
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
25
sclient.go
25
sclient.go
@ -17,6 +17,7 @@ type Tunnel struct {
|
|||||||
LocalAddress string
|
LocalAddress string
|
||||||
LocalPort int
|
LocalPort int
|
||||||
InsecureSkipVerify bool
|
InsecureSkipVerify bool
|
||||||
|
NextProtos []string
|
||||||
ServerName string
|
ServerName string
|
||||||
Silent bool
|
Silent bool
|
||||||
}
|
}
|
||||||
@ -28,20 +29,21 @@ func (t *Tunnel) DialAndListen() error {
|
|||||||
conn, err := tls.Dial("tcp", remote,
|
conn, err := tls.Dial("tcp", remote,
|
||||||
&tls.Config{
|
&tls.Config{
|
||||||
ServerName: t.ServerName,
|
ServerName: t.ServerName,
|
||||||
|
NextProtos: t.NextProtos,
|
||||||
InsecureSkipVerify: t.InsecureSkipVerify,
|
InsecureSkipVerify: t.InsecureSkipVerify,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "[warn] '%s' may not be accepting connections: %s\n", remote, err)
|
fmt.Fprintf(os.Stderr, "[warn] '%s' may not be accepting connections: %s\n", remote, err)
|
||||||
} else {
|
} else {
|
||||||
conn.Close()
|
_ = conn.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
// use stdin/stdout
|
// use stdin/stdout
|
||||||
if "-" == t.LocalAddress || "|" == t.LocalAddress {
|
if t.LocalAddress == "-" || t.LocalAddress == "|" {
|
||||||
var name string
|
var name string
|
||||||
network := "stdio"
|
network := "stdio"
|
||||||
if "|" == t.LocalAddress {
|
if t.LocalAddress == "|" {
|
||||||
name = "pipe"
|
name = "pipe"
|
||||||
} else {
|
} else {
|
||||||
name = "stdin"
|
name = "stdin"
|
||||||
@ -59,7 +61,7 @@ func (t *Tunnel) DialAndListen() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !t.Silent {
|
if !t.Silent {
|
||||||
fmt.Fprintf(os.Stdout, "[listening] %s:%d <= %s:%d\n",
|
_, _ = fmt.Fprintf(os.Stdout, "[listening] %s:%d <= %s:%d\n",
|
||||||
t.RemoteAddress, t.RemotePort, t.LocalAddress, t.LocalPort)
|
t.RemoteAddress, t.RemotePort, t.LocalAddress, t.LocalPort)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,11 +117,11 @@ func pipe(r netReadWriteCloser, w netReadWriteCloser, t string) {
|
|||||||
if io.EOF != err {
|
if io.EOF != err {
|
||||||
fmt.Fprintf(os.Stderr, "[read error] (%s:%d) %s\n", t, count, err)
|
fmt.Fprintf(os.Stderr, "[read error] (%s:%d) %s\n", t, count, err)
|
||||||
}
|
}
|
||||||
r.Close()
|
_ = r.Close()
|
||||||
//w.Close()
|
//w.Close()
|
||||||
done = true
|
done = true
|
||||||
}
|
}
|
||||||
if 0 == count {
|
if count == 0 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
_, err = w.Write(buffer[:count])
|
_, err = w.Write(buffer[:count])
|
||||||
@ -129,7 +131,7 @@ func pipe(r netReadWriteCloser, w netReadWriteCloser, t string) {
|
|||||||
fmt.Fprintf(os.Stderr, "[write error] (%s) %s\n", t, err)
|
fmt.Fprintf(os.Stderr, "[write error] (%s) %s\n", t, err)
|
||||||
}
|
}
|
||||||
// TODO handle error closing?
|
// TODO handle error closing?
|
||||||
r.Close()
|
_ = r.Close()
|
||||||
//w.Close()
|
//w.Close()
|
||||||
done = true
|
done = true
|
||||||
}
|
}
|
||||||
@ -143,21 +145,22 @@ func (t *Tunnel) handleConnection(remote string, conn netReadWriteCloser) {
|
|||||||
sclient, err := tls.Dial("tcp", remote,
|
sclient, err := tls.Dial("tcp", remote,
|
||||||
&tls.Config{
|
&tls.Config{
|
||||||
ServerName: t.ServerName,
|
ServerName: t.ServerName,
|
||||||
|
NextProtos: t.NextProtos,
|
||||||
InsecureSkipVerify: t.InsecureSkipVerify,
|
InsecureSkipVerify: t.InsecureSkipVerify,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "[error] (remote) %s\n", err)
|
fmt.Fprintf(os.Stderr, "[error] (remote) %s\n", err)
|
||||||
conn.Close()
|
_ = conn.Close()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !t.Silent {
|
if !t.Silent {
|
||||||
if "stdio" == conn.RemoteAddr().Network() {
|
if conn.RemoteAddr().Network() == "stdio" {
|
||||||
fmt.Fprintf(os.Stdout, "(connected to %s:%d and reading from %s)\n",
|
_, _ = fmt.Fprintf(os.Stdout, "(connected to %s:%d and reading from %s)\n",
|
||||||
t.RemoteAddress, t.RemotePort, conn.RemoteAddr().String())
|
t.RemoteAddress, t.RemotePort, conn.RemoteAddr().String())
|
||||||
} else {
|
} else {
|
||||||
fmt.Fprintf(os.Stdout, "[connect] %s => %s:%d\n",
|
_, _ = fmt.Fprintf(os.Stdout, "[connect] %s => %s:%d\n",
|
||||||
strings.Replace(conn.RemoteAddr().String(), "[::1]:", "localhost:", 1), t.RemoteAddress, t.RemotePort)
|
strings.Replace(conn.RemoteAddr().String(), "[::1]:", "localhost:", 1), t.RemoteAddress, t.RemotePort)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user