mirror of
https://github.com/therootcompany/sclient
synced 2026-10-30 22:54:56 +00:00
feat: authenticate via HTTP/1.1 before establishing SSH tunnel
Add --http-auth and --auth-token flags to perform HTTP authentication over TLS (with http/1.1 ALPN) before establishing the SSH tunnel. Add --http-upgrade flag to use HTTP Upgrade header mechanism, allowing the proxy to hijack/switch the connection to SSH on the same TLS connection (similar to WebSocket upgrade but without websockets). Two modes: - Without --http-upgrade: Auth connection is closed after auth, then a second TLS+SSH connection is established (two connections) - With --http-upgrade <proto>: After auth, if proxy responds with 101 Switching Protocols + Upgrade: <proto>, the same connection is used for SSH (single connection, protocol switch) No changes to existing behavior when --http-auth is not used.
This commit is contained in:
parent
d806c1853c
commit
01501f4c60
@ -51,6 +51,9 @@ func main() {
|
|||||||
var insecure bool
|
var insecure bool
|
||||||
var servername string
|
var servername string
|
||||||
var silent bool
|
var silent bool
|
||||||
|
var httpAuthURL string
|
||||||
|
var authToken string
|
||||||
|
var httpUpgradeProtocol string
|
||||||
|
|
||||||
flag.Usage = usage
|
flag.Usage = usage
|
||||||
|
|
||||||
@ -60,6 +63,9 @@ func main() {
|
|||||||
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.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(&insecure, "insecure", false, "ignore bad TLS/SSL/HTTPS certificates")
|
||||||
flag.BoolVar(&silent, "silent", false, "less verbose output")
|
flag.BoolVar(&silent, "silent", false, "less verbose output")
|
||||||
|
flag.StringVar(&httpAuthURL, "http-auth", "", "authenticate via HTTP/1.1 before establishing tunnel (ex: 'proxy.example.com/auth')")
|
||||||
|
flag.StringVar(&authToken, "auth-token", "", "authentication token (sent as Bearer token in Authorization header)")
|
||||||
|
flag.StringVar(&httpUpgradeProtocol, "http-upgrade", "", "protocol to upgrade to after authentication (e.g., 'ssh') - uses HTTP Upgrade header to switch protocols on the same connection")
|
||||||
|
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
@ -78,13 +84,32 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
authTokenFromURL := ""
|
||||||
|
if httpAuthURL != "" {
|
||||||
|
if idx := strings.Index(httpAuthURL, "token="); idx != -1 {
|
||||||
|
authTokenFromURL = httpAuthURL[idx+6:]
|
||||||
|
if idx2 := strings.Index(authTokenFromURL, "&"); idx2 != -1 {
|
||||||
|
authTokenFromURL = authTokenFromURL[:idx2]
|
||||||
|
} else if idx2 := strings.Index(authTokenFromURL, " "); idx2 != -1 {
|
||||||
|
authTokenFromURL = authTokenFromURL[:idx2]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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,
|
NextProtos: alpns,
|
||||||
|
HttpAuthURL: httpAuthURL,
|
||||||
|
AuthToken: authToken,
|
||||||
|
HttpUpgradeProtocol: httpUpgradeProtocol,
|
||||||
|
}
|
||||||
|
|
||||||
|
if sclient.AuthToken == "" {
|
||||||
|
sclient.AuthToken = authTokenFromURL
|
||||||
}
|
}
|
||||||
|
|
||||||
remote := strings.Split(remotestr, ":")
|
remote := strings.Split(remotestr, ":")
|
||||||
|
|||||||
149
sclient.go
149
sclient.go
@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@ -12,24 +13,84 @@ import (
|
|||||||
|
|
||||||
// Tunnel specifies which remote encrypted connection to make available as a plain connection locally.
|
// Tunnel specifies which remote encrypted connection to make available as a plain connection locally.
|
||||||
type Tunnel struct {
|
type Tunnel struct {
|
||||||
RemoteAddress string
|
RemoteAddress string
|
||||||
RemotePort int
|
RemotePort int
|
||||||
LocalAddress string
|
LocalAddress string
|
||||||
LocalPort int
|
LocalPort int
|
||||||
InsecureSkipVerify bool
|
InsecureSkipVerify bool
|
||||||
NextProtos []string
|
NextProtos []string
|
||||||
ServerName string
|
ServerName string
|
||||||
Silent bool
|
Silent bool
|
||||||
|
HttpAuthURL string
|
||||||
|
AuthToken string
|
||||||
|
HttpUpgradeProtocol string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tunnel) httpAuth(conn net.Conn) (bool, error) {
|
||||||
|
url := t.HttpAuthURL
|
||||||
|
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
|
||||||
|
url = "http://" + url
|
||||||
|
}
|
||||||
|
parts := strings.SplitN(url, "//", 2)
|
||||||
|
path := "/"
|
||||||
|
if idx := strings.Index(parts[1], "/"); idx != -1 {
|
||||||
|
path = parts[1][idx:]
|
||||||
|
}
|
||||||
|
host := parts[0]
|
||||||
|
|
||||||
|
req, _ := http.NewRequest("GET", path, nil)
|
||||||
|
req.Host = host
|
||||||
|
if t.AuthToken != "" {
|
||||||
|
req.Header.Set("Authorization", "Bearer "+t.AuthToken)
|
||||||
|
}
|
||||||
|
if t.HttpUpgradeProtocol != "" {
|
||||||
|
req.Header.Set("Upgrade", t.HttpUpgradeProtocol)
|
||||||
|
}
|
||||||
|
req.ContentLength = 0
|
||||||
|
req.Close = true
|
||||||
|
|
||||||
|
if err := req.Write(conn); nil != err {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := make([]byte, 4096)
|
||||||
|
n, err := conn.Read(buf)
|
||||||
|
if nil != err {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := string(buf[:n])
|
||||||
|
|
||||||
|
if t.HttpUpgradeProtocol != "" {
|
||||||
|
if strings.Contains(resp, "101") && strings.Contains(strings.ToLower(resp), "upgrade: "+strings.ToLower(t.HttpUpgradeProtocol)) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
if !strings.Contains(resp, "200") && !strings.Contains(resp, "201") && !strings.Contains(resp, "202") && !strings.Contains(resp, "204") {
|
||||||
|
return false, fmt.Errorf("authentication failed: %s", resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(resp, "200") && !strings.Contains(resp, "201") && !strings.Contains(resp, "202") && !strings.Contains(resp, "204") {
|
||||||
|
return false, fmt.Errorf("authentication failed: %s", resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DialAndListen will create a test TLS connection to the remote address and then
|
// DialAndListen will create a test TLS connection to the remote address and then
|
||||||
// begin listening locally. Each local connection will result in a separate remote connection.
|
// begin listening locally. Each local connection will result in a separate remote connection.
|
||||||
func (t *Tunnel) DialAndListen() error {
|
func (t *Tunnel) DialAndListen() error {
|
||||||
remote := t.RemoteAddress + ":" + strconv.Itoa(t.RemotePort)
|
remote := t.RemoteAddress + ":" + strconv.Itoa(t.RemotePort)
|
||||||
|
|
||||||
|
var nextProtos []string = t.NextProtos
|
||||||
|
if t.HttpAuthURL != "" {
|
||||||
|
nextProtos = []string{"http/1.1"}
|
||||||
|
}
|
||||||
|
|
||||||
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,
|
NextProtos: nextProtos,
|
||||||
InsecureSkipVerify: t.InsecureSkipVerify,
|
InsecureSkipVerify: t.InsecureSkipVerify,
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -142,17 +203,67 @@ func pipe(r netReadWriteCloser, w netReadWriteCloser, t string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *Tunnel) handleConnection(remote string, conn netReadWriteCloser) {
|
func (t *Tunnel) handleConnection(remote string, conn netReadWriteCloser) {
|
||||||
sclient, err := tls.Dial("tcp", remote,
|
var sclient net.Conn
|
||||||
&tls.Config{
|
|
||||||
ServerName: t.ServerName,
|
|
||||||
NextProtos: t.NextProtos,
|
|
||||||
InsecureSkipVerify: t.InsecureSkipVerify,
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
if t.HttpAuthURL != "" {
|
||||||
fmt.Fprintf(os.Stderr, "[error] (remote) %s\n", err)
|
authConn, err := tls.Dial("tcp", remote,
|
||||||
_ = conn.Close()
|
&tls.Config{
|
||||||
return
|
ServerName: t.ServerName,
|
||||||
|
NextProtos: []string{"http/1.1"},
|
||||||
|
InsecureSkipVerify: t.InsecureSkipVerify,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "[error] (auth connection) %s\n", err)
|
||||||
|
_ = conn.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
upgraded, err := t.httpAuth(authConn)
|
||||||
|
if nil != err {
|
||||||
|
fmt.Fprintf(os.Stderr, "[error] (authentication) %s\n", err)
|
||||||
|
_ = conn.Close()
|
||||||
|
_ = authConn.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.HttpUpgradeProtocol != "" && upgraded {
|
||||||
|
sclient = authConn
|
||||||
|
} else {
|
||||||
|
_ = authConn.Close()
|
||||||
|
|
||||||
|
var nextProtos []string = t.NextProtos
|
||||||
|
if len(nextProtos) == 0 {
|
||||||
|
nextProtos = []string{"ssh"}
|
||||||
|
}
|
||||||
|
|
||||||
|
sclient, err = tls.Dial("tcp", remote,
|
||||||
|
&tls.Config{
|
||||||
|
ServerName: t.ServerName,
|
||||||
|
NextProtos: nextProtos,
|
||||||
|
InsecureSkipVerify: t.InsecureSkipVerify,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "[error] (remote) %s\n", err)
|
||||||
|
_ = conn.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var dialErr error
|
||||||
|
sclient, dialErr = tls.Dial("tcp", remote,
|
||||||
|
&tls.Config{
|
||||||
|
ServerName: t.ServerName,
|
||||||
|
NextProtos: t.NextProtos,
|
||||||
|
InsecureSkipVerify: t.InsecureSkipVerify,
|
||||||
|
})
|
||||||
|
|
||||||
|
if dialErr != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "[error] (remote) %s\n", dialErr)
|
||||||
|
_ = conn.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !t.Silent {
|
if !t.Silent {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user