Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 61 additions & 1 deletion cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ func printHelp() {
fmt.Println("\t--tls\t\t\tEnable TLS on socket (ssh/http over TLS)")
fmt.Println("\t--tlscert\t\tTLS certificate path")
fmt.Println("\t--tlskey\t\tTLS key path")
fmt.Println("\t--trusted-proxy-cidr\tTrust X-Forwarded-For/X-Real-IP only from this CIDR; may be repeated or comma-separated")
fmt.Println("\t--webserver\t\t(Depreciated) Enable webserver on the listen_address port")
fmt.Println("\t--enable-client-downloads\t\tEnable webserver and raw TCP to download clients")
fmt.Println("\t--external_address\tIf the external IP and port of the RSSH server is different from the listening address, set that here")
Expand All @@ -46,6 +47,7 @@ func main() {
"tls": true,
"tlscert": true,
"tlskey": true,
"trusted-proxy-cidr": true,
"external_address": true,
"fingerprint": true,
"webserver": true, // deprecated
Expand Down Expand Up @@ -164,6 +166,12 @@ func main() {
tls := options.IsSet("tls")
tlscert, _ := options.GetArgString("tlscert")
tlskey, _ := options.GetArgString("tlskey")
trustedProxyCIDRs, err := parseTrustedProxyCIDRs(options)
if err != nil {
fmt.Println(err)
printHelp()
return
}

enabledDownloads := options.IsSet("webserver") || options.IsSet("enable-client-downloads")

Expand Down Expand Up @@ -211,5 +219,57 @@ func main() {

log.Println("connect back: ", connectBackAddress)

server.Run(listenAddress, dataDir, connectBackAddress, autogeneratedConnectBack, tlscert, tlskey, insecure, enabledDownloads, tls, openproxy, timeout)
server.Run(listenAddress, dataDir, connectBackAddress, autogeneratedConnectBack, tlscert, tlskey, insecure, enabledDownloads, tls, openproxy, timeout, trustedProxyCIDRs)
}

func parseTrustedProxyCIDRs(options terminal.ParsedLine) ([]*net.IPNet, error) {
values, err := options.GetArgsString("trusted-proxy-cidr")
if err == terminal.ErrFlagNotSet {
return nil, nil
}
if err != nil {
return nil, err
}
return parseTrustedProxyCIDRValues(values)
}

func parseTrustedProxyCIDRValues(values []string) ([]*net.IPNet, error) {
var cidrs []*net.IPNet
for _, value := range values {
for _, part := range strings.Split(value, ",") {
part = strings.TrimSpace(part)
if part == "" {
continue
}
cidr, err := parseTrustedProxyCIDR(part)
if err != nil {
return nil, err
}
cidrs = append(cidrs, cidr)
}
}
return cidrs, nil
}

func parseTrustedProxyCIDR(value string) (*net.IPNet, error) {
_, cidr, err := net.ParseCIDR(value)
if err == nil {
return cidr, nil
}

ip := net.ParseIP(value)
if ip == nil {
return nil, fmt.Errorf("invalid --trusted-proxy-cidr %q", value)
}
if ip.To4() != nil {
value += "/32"
} else {
value += "/128"
}

_, cidr, err = net.ParseCIDR(value)
if err != nil {
return nil, fmt.Errorf("invalid --trusted-proxy-cidr %q", value)
}
return cidr, nil
}
56 changes: 56 additions & 0 deletions cmd/server/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package main

import (
"sort"
"strings"
"testing"

"github.com/NHAS/reverse_ssh/internal/terminal"
)

func TestParseTrustedProxyCIDRs(t *testing.T) {
line := terminal.ParseLine("--trusted-proxy-cidr 192.0.2.0/24,198.51.100.10 --trusted-proxy-cidr 2001:db8::1", 0)

got, err := parseTrustedProxyCIDRs(line)
if err != nil {
t.Fatal(err)
}

want := []string{"192.0.2.0/24", "198.51.100.10/32", "2001:db8::1/128"}
if len(got) != len(want) {
t.Fatalf("got %d CIDRs, want %d", len(got), len(want))
}
gotStrings := make([]string, 0, len(got))
for _, cidr := range got {
gotStrings = append(gotStrings, cidr.String())
}
sort.Strings(gotStrings)
sort.Strings(want)
for i := range want {
if gotStrings[i] != want[i] {
t.Fatalf("CIDR %d = %q, want %q", i, gotStrings[i], want[i])
}
}
}

func TestParseTrustedProxyCIDRsUnset(t *testing.T) {
line := terminal.ParseLine(":2222", 0)

got, err := parseTrustedProxyCIDRs(line)
if err != nil {
t.Fatal(err)
}
if got != nil {
t.Fatalf("got %v, want nil", got)
}
}

func TestParseTrustedProxyCIDRsInvalid(t *testing.T) {
_, err := parseTrustedProxyCIDRValues([]string{"not-an-ip"})
if err == nil {
t.Fatal("expected invalid CIDR error")
}
if !strings.Contains(err.Error(), "invalid --trusted-proxy-cidr") {
t.Fatalf("error = %q", err)
}
}
15 changes: 9 additions & 6 deletions internal/server/observers/clientstate.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@ import (
)

type ClientState struct {
Status string
ID string
IP string
HostName string
Version string
Timestamp time.Time
Status string
ID string
IP string
HostName string
Version string
Timestamp time.Time
Transport string `json:",omitempty"`
PublicKeyFingerprint string `json:",omitempty"`
ProxySourceIP string `json:",omitempty"`
}

func (cs ClientState) Summary() string {
Expand Down
56 changes: 56 additions & 0 deletions internal/server/observers/clientstate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package observers

import (
"strings"
"testing"
"time"
)

func TestClientStateJSONIncludesOptionalTransportFields(t *testing.T) {
payload, err := ClientState{
Status: "connected",
ID: "id1",
IP: "198.51.100.10:0",
HostName: "host",
Version: "SSH-test",
Timestamp: time.Unix(0, 0).UTC(),
Transport: "wss",
PublicKeyFingerprint: "fp",
ProxySourceIP: "192.0.2.10",
}.Json()
if err != nil {
t.Fatal(err)
}

body := string(payload)
for _, want := range []string{
`"Transport":"wss"`,
`"PublicKeyFingerprint":"fp"`,
`"ProxySourceIP":"192.0.2.10"`,
} {
if !strings.Contains(body, want) {
t.Fatalf("payload missing %s: %s", want, body)
}
}
}

func TestClientStateJSONOmitsEmptyOptionalTransportFields(t *testing.T) {
payload, err := ClientState{
Status: "connected",
ID: "id1",
IP: "198.51.100.10:0",
HostName: "host",
Version: "SSH-test",
Timestamp: time.Unix(0, 0).UTC(),
}.Json()
if err != nil {
t.Fatal(err)
}

body := string(payload)
for _, field := range []string{"Transport", "PublicKeyFingerprint", "ProxySourceIP"} {
if strings.Contains(body, field) {
t.Fatalf("payload should omit %s: %s", field, body)
}
}
}
3 changes: 2 additions & 1 deletion internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func CreateOrLoadServerKeys(privateKeyPath string) (ssh.Signer, error) {
return private, nil
}

func Run(addr, dataDir, connectBackAddress string, autogeneratedConnectBack bool, TLSCertPath, TLSKeyPath string, insecure, enabledDownloads, enabletTLS, openproxy bool, timeout int) {
func Run(addr, dataDir, connectBackAddress string, autogeneratedConnectBack bool, TLSCertPath, TLSKeyPath string, insecure, enabledDownloads, enabletTLS, openproxy bool, timeout int, trustedProxyCIDRs []*net.IPNet) {
c := mux.MultiplexerConfig{
Control: true,
Downloads: enabledDownloads,
Expand All @@ -56,6 +56,7 @@ func Run(addr, dataDir, connectBackAddress string, autogeneratedConnectBack bool
TLSKeyPath: TLSKeyPath,
AutoTLSCommonName: connectBackAddress,
TcpKeepAlive: timeout,
TrustedProxyCIDRs: trustedProxyCIDRs,
PollingAuthChecker: func(key string, addr net.Addr) bool {

authorizedKey, err := hex.DecodeString(key)
Expand Down
33 changes: 21 additions & 12 deletions internal/server/sshd.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/NHAS/reverse_ssh/internal/server/observers"
"github.com/NHAS/reverse_ssh/internal/server/users"
"github.com/NHAS/reverse_ssh/pkg/logger"
"github.com/NHAS/reverse_ssh/pkg/mux"
"github.com/fatih/color"
"golang.org/x/crypto/ssh"
)
Expand Down Expand Up @@ -389,6 +390,8 @@ func getIP(ip string) net.IP {

func acceptConn(c net.Conn, config *ssh.ServerConfig, timeout int, dataDir string) {

connMetadata := mux.Metadata(c)

//Initially set the timeout high, so people who type in their ssh key password can actually use rssh
realConn := &internal.TimeoutConn{Conn: c, Timeout: time.Duration(timeout) * time.Minute}

Expand Down Expand Up @@ -470,24 +473,30 @@ func acceptConn(c net.Conn, config *ssh.ServerConfig, timeout int, dataDir strin
users.DisassociateClient(id, sshConn)

observers.ConnectionState.Notify(observers.ClientState{
Status: "disconnected",
ID: id,
IP: sshConn.RemoteAddr().String(),
HostName: username,
Version: string(sshConn.ClientVersion()),
Timestamp: time.Now(),
Status: "disconnected",
ID: id,
IP: sshConn.RemoteAddr().String(),
HostName: username,
Version: string(sshConn.ClientVersion()),
Timestamp: time.Now(),
Transport: connMetadata.Transport,
PublicKeyFingerprint: sshConn.Permissions.Extensions["pubkey-fp"],
ProxySourceIP: connMetadata.ProxySourceIP,
})
}()

clientLog.Info("New controllable connection from %s with id %s", color.BlueString(username), color.YellowString(id))

observers.ConnectionState.Notify(observers.ClientState{
Status: "connected",
ID: id,
IP: sshConn.RemoteAddr().String(),
HostName: username,
Version: string(sshConn.ClientVersion()),
Timestamp: time.Now(),
Status: "connected",
ID: id,
IP: sshConn.RemoteAddr().String(),
HostName: username,
Version: string(sshConn.ClientVersion()),
Timestamp: time.Now(),
Transport: connMetadata.Transport,
PublicKeyFingerprint: sshConn.Permissions.Extensions["pubkey-fp"],
ProxySourceIP: connMetadata.ProxySourceIP,
})

case "proxy":
Expand Down
4 changes: 4 additions & 0 deletions pkg/mux/bufferedConn.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ func (bc *bufferedConn) RemoteAddr() net.Addr {
return bc.conn.RemoteAddr()
}

func (bc *bufferedConn) Metadata() ConnectionMetadata {
return Metadata(bc.conn)
}

func (bc *bufferedConn) SetDeadline(t time.Time) error {
return bc.conn.SetDeadline(t)
}
Expand Down
8 changes: 7 additions & 1 deletion pkg/mux/fragmentedConn.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,14 @@ type fragmentedConnection struct {

localAddr net.Addr
remoteAddr net.Addr
metadata ConnectionMetadata

isDead *time.Timer

onClose func()
}

func NewFragmentCollector(localAddr net.Addr, remoteAddr net.Addr, onClosed func()) (*fragmentedConnection, string, error) {
func NewFragmentCollector(localAddr net.Addr, remoteAddr net.Addr, metadata ConnectionMetadata, onClosed func()) (*fragmentedConnection, string, error) {

fc := &fragmentedConnection{
done: make(chan interface{}),
Expand All @@ -36,6 +37,7 @@ func NewFragmentCollector(localAddr net.Addr, remoteAddr net.Addr, onClosed func
writeBuffer: NewSyncBuffer(maxBuffer),
localAddr: localAddr,
remoteAddr: remoteAddr,
metadata: metadata,
onClose: onClosed,
}

Expand Down Expand Up @@ -112,6 +114,10 @@ func (fc *fragmentedConnection) RemoteAddr() net.Addr {
return fc.remoteAddr
}

func (fc *fragmentedConnection) Metadata() ConnectionMetadata {
return fc.metadata
}

func (fc *fragmentedConnection) SetDeadline(t time.Time) error {
return nil
}
Expand Down
Loading