ssh: raw handshake support, settings: API settings update & loading fixes
This commit is contained in:
parent
fa9b7b1838
commit
0f27400a48
1
.gitignore
vendored
1
.gitignore
vendored
@ -21,6 +21,7 @@ build/
|
|||||||
*.pb.go
|
*.pb.go
|
||||||
|
|
||||||
# Go workspace file
|
# Go workspace file
|
||||||
|
cryptolib
|
||||||
go.work
|
go.work
|
||||||
.idea
|
.idea
|
||||||
config.yml
|
config.yml
|
||||||
|
19
Dockerfile
Normal file
19
Dockerfile
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
FROM golang:1.21-alpine AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY ./ ./
|
||||||
|
RUN set -eux; \
|
||||||
|
apk add --no-cache bash make git protoc protobuf-dev dumb-init mailcap ca-certificates tzdata; \
|
||||||
|
update-ca-certificates; \
|
||||||
|
make install_protobuf build
|
||||||
|
|
||||||
|
FROM scratch
|
||||||
|
WORKDIR /
|
||||||
|
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||||
|
COPY --from=builder /etc/mime.types /etc/mime.types
|
||||||
|
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
|
||||||
|
COPY --from=builder /usr/bin/dumb-init /bin/dumb-init
|
||||||
|
COPY --from=builder /app/build/sshpoke /sshpoke
|
||||||
|
EXPOSE 25680
|
||||||
|
EXPOSE 25681
|
||||||
|
ENTRYPOINT ["/bin/dumb-init", "--"]
|
||||||
|
CMD ["/sshpoke"]
|
26
Makefile
26
Makefile
@ -8,7 +8,7 @@ GO_VERSION=$(shell go version | sed -e 's/go version //')
|
|||||||
VERSION=$(shell git describe --tags 2>/dev/null || git log --format="v0.0.0-%h" -n 1 || echo "v0.0.0-unknown")
|
VERSION=$(shell git describe --tags 2>/dev/null || git log --format="v0.0.0-%h" -n 1 || echo "v0.0.0-unknown")
|
||||||
LDFLAGS="-X 'github.com/Neur0toxine/sshpoke/cmd.Version=${VERSION}'"
|
LDFLAGS="-X 'github.com/Neur0toxine/sshpoke/cmd.Version=${VERSION}'"
|
||||||
|
|
||||||
.PHONY: run clean_backend
|
.PHONY: run clean_backend clone_sshlib
|
||||||
|
|
||||||
build: generate deps fmt
|
build: generate deps fmt
|
||||||
@echo " ► Building with ${GO_VERSION}"
|
@echo " ► Building with ${GO_VERSION}"
|
||||||
@ -42,17 +42,23 @@ install_protobuf:
|
|||||||
@go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28
|
@go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28
|
||||||
@go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2
|
@go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2
|
||||||
|
|
||||||
update_sshlib:
|
clone_cryptolib:
|
||||||
@rm -rf cryptolib && \
|
@rm -rf cryptolib && \
|
||||||
git clone https://go.googlesource.com/crypto cryptolib && \
|
git clone https://go.googlesource.com/crypto cryptolib && \
|
||||||
rm -rf pkg/proto/ssh && \
|
mv cryptolib/internal/poly1305 cryptolib/ssh/internal/ && \
|
||||||
|
find cryptolib/ssh -type f -name '*.go' -exec sed -i 's?golang.org/x/crypto/ssh?github.com/Neur0toxine/sshpoke/pkg/proto/ssh?g' {} \; && \
|
||||||
|
find cryptolib/ssh -type f -name '*.go' -exec sed -i 's?golang.org/x/crypto/internal/poly1305?github.com/Neur0toxine/sshpoke/pkg/proto/ssh/internal/poly1305?g' {} \; && \
|
||||||
|
find cryptolib/ssh -type f -name '*_test.go' -delete && \
|
||||||
|
rm -rf cryptolib/ssh/test && \
|
||||||
|
rm -rf cryptolib/ssh/testdata
|
||||||
|
|
||||||
|
update_sshlib_patch:
|
||||||
|
@diff -Naru cryptolib/ssh pkg/proto/ssh > patch/ssh_proto_patches.patch
|
||||||
|
|
||||||
|
update_sshlib: clone_cryptolib
|
||||||
|
@mv pkg/proto/ssh pkg/proto/ssh.bak && \
|
||||||
mv cryptolib/ssh pkg/proto/ && \
|
mv cryptolib/ssh pkg/proto/ && \
|
||||||
mv cryptolib/internal/poly1305 pkg/proto/ssh/internal/ && \
|
patch -p0 < patch/ssh_proto_patches.patch && \
|
||||||
find pkg/proto/ssh -type f -name '*.go' -exec sed -i 's?golang.org/x/crypto/ssh?github.com/Neur0toxine/sshpoke/pkg/proto/ssh?g' {} \; && \
|
rm -rf pkg/proto/ssh.bak && \
|
||||||
find pkg/proto/ssh -type f -name '*.go' -exec sed -i 's?golang.org/x/crypto/internal/poly1305?github.com/Neur0toxine/sshpoke/pkg/proto/ssh/internal/poly1305?g' {} \; && \
|
|
||||||
find pkg/proto/ssh -type f -name '*_test.go' -delete && \
|
|
||||||
patch -p0 < patch/ssh_fakehost.patch && \
|
|
||||||
rm -rf pkg/proto/ssh/test && \
|
|
||||||
rm -rf pkg/proto/ssh/testdata && \
|
|
||||||
rm -rf cryptolib
|
rm -rf cryptolib
|
||||||
|
|
||||||
|
@ -1,145 +0,0 @@
|
|||||||
# Enable or disable debug logging.
|
|
||||||
debug: true
|
|
||||||
# API settings.
|
|
||||||
api:
|
|
||||||
# Local port for Web API. Will be bound to localhost.
|
|
||||||
web_port: 25680
|
|
||||||
# Local port for plugin API. Will listen on all interfaces because it has auth.
|
|
||||||
plugin_port: 25681
|
|
||||||
# Docker client preferences.
|
|
||||||
docker:
|
|
||||||
# Extract client params from the environment.
|
|
||||||
from_env: true
|
|
||||||
# Cert path for the Docker client.
|
|
||||||
cert_path: ~
|
|
||||||
# Set it to false to disable TLS cert verification.
|
|
||||||
tls_verify: true
|
|
||||||
# Docker host. Can be useful for running containers alongside remote plugin (although it sounds weird to do so).
|
|
||||||
host: ~
|
|
||||||
# Docker version.
|
|
||||||
version: ~
|
|
||||||
# Default server to use if `sshpoke.server` is not specified in the target container labels.
|
|
||||||
default_server: mine
|
|
||||||
# Servers configuration.
|
|
||||||
servers:
|
|
||||||
# Server name.
|
|
||||||
- name: mine
|
|
||||||
# Server driver. Each driver has its own set of params. Supported drivers: ssh, plugin, null.
|
|
||||||
driver: ssh
|
|
||||||
params:
|
|
||||||
# SSH server address
|
|
||||||
address: "your1.server:2222"
|
|
||||||
# Remote port to be used for forwarding.
|
|
||||||
forward_port: 80
|
|
||||||
# This disables remote host resolution and forcibly uses server IP for remote host.
|
|
||||||
# It's the same as this syntax for sish: `ssh -R addr:80:localhost:80 your.sish.server`
|
|
||||||
# Set this to true if you're using sish, otherwise you'll get weird domains with IP's in them.
|
|
||||||
fake_remote_host: true
|
|
||||||
# Disables PTY request for this server.
|
|
||||||
nopty: true
|
|
||||||
# Requests interactive shell for SSH sessions. Should be `true` for the `commands`.
|
|
||||||
# You can also pass a string with shell binary, for example, "/bin/sh".
|
|
||||||
# Note: commands will be executed using provided shell binary.
|
|
||||||
shell: false
|
|
||||||
# Spoof client version with provided (value below is taken directly from OpenSSH). This value must be compliant with RFC-4253.
|
|
||||||
client_version: "SSH-2.0-OpenSSH_9.5"
|
|
||||||
# Authentication data.
|
|
||||||
auth:
|
|
||||||
# Authentication type. Supported types: key, password, passwordless
|
|
||||||
type: key
|
|
||||||
# Remote user
|
|
||||||
user: user
|
|
||||||
# Directory with SSH keys. ssh-config from this directory will be used if `keyfile` is not provided.
|
|
||||||
# Supported ssh-config directives: HostName, Port, User, IdentityFile
|
|
||||||
# known_hosts from this directory will be used if `host_keys` is not provided.
|
|
||||||
directory: "~/.ssh"
|
|
||||||
# Expose mode (multiple domains or single domain). Allowed values: single, multi.
|
|
||||||
mode: multi
|
|
||||||
# Keep-alive settings. Remove to disable keep-alive completely.
|
|
||||||
keepalive:
|
|
||||||
# Interval for keep-alive requests in seconds.
|
|
||||||
interval: 1
|
|
||||||
# How many attempts should fail to forcibly restart the connection.
|
|
||||||
max_attempts: 2
|
|
||||||
# Regular expression that will be used to extract domain from stdout & stderr. Useful for services like sish or
|
|
||||||
# localhost.run. `commands` output will also be parsed by this regex.
|
|
||||||
# With `!name` syntax you can use some built-in expressions:
|
|
||||||
# - !webUrl - any HTTP or HTTPS URL.
|
|
||||||
# - !httpUrl - any HTTP URL.
|
|
||||||
# - !httpsUrl - any HTTPS URL.
|
|
||||||
domain_extract_regex: "!httpsUrl"
|
|
||||||
# Host keys to prevent MITM. You can obtain those via `ssh-keyscan <address>` (specify `-p` for non-standard port).
|
|
||||||
# Always use '|' YAML syntax here (not '>') or sshpoke won't be able to parse keys.
|
|
||||||
host_keys: |
|
|
||||||
# ssh.neur0tx.site:2222 SSH-2.0-sish
|
|
||||||
# ssh.neur0tx.site:2222 SSH-2.0-sish
|
|
||||||
# ssh.neur0tx.site:2222 SSH-2.0-sish
|
|
||||||
[ssh.neur0tx.site]:2222 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEvxbqK0u8UjqEtrO/83GPS7MeoFp6C3+7KjOHd8+1GF
|
|
||||||
# ssh.neur0tx.site:2222 SSH-2.0-sish
|
|
||||||
# ssh.neur0tx.site:2222 SSH-2.0-sish
|
|
||||||
- name: ssh-demo-single-domain
|
|
||||||
driver: ssh
|
|
||||||
params:
|
|
||||||
auth:
|
|
||||||
type: key
|
|
||||||
user: user
|
|
||||||
directory: "~/.ssh"
|
|
||||||
keyfile: id_ed25519
|
|
||||||
address: "your2.server"
|
|
||||||
forward_port: 80
|
|
||||||
fake_remote_host: true
|
|
||||||
nopty: false
|
|
||||||
shell: "/usr/bin/bash"
|
|
||||||
mode: single
|
|
||||||
keepalive:
|
|
||||||
interval: 1
|
|
||||||
max_attempts: 2
|
|
||||||
domain_extract_regex: "!webUrl"
|
|
||||||
- name: ssh-demo-commands
|
|
||||||
driver: ssh
|
|
||||||
params:
|
|
||||||
address: "your3.server"
|
|
||||||
forward_port: 8080
|
|
||||||
auth:
|
|
||||||
type: key
|
|
||||||
user: user
|
|
||||||
directory: "~/.ssh"
|
|
||||||
mode: multi
|
|
||||||
keepalive:
|
|
||||||
interval: 1
|
|
||||||
max_attempts: 2
|
|
||||||
domain_extract_regex: "!webUrl"
|
|
||||||
# Commands that will be executed on the host.
|
|
||||||
commands:
|
|
||||||
# These commands will be executed after connect.
|
|
||||||
on_connect:
|
|
||||||
- echo https://`date +%s`.proxy.test
|
|
||||||
# These commands will be executed before disconnect.
|
|
||||||
on_disconnect:
|
|
||||||
- echo disconnect from `cat /etc/hostname`
|
|
||||||
- name: ssh-demo-with-password
|
|
||||||
driver: ssh
|
|
||||||
params:
|
|
||||||
address: "ssh.neur0tx.site"
|
|
||||||
forward_port: 8081
|
|
||||||
auth:
|
|
||||||
type: password
|
|
||||||
user: user
|
|
||||||
# Remote user password.
|
|
||||||
password: password
|
|
||||||
mode: multi
|
|
||||||
keepalive:
|
|
||||||
interval: 1
|
|
||||||
max_attempts: 2
|
|
||||||
domain_extract_regex: "!httpUrl"
|
|
||||||
commands:
|
|
||||||
on_connect:
|
|
||||||
- echo http://`date +%s`.proxy.test
|
|
||||||
- name: plugin-demo
|
|
||||||
driver: plugin
|
|
||||||
params:
|
|
||||||
# This token will be used by plugin while connecting to gRPC API.
|
|
||||||
token: key
|
|
||||||
- name: noop
|
|
||||||
# Null driver doesn't do anything. This driver will automatically be used for servers with invalid 'driver' value.
|
|
||||||
driver: null
|
|
10
examples/docker-compose.yml
Normal file
10
examples/docker-compose.yml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
version: "3.8"
|
||||||
|
services:
|
||||||
|
sshpoke:
|
||||||
|
image: "neur0toxine/sshpoke:latest"
|
||||||
|
container_name: "sshpoke"
|
||||||
|
network_mode: host
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
|
- ./config.yml:/config.yml
|
@ -61,7 +61,7 @@ func (p *pluginAPI) receiverForContext(ctx context.Context) plugin.Plugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func StartPluginAPI() {
|
func StartPluginAPI() {
|
||||||
port := config.Default.API.PluginPort
|
port := config.Default.API.Plugin.Port
|
||||||
if port == 0 {
|
if port == 0 {
|
||||||
port = plugin2.DefaultPort
|
port = plugin2.DefaultPort
|
||||||
}
|
}
|
||||||
|
@ -19,8 +19,17 @@ type Config struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type API struct {
|
type API struct {
|
||||||
WebPort int `mapstructure:"web_port" validate:"gte=0,lte=65535"`
|
Web WebAPI `mapstructure:"web"`
|
||||||
PluginPort int `mapstructure:"plugin_port" validate:"gte=0,lte=65535"`
|
Plugin PluginAPI `mapstructure:"plugin"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebAPI struct {
|
||||||
|
Port int `mapstructure:"port" validate:"gte=0,lte=65535"`
|
||||||
|
Token string `mapstructure:"token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PluginAPI struct {
|
||||||
|
Port int `mapstructure:"port" validate:"gte=0,lte=65535"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DockerConfig struct {
|
type DockerConfig struct {
|
||||||
|
@ -20,6 +20,7 @@ import (
|
|||||||
"github.com/Neur0toxine/sshpoke/pkg/dto"
|
"github.com/Neur0toxine/sshpoke/pkg/dto"
|
||||||
"github.com/Neur0toxine/sshpoke/pkg/proto/ssh"
|
"github.com/Neur0toxine/sshpoke/pkg/proto/ssh"
|
||||||
"github.com/Neur0toxine/sshpoke/pkg/proto/ssh/knownhosts"
|
"github.com/Neur0toxine/sshpoke/pkg/proto/ssh/knownhosts"
|
||||||
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
const KnownHostsFile = "known_hosts"
|
const KnownHostsFile = "known_hosts"
|
||||||
@ -85,17 +86,42 @@ func (d *SSH) forward(val sshtun.Forward, domainMatcher func(string)) conn {
|
|||||||
d.Log())
|
d.Log())
|
||||||
ctx, cancel := context.WithCancel(d.Context())
|
ctx, cancel := context.WithCancel(d.Context())
|
||||||
tunDbgLog := d.Log().With("ssh-output", val.Remote.String())
|
tunDbgLog := d.Log().With("ssh-output", val.Remote.String())
|
||||||
go tun.Connect(ctx,
|
var outputReaderCb sshtun.SessionCallback
|
||||||
sshtun.BannerDebugLogCallback(tunDbgLog),
|
if d.params.ReadSessionsOutput == nil || (*d.params.ReadSessionsOutput) {
|
||||||
sshtun.OutputReaderCallback(func(msg string) {
|
outputReaderCb = sshtun.OutputReaderCallback(func(msg string) {
|
||||||
d.Log().Debug(msg)
|
msg = strings.TrimSpace(msg)
|
||||||
|
if msg == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tunDbgLog.Debug("session: ", msg)
|
||||||
if domainMatcher != nil {
|
if domainMatcher != nil {
|
||||||
domainMatcher(msg)
|
domainMatcher(msg)
|
||||||
}
|
}
|
||||||
}))
|
})
|
||||||
|
}
|
||||||
|
go tun.Connect(ctx,
|
||||||
|
d.buildHandshakeLineCallback(domainMatcher, tunDbgLog),
|
||||||
|
sshtun.BannerDebugLogCallback(tunDbgLog),
|
||||||
|
outputReaderCb)
|
||||||
return conn{ctx: ctx, cancel: cancel, tun: tun}
|
return conn{ctx: ctx, cancel: cancel, tun: tun}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *SSH) buildHandshakeLineCallback(domainMatcher func(string), tunDbgLog *zap.SugaredLogger) func(string) {
|
||||||
|
if d.params.ReadRawPackets {
|
||||||
|
return func(msg string) {
|
||||||
|
msg = strings.TrimSpace(msg)
|
||||||
|
if msg == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tunDbgLog.Debugf("ssh: %s", msg)
|
||||||
|
if domainMatcher != nil {
|
||||||
|
domainMatcher(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (d *SSH) buildHostKeyCallback() ssh.HostKeyCallback {
|
func (d *SSH) buildHostKeyCallback() ssh.HostKeyCallback {
|
||||||
keysCallback := func() ssh.HostKeyCallback {
|
keysCallback := func() ssh.HostKeyCallback {
|
||||||
if d.hostKeys == nil || len(d.hostKeys) == 0 {
|
if d.hostKeys == nil || len(d.hostKeys) == 0 {
|
||||||
@ -280,6 +306,10 @@ func (d *SSH) remoteEndpoint(remoteHost string) sshtun.Endpoint {
|
|||||||
if port == 0 {
|
if port == 0 {
|
||||||
port = 80
|
port = 80
|
||||||
}
|
}
|
||||||
|
if remoteHost == "" && !d.params.FakeRemoteHost {
|
||||||
|
// Listen on all interfaces if no host was provided.
|
||||||
|
remoteHost = "0.0.0.0"
|
||||||
|
}
|
||||||
return sshtun.Endpoint{
|
return sshtun.Endpoint{
|
||||||
Host: remoteHost,
|
Host: remoteHost,
|
||||||
Port: port,
|
Port: port,
|
||||||
|
@ -15,6 +15,8 @@ type Params struct {
|
|||||||
Auth types.Auth `mapstructure:"auth"`
|
Auth types.Auth `mapstructure:"auth"`
|
||||||
KeepAlive types.KeepAlive `mapstructure:"keepalive"`
|
KeepAlive types.KeepAlive `mapstructure:"keepalive"`
|
||||||
DomainExtractRegex string `mapstructure:"domain_extract_regex" validate:"validregexp"`
|
DomainExtractRegex string `mapstructure:"domain_extract_regex" validate:"validregexp"`
|
||||||
|
ReadRawPackets bool `mapstructure:"read_raw_packets"`
|
||||||
|
ReadSessionsOutput *bool `mapstructure:"read_sessions_output"`
|
||||||
Mode types.DomainMode `mapstructure:"mode" validate:"required,oneof=single multi"`
|
Mode types.DomainMode `mapstructure:"mode" validate:"required,oneof=single multi"`
|
||||||
FakeRemoteHost bool `mapstructure:"fake_remote_host"`
|
FakeRemoteHost bool `mapstructure:"fake_remote_host"`
|
||||||
NoPTY bool `mapstructure:"nopty"`
|
NoPTY bool `mapstructure:"nopty"`
|
||||||
|
@ -39,11 +39,11 @@ type TunnelConfig struct {
|
|||||||
type BoolOrStr string
|
type BoolOrStr string
|
||||||
|
|
||||||
func (b BoolOrStr) IsBool() bool {
|
func (b BoolOrStr) IsBool() bool {
|
||||||
return b == "true" || b == "false"
|
return b == "true" || b == "false" || b == "1" || b == "0" || b == ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b BoolOrStr) Falsy() bool {
|
func (b BoolOrStr) Falsy() bool {
|
||||||
return b == "" || b == "false"
|
return b == "" || b == "0" || b == "false"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b BoolOrStr) String() string {
|
func (b BoolOrStr) String() string {
|
||||||
@ -60,7 +60,8 @@ func New(address, user string, auth []ssh.AuthMethod, sc TunnelConfig, log *zap.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Tunnel) Connect(ctx context.Context, bannerCb ssh.BannerCallback, sessionCb SessionCallback) {
|
func (t *Tunnel) Connect(ctx context.Context,
|
||||||
|
hsLineCb func(msg string), bannerCb ssh.BannerCallback, sessionCb SessionCallback) {
|
||||||
if t.connected.Load() {
|
if t.connected.Load() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -69,7 +70,7 @@ func (t *Tunnel) Connect(ctx context.Context, bannerCb ssh.BannerCallback, sessi
|
|||||||
backoffTime := backoff.ExponentialWithCappedMax(100*time.Millisecond, 5*time.Second)
|
backoffTime := backoff.ExponentialWithCappedMax(100*time.Millisecond, 5*time.Second)
|
||||||
for {
|
for {
|
||||||
t.connected.Store(true)
|
t.connected.Store(true)
|
||||||
err := t.connect(ctx, bannerCb, sessionCb)
|
err := t.connect(ctx, hsLineCb, bannerCb, sessionCb)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.log.Error("connect error: ", err)
|
t.log.Error("connect error: ", err)
|
||||||
}
|
}
|
||||||
@ -86,9 +87,13 @@ func (t *Tunnel) Connect(ctx context.Context, bannerCb ssh.BannerCallback, sessi
|
|||||||
|
|
||||||
// connect once to the SSH server. if the connection breaks, we return error and the caller
|
// connect once to the SSH server. if the connection breaks, we return error and the caller
|
||||||
// will try to re-connect
|
// will try to re-connect
|
||||||
func (t *Tunnel) connect(ctx context.Context, bannerCb ssh.BannerCallback, sessionCb SessionCallback) error {
|
func (t *Tunnel) connect(ctx context.Context,
|
||||||
|
hsLineCb func(string), bannerCb ssh.BannerCallback, sessionCb SessionCallback) error {
|
||||||
t.log.Debug("connecting")
|
t.log.Debug("connecting")
|
||||||
sshConfig := &ssh.ClientConfig{
|
sshConfig := &ssh.ClientConfig{
|
||||||
|
Config: ssh.Config{
|
||||||
|
HandshakePacketReader: newHandshakePerLineReader(hsLineCb),
|
||||||
|
},
|
||||||
User: t.user,
|
User: t.user,
|
||||||
Auth: t.authMethods,
|
Auth: t.authMethods,
|
||||||
HostKeyCallback: t.tunConfig.HostKeyCallback,
|
HostKeyCallback: t.tunConfig.HostKeyCallback,
|
||||||
|
@ -15,7 +15,7 @@ import (
|
|||||||
// assume that the underlying net.Conn abruptly died.
|
// assume that the underlying net.Conn abruptly died.
|
||||||
func (t *Tunnel) keepAlive(ctx context.Context, client *ssh.Client, wg *sync.WaitGroup) {
|
func (t *Tunnel) keepAlive(ctx context.Context, client *ssh.Client, wg *sync.WaitGroup) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
if t.tunConfig.KeepAliveInterval == 0 || t.tunConfig.KeepAliveMax == 0 {
|
if t.tunConfig.KeepAliveInterval <= 0 || t.tunConfig.KeepAliveMax <= 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
31
internal/server/driver/ssh/sshtun/packet.go
Normal file
31
internal/server/driver/ssh/sshtun/packet.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package sshtun
|
||||||
|
|
||||||
|
type handshakePerLineReader struct {
|
||||||
|
buf []byte
|
||||||
|
off int
|
||||||
|
cb func(line string)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHandshakePerLineReader(cb func(string)) func([]byte) {
|
||||||
|
if cb == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
lr := &handshakePerLineReader{cb: cb, buf: make([]byte, 1024)}
|
||||||
|
return lr.packet
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *handshakePerLineReader) packet(p []byte) {
|
||||||
|
for _, char := range p {
|
||||||
|
if (char == 10 || char == 13) && len(h.buf) > 0 {
|
||||||
|
h.cb(string(h.buf[:h.off]))
|
||||||
|
h.off = 0
|
||||||
|
}
|
||||||
|
if cap(h.buf) <= (h.off + 1) {
|
||||||
|
newBuf := make([]byte, cap(h.buf)+256)
|
||||||
|
copy(newBuf, h.buf)
|
||||||
|
h.buf = newBuf
|
||||||
|
}
|
||||||
|
h.buf[h.off] = char
|
||||||
|
h.off++
|
||||||
|
}
|
||||||
|
}
|
@ -37,7 +37,7 @@ func OutputReaderCallback(callback func(string)) SessionCallback {
|
|||||||
|
|
||||||
func BannerDebugLogCallback(log *zap.SugaredLogger) ssh.BannerCallback {
|
func BannerDebugLogCallback(log *zap.SugaredLogger) ssh.BannerCallback {
|
||||||
return func(msg string) error {
|
return func(msg string) error {
|
||||||
log.Debug(msg)
|
log.Debug("banner: ", msg)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,5 +2,5 @@ package types
|
|||||||
|
|
||||||
type KeepAlive struct {
|
type KeepAlive struct {
|
||||||
Interval int `mapstructure:"interval" validate:"gte=0"`
|
Interval int `mapstructure:"interval" validate:"gte=0"`
|
||||||
MaxAttempts int `mapstructure:"max_attempts" validate:"gte=1"`
|
MaxAttempts int `mapstructure:"max_attempts" validate:"gte=0"`
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,68 @@
|
|||||||
--- cryptolib/ssh/tcpip.go 2023-11-18 22:20:50.609146922 +0300
|
diff -Naru cryptolib/ssh/cipher.go pkg/proto/ssh/cipher.go
|
||||||
+++ pkg/proto/ssh/tcpip.go 2023-11-18 22:19:08.891684669 +0300
|
--- cryptolib/ssh/cipher.go 2023-11-21 18:52:03.117248053 +0300
|
||||||
|
+++ pkg/proto/ssh/cipher.go 2023-11-21 17:19:04.688042738 +0300
|
||||||
|
@@ -16,8 +16,8 @@
|
||||||
|
"hash"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
- "golang.org/x/crypto/chacha20"
|
||||||
|
"github.com/Neur0toxine/sshpoke/pkg/proto/ssh/internal/poly1305"
|
||||||
|
+ "golang.org/x/crypto/chacha20"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
diff -Naru cryptolib/ssh/common.go pkg/proto/ssh/common.go
|
||||||
|
--- cryptolib/ssh/common.go 2023-11-21 18:52:03.217249582 +0300
|
||||||
|
+++ pkg/proto/ssh/common.go 2023-11-21 17:28:23.221203344 +0300
|
||||||
|
@@ -7,14 +7,13 @@
|
||||||
|
import (
|
||||||
|
"crypto"
|
||||||
|
"crypto/rand"
|
||||||
|
+ _ "crypto/sha1"
|
||||||
|
+ _ "crypto/sha256"
|
||||||
|
+ _ "crypto/sha512"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math"
|
||||||
|
"sync"
|
||||||
|
-
|
||||||
|
- _ "crypto/sha1"
|
||||||
|
- _ "crypto/sha256"
|
||||||
|
- _ "crypto/sha512"
|
||||||
|
)
|
||||||
|
|
||||||
|
// These are string constants in the SSH protocol.
|
||||||
|
@@ -279,6 +278,9 @@
|
||||||
|
// The allowed MAC algorithms. If unspecified then a sensible default is
|
||||||
|
// used. Unsupported values are silently ignored.
|
||||||
|
MACs []string
|
||||||
|
+
|
||||||
|
+ // Called on every incoming handshake packet for client. Only receives data and extended data packets.
|
||||||
|
+ HandshakePacketReader func(p []byte)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDefaults sets sensible values for unset fields in config. This is
|
||||||
|
diff -Naru cryptolib/ssh/handshake.go pkg/proto/ssh/handshake.go
|
||||||
|
--- cryptolib/ssh/handshake.go 2023-11-21 18:52:03.209249460 +0300
|
||||||
|
+++ pkg/proto/ssh/handshake.go 2023-11-21 18:51:58.681180283 +0300
|
||||||
|
@@ -401,6 +401,14 @@
|
||||||
|
t.printPacket(p, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
+ if t.config.HandshakePacketReader != nil && p[0] == msgChannelData {
|
||||||
|
+ data, err := decode(p)
|
||||||
|
+ packetData, ok := data.(*channelDataMsg)
|
||||||
|
+ if err == nil && ok && packetData != nil {
|
||||||
|
+ t.config.HandshakePacketReader(packetData.Rest)
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
if first && p[0] != msgKexInit {
|
||||||
|
return nil, fmt.Errorf("ssh: first packet should be msgKexInit")
|
||||||
|
}
|
||||||
|
diff -Naru cryptolib/ssh/tcpip.go pkg/proto/ssh/tcpip.go
|
||||||
|
--- cryptolib/ssh/tcpip.go 2023-11-21 18:52:03.129248237 +0300
|
||||||
|
+++ pkg/proto/ssh/tcpip.go 2023-11-21 17:19:04.688042738 +0300
|
||||||
@@ -101,14 +101,18 @@
|
@@ -101,14 +101,18 @@
|
||||||
// ListenTCP requests the remote peer open a listening socket
|
// ListenTCP requests the remote peer open a listening socket
|
||||||
// on laddr. Incoming connections will be available by calling
|
// on laddr. Incoming connections will be available by calling
|
@ -7,14 +7,13 @@ package ssh
|
|||||||
import (
|
import (
|
||||||
"crypto"
|
"crypto"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
_ "crypto/sha1"
|
||||||
|
_ "crypto/sha256"
|
||||||
|
_ "crypto/sha512"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"math"
|
"math"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
_ "crypto/sha1"
|
|
||||||
_ "crypto/sha256"
|
|
||||||
_ "crypto/sha512"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// These are string constants in the SSH protocol.
|
// These are string constants in the SSH protocol.
|
||||||
@ -279,6 +278,9 @@ type Config struct {
|
|||||||
// The allowed MAC algorithms. If unspecified then a sensible default is
|
// The allowed MAC algorithms. If unspecified then a sensible default is
|
||||||
// used. Unsupported values are silently ignored.
|
// used. Unsupported values are silently ignored.
|
||||||
MACs []string
|
MACs []string
|
||||||
|
|
||||||
|
// Called on every incoming handshake packet for client. Only receives data and extended data packets.
|
||||||
|
HandshakePacketReader func(p []byte)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetDefaults sets sensible values for unset fields in config. This is
|
// SetDefaults sets sensible values for unset fields in config. This is
|
||||||
|
@ -401,6 +401,14 @@ func (t *handshakeTransport) readOnePacket(first bool) ([]byte, error) {
|
|||||||
t.printPacket(p, false)
|
t.printPacket(p, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if t.config.HandshakePacketReader != nil && p[0] == msgChannelData {
|
||||||
|
data, err := decode(p)
|
||||||
|
packetData, ok := data.(*channelDataMsg)
|
||||||
|
if err == nil && ok && packetData != nil {
|
||||||
|
t.config.HandshakePacketReader(packetData.Rest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if first && p[0] != msgKexInit {
|
if first && p[0] != msgKexInit {
|
||||||
return nil, fmt.Errorf("ssh: first packet should be msgKexInit")
|
return nil, fmt.Errorf("ssh: first packet should be msgKexInit")
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user