From 88435839f798e0dccbca54ecc2f655214e5a7367 Mon Sep 17 00:00:00 2001 From: Andy_Allan <58987282+andya1lan@users.noreply.github.com> Date: Sun, 4 Jan 2026 01:12:51 +0800 Subject: [PATCH 1/5] feat(ssh): add detailed SSH agent connection logging - Add logSSHKeywords helper to log SSH config with privacy masking - Enhance agent connection/communication logging with blocklogger - Use INFO level for key events (connect success/failure, signer count) - Use DEBUG level for diagnostic info (config details, key fingerprints) - Mask hostname (show first 3 and last 3 chars only) - Show only basename for identity files - Add Windows-specific hint when agent connection fails --- pkg/remote/sshclient.go | 68 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/pkg/remote/sshclient.go b/pkg/remote/sshclient.go index 50a5b9dbfb..abadbcf507 100644 --- a/pkg/remote/sshclient.go +++ b/pkg/remote/sshclient.go @@ -89,6 +89,41 @@ func SimpleMessageFromPossibleConnectionError(err error) string { return err.Error() } +// logSSHKeywords logs SSH configuration in a sanitized way (DEBUG level) +func logSSHKeywords(ctx context.Context, sshKeywords *wconfig.ConnKeywords) { + blocklogger.Debugf(ctx, "[ssh-config] User: %s\n", utilfn.SafeDeref(sshKeywords.SshUser)) + blocklogger.Debugf(ctx, "[ssh-config] HostName: %s\n", maskHostName(utilfn.SafeDeref(sshKeywords.SshHostName))) + blocklogger.Debugf(ctx, "[ssh-config] Port: %s\n", utilfn.SafeDeref(sshKeywords.SshPort)) + blocklogger.Debugf(ctx, "[ssh-config] IdentityAgent: %s\n", utilfn.SafeDeref(sshKeywords.SshIdentityAgent)) + blocklogger.Debugf(ctx, "[ssh-config] IdentitiesOnly: %v\n", utilfn.SafeDeref(sshKeywords.SshIdentitiesOnly)) + blocklogger.Debugf(ctx, "[ssh-config] IdentityFile count: %d\n", len(sshKeywords.SshIdentityFile)) + // Only log file basename, not full path for privacy + for i, f := range sshKeywords.SshIdentityFile { + blocklogger.Debugf(ctx, "[ssh-config] IdentityFile[%d]: %s\n", i, filepath.Base(f)) + } + blocklogger.Debugf(ctx, "[ssh-config] PubkeyAuthentication: %v\n", utilfn.SafeDeref(sshKeywords.SshPubkeyAuthentication)) + blocklogger.Debugf(ctx, "[ssh-config] PasswordAuthentication: %v\n", utilfn.SafeDeref(sshKeywords.SshPasswordAuthentication)) + blocklogger.Debugf(ctx, "[ssh-config] KbdInteractiveAuthentication: %v\n", utilfn.SafeDeref(sshKeywords.SshKbdInteractiveAuthentication)) + blocklogger.Debugf(ctx, "[ssh-config] PreferredAuthentications: %v\n", sshKeywords.SshPreferredAuthentications) + blocklogger.Debugf(ctx, "[ssh-config] AddKeysToAgent: %v\n", utilfn.SafeDeref(sshKeywords.SshAddKeysToAgent)) + blocklogger.Debugf(ctx, "[ssh-config] ProxyJump: %v\n", sshKeywords.SshProxyJump) + // Note: do not log PasswordSecretName value, only indicate if configured + if sshKeywords.SshPasswordSecretName != nil && *sshKeywords.SshPasswordSecretName != "" { + blocklogger.Debugf(ctx, "[ssh-config] PasswordSecretName: \n") + } +} + +// maskHostName masks hostname for privacy, showing only first 3 and last 3 characters +func maskHostName(hostname string) string { + if hostname == "" { + return "" + } + if len(hostname) <= 6 { + return "***" + } + return hostname[:3] + "***" + hostname[len(hostname)-3:] +} + // This exists to trick the ssh library into continuing to try // different public keys even when the current key cannot be // properly parsed @@ -608,6 +643,9 @@ func createClientConfig(connCtx context.Context, sshKeywords *wconfig.ConnKeywor remoteName = chosenUser + "@" + remoteName } + // Log SSH configuration (DEBUG level) + logSSHKeywords(connCtx, sshKeywords) + var authSockSigners []ssh.Signer var agentClient agent.ExtendedAgent @@ -615,12 +653,36 @@ func createClientConfig(connCtx context.Context, sshKeywords *wconfig.ConnKeywor // TODO: Update if we decide to support PKCS11Provider and SecurityKeyProvider agentPath := strings.TrimSpace(utilfn.SafeDeref(sshKeywords.SshIdentityAgent)) if !utilfn.SafeDeref(sshKeywords.SshIdentitiesOnly) && agentPath != "" { + blocklogger.Debugf(connCtx, "[ssh-agent] attempting to connect to agent at %q\n", agentPath) conn, err := dialIdentityAgent(agentPath) if err != nil { - log.Printf("Failed to open Identity Agent Socket %q: %v", agentPath, err) + blocklogger.Infof(connCtx, "[ssh-agent] ERROR failed to connect to agent at %q: %v\n", agentPath, err) + if runtime.GOOS == "windows" { + blocklogger.Infof(connCtx, "[ssh-agent] hint: ensure OpenSSH Authentication Agent service is running (Get-Service ssh-agent)\n") + } } else { + blocklogger.Infof(connCtx, "[ssh-agent] successfully connected to agent at %q\n", agentPath) agentClient = agent.NewClient(conn) - authSockSigners, _ = agentClient.Signers() + blocklogger.Debugf(connCtx, "[ssh-agent] requesting key list from agent...\n") + var signerErr error + authSockSigners, signerErr = agentClient.Signers() + if signerErr != nil { + blocklogger.Infof(connCtx, "[ssh-agent] WARNING failed to get signers from agent: %v\n", signerErr) + } else { + blocklogger.Infof(connCtx, "[ssh-agent] retrieved %d signers from agent\n", len(authSockSigners)) + // Log public key fingerprints (DEBUG level, for troubleshooting) + for i, signer := range authSockSigners { + pubKey := signer.PublicKey() + fingerprint := ssh.FingerprintSHA256(pubKey) + blocklogger.Debugf(connCtx, "[ssh-agent] key[%d]: type=%s fingerprint=%s\n", i, pubKey.Type(), fingerprint) + } + } + } + } else { + if agentPath == "" { + blocklogger.Debugf(connCtx, "[ssh-agent] no agent path configured\n") + } else { + blocklogger.Debugf(connCtx, "[ssh-agent] agent skipped (IdentitiesOnly=%v)\n", utilfn.SafeDeref(sshKeywords.SshIdentitiesOnly)) } } @@ -731,6 +793,7 @@ func ConnectToClient(connCtx context.Context, opts *SSHOpts, currentClient *ssh. var sshConfigKeywords *wconfig.ConnKeywords if utilfn.SafeDeref(internalSshConfigKeywords.ConnIgnoreSshConfig) { + blocklogger.Debugf(connCtx, "[ssh-config] loading config for host %q (ignoresshconfig=true, using defaults only)\n", opts.SSHHost) var err error sshConfigKeywords, err = findSshDefaults(opts.SSHHost) if err != nil { @@ -738,6 +801,7 @@ func ConnectToClient(connCtx context.Context, opts *SSHOpts, currentClient *ssh. return nil, debugInfo.JumpNum, ConnectionError{ConnectionDebugInfo: debugInfo, Err: err} } } else { + blocklogger.Debugf(connCtx, "[ssh-config] loading config for host %q (using ssh_config + internal)\n", opts.SSHHost) var err error sshConfigKeywords, err = findSshConfigKeywords(opts.SSHHost) if err != nil { From 43fb6e8d538656d1d3cec50be59d26dd13c1b085 Mon Sep 17 00:00:00 2001 From: Andy_Allan <58987282+andya1lan@users.noreply.github.com> Date: Sun, 4 Jan 2026 15:54:26 +0800 Subject: [PATCH 2/5] fix(wshrpc): update StreamCancelFn signature to include context parameter --- pkg/wshrpc/wshrpctypes.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/wshrpc/wshrpctypes.go b/pkg/wshrpc/wshrpctypes.go index 14ffb14779..d85ce20c8e 100644 --- a/pkg/wshrpc/wshrpctypes.go +++ b/pkg/wshrpc/wshrpctypes.go @@ -373,7 +373,7 @@ type RpcOpts struct { NoResponse bool `json:"noresponse,omitempty"` Route string `json:"route,omitempty"` - StreamCancelFn func() `json:"-"` // this is an *output* parameter, set by the handler + StreamCancelFn func(ctx context.Context) error `json:"-"` // this is an *output* parameter, set by the handler } const ( From 4e8f8aed65c4520c374d263957b4044b8a8eab74 Mon Sep 17 00:00:00 2001 From: Andy_Allan <58987282+andya1lan@users.noreply.github.com> Date: Sun, 4 Jan 2026 16:03:41 +0800 Subject: [PATCH 3/5] feat(ssh): add privacy masking for sensitive connection log data Add comprehensive privacy masking for SSH connection logs: - Add MaskString() to mask usernames, hostnames, and connection identifiers - Add maskIdentityFile() to mask paths while preserving directory structure - Add logSSHKeywords() to log SSH config with masked sensitive values - Mask remoteName and shellPath in shellcontroller.go - Mask connection name and knownhosts address in conncontroller.go - Mask networkAddr, opts.String(), and fingerprints in sshclient.go - Fix StreamCancelFn type signature to accept context.Context --- pkg/blockcontroller/shellcontroller.go | 2 +- pkg/remote/conncontroller/conncontroller.go | 6 +- pkg/remote/sshclient.go | 111 ++++++++++++++++---- 3 files changed, 92 insertions(+), 27 deletions(-) diff --git a/pkg/blockcontroller/shellcontroller.go b/pkg/blockcontroller/shellcontroller.go index 5411c40404..b2ab9ff9cd 100644 --- a/pkg/blockcontroller/shellcontroller.go +++ b/pkg/blockcontroller/shellcontroller.go @@ -383,7 +383,7 @@ func (bc *ShellController) setupAndStartShellProcess(logCtx context.Context, rc if err != nil { return nil, err } - blocklogger.Infof(logCtx, "[conndebug] remoteName: %q, connType: %s, wshEnabled: %v, shell: %q, shellType: %s\n", remoteName, connUnion.ConnType, connUnion.WshEnabled, connUnion.ShellPath, connUnion.ShellType) + blocklogger.Infof(logCtx, "[conndebug] remoteName: %q, connType: %s, wshEnabled: %v, shell: %q, shellType: %s\n", remote.MaskString(remoteName), connUnion.ConnType, connUnion.WshEnabled, remote.MaskString(connUnion.ShellPath), connUnion.ShellType) var cmdStr string var cmdOpts shellexec.CommandOptsType if bc.ControllerType == BlockController_Shell { diff --git a/pkg/remote/conncontroller/conncontroller.go b/pkg/remote/conncontroller/conncontroller.go index b5d8779a58..aaaa69a477 100644 --- a/pkg/remote/conncontroller/conncontroller.go +++ b/pkg/remote/conncontroller/conncontroller.go @@ -550,7 +550,7 @@ func (conn *SSHConn) Connect(ctx context.Context, connFlags *wconfig.ConnKeyword conn.Infof(ctx, "cannot connect to %q when status is %q\n", conn.GetName(), conn.GetStatus()) return fmt.Errorf("cannot connect to %q when status is %q", conn.GetName(), conn.GetStatus()) } - conn.Infof(ctx, "trying to connect to %q...\n", conn.GetName()) + conn.Infof(ctx, "trying to connect to %q...\n", remote.MaskString(conn.GetName())) conn.FireConnChangeEvent() err := conn.connectInternal(ctx, connFlags) conn.WithLock(func() { @@ -748,7 +748,7 @@ func (conn *SSHConn) persistWshInstalled(ctx context.Context, result WshCheckRes // returns (connect-error) func (conn *SSHConn) connectInternal(ctx context.Context, connFlags *wconfig.ConnKeywords) error { - conn.Infof(ctx, "connectInternal %s\n", conn.GetName()) + conn.Infof(ctx, "connectInternal %s\n", remote.MaskString(conn.GetName())) client, _, err := remote.ConnectToClient(ctx, conn.Opts, nil, 0, connFlags) if err != nil { conn.Infof(ctx, "ERROR ConnectToClient: %s\n", remote.SimpleMessageFromPossibleConnectionError(err)) @@ -765,7 +765,7 @@ func (conn *SSHConn) connectInternal(ctx context.Context, connFlags *wconfig.Con conn.waitForDisconnect() }() fmtAddr := knownhosts.Normalize(fmt.Sprintf("%s@%s", client.User(), client.RemoteAddr().String())) - conn.Infof(ctx, "normalized knownhosts address: %s\n", fmtAddr) + conn.Infof(ctx, "normalized knownhosts address: %s\n", remote.MaskString(fmtAddr)) clientDisplayName := fmt.Sprintf("%s (%s)", conn.GetName(), fmtAddr) wshResult := conn.tryEnableWsh(ctx, clientDisplayName) if !wshResult.WshEnabled { diff --git a/pkg/remote/sshclient.go b/pkg/remote/sshclient.go index abadbcf507..cb0e862628 100644 --- a/pkg/remote/sshclient.go +++ b/pkg/remote/sshclient.go @@ -74,9 +74,9 @@ type ConnectionError struct { func (ce ConnectionError) Error() string { if ce.CurrentClient == nil { - return fmt.Sprintf("Connecting to %s, Error: %v", ce.NextOpts, ce.Err) + return fmt.Sprintf("Connecting to %s, Error: %v", MaskString(ce.NextOpts.String()), ce.Err) } - return fmt.Sprintf("Connecting from %v to %s (jump number %d), Error: %v", ce.CurrentClient, ce.NextOpts, ce.JumpNum, ce.Err) + return fmt.Sprintf("Connecting from client to %s (jump number %d), Error: %v", MaskString(ce.NextOpts.String()), ce.JumpNum, ce.Err) } func SimpleMessageFromPossibleConnectionError(err error) string { @@ -91,37 +91,102 @@ func SimpleMessageFromPossibleConnectionError(err error) string { // logSSHKeywords logs SSH configuration in a sanitized way (DEBUG level) func logSSHKeywords(ctx context.Context, sshKeywords *wconfig.ConnKeywords) { - blocklogger.Debugf(ctx, "[ssh-config] User: %s\n", utilfn.SafeDeref(sshKeywords.SshUser)) - blocklogger.Debugf(ctx, "[ssh-config] HostName: %s\n", maskHostName(utilfn.SafeDeref(sshKeywords.SshHostName))) + blocklogger.Debugf(ctx, "[ssh-config] User: %s\n", MaskString(utilfn.SafeDeref(sshKeywords.SshUser))) + blocklogger.Debugf(ctx, "[ssh-config] HostName: %s\n", MaskString(utilfn.SafeDeref(sshKeywords.SshHostName))) blocklogger.Debugf(ctx, "[ssh-config] Port: %s\n", utilfn.SafeDeref(sshKeywords.SshPort)) - blocklogger.Debugf(ctx, "[ssh-config] IdentityAgent: %s\n", utilfn.SafeDeref(sshKeywords.SshIdentityAgent)) + blocklogger.Debugf(ctx, "[ssh-config] IdentityAgent: %s\n", filepath.Base(utilfn.SafeDeref(sshKeywords.SshIdentityAgent))) blocklogger.Debugf(ctx, "[ssh-config] IdentitiesOnly: %v\n", utilfn.SafeDeref(sshKeywords.SshIdentitiesOnly)) blocklogger.Debugf(ctx, "[ssh-config] IdentityFile count: %d\n", len(sshKeywords.SshIdentityFile)) - // Only log file basename, not full path for privacy + // Log masked identity file paths for privacy for i, f := range sshKeywords.SshIdentityFile { - blocklogger.Debugf(ctx, "[ssh-config] IdentityFile[%d]: %s\n", i, filepath.Base(f)) + blocklogger.Debugf(ctx, "[ssh-config] IdentityFile[%d]: %s\n", i, maskIdentityFile(f)) } blocklogger.Debugf(ctx, "[ssh-config] PubkeyAuthentication: %v\n", utilfn.SafeDeref(sshKeywords.SshPubkeyAuthentication)) blocklogger.Debugf(ctx, "[ssh-config] PasswordAuthentication: %v\n", utilfn.SafeDeref(sshKeywords.SshPasswordAuthentication)) blocklogger.Debugf(ctx, "[ssh-config] KbdInteractiveAuthentication: %v\n", utilfn.SafeDeref(sshKeywords.SshKbdInteractiveAuthentication)) blocklogger.Debugf(ctx, "[ssh-config] PreferredAuthentications: %v\n", sshKeywords.SshPreferredAuthentications) blocklogger.Debugf(ctx, "[ssh-config] AddKeysToAgent: %v\n", utilfn.SafeDeref(sshKeywords.SshAddKeysToAgent)) - blocklogger.Debugf(ctx, "[ssh-config] ProxyJump: %v\n", sshKeywords.SshProxyJump) + blocklogger.Debugf(ctx, "[ssh-config] ProxyJump count: %d\n", len(sshKeywords.SshProxyJump)) // Note: do not log PasswordSecretName value, only indicate if configured if sshKeywords.SshPasswordSecretName != nil && *sshKeywords.SshPasswordSecretName != "" { blocklogger.Debugf(ctx, "[ssh-config] PasswordSecretName: \n") } } -// maskHostName masks hostname for privacy, showing only first 3 and last 3 characters -func maskHostName(hostname string) string { - if hostname == "" { +// MaskString masks a string for privacy, showing only first 3 and last 3 characters. +// Uses rune-based slicing to properly handle multi-byte UTF-8 characters. +func MaskString(s string) string { + if s == "" { return "" } - if len(hostname) <= 6 { + runes := []rune(s) + if len(runes) <= 6 { return "***" } - return hostname[:3] + "***" + hostname[len(hostname)-3:] + return string(runes[:3]) + "***" + string(runes[len(runes)-3:]) +} + +// maskIdentityFile masks an identity file path for privacy. +// It masks the username in home directory paths (/home/user/ or C:\Users\user\) +// and masks the filename while preserving .pub suffix if present. +func maskIdentityFile(path string) string { + if path == "" { + return "" + } + + // Normalize path separators for consistent handling + normalizedPath := filepath.ToSlash(path) + + // Extract directory and filename + dir := filepath.Dir(path) + filename := filepath.Base(path) + + // Check for .pub suffix + hasPubSuffix := strings.HasSuffix(filename, ".pub") + if hasPubSuffix { + filename = strings.TrimSuffix(filename, ".pub") + } + + // Mask the filename + maskedFilename := MaskString(filename) + if hasPubSuffix { + maskedFilename += ".pub" + } + + // Mask username in home directory paths + // Unix: /home/username/... or /Users/username/... + // Windows: C:\Users\username\... (normalized to C:/Users/username/...) + maskedDir := dir + if strings.HasPrefix(normalizedPath, "/home/") { + parts := strings.SplitN(normalizedPath, "/", 4) // ["", "home", "username", "rest..."] + if len(parts) >= 3 { + maskedUsername := MaskString(parts[2]) + if len(parts) >= 4 { + maskedDir = "/home/" + maskedUsername + "/" + filepath.Dir(parts[3]) + } else { + maskedDir = "/home/" + maskedUsername + } + } + } else if strings.Contains(normalizedPath, "/Users/") { + // Handle both Unix /Users/ and Windows C:/Users/ + idx := strings.Index(normalizedPath, "/Users/") + prefix := normalizedPath[:idx] + rest := normalizedPath[idx+7:] // Skip "/Users/" + parts := strings.SplitN(rest, "/", 2) + if len(parts) >= 1 { + maskedUsername := MaskString(parts[0]) + if len(parts) >= 2 { + maskedDir = prefix + "/Users/" + maskedUsername + "/" + filepath.Dir(parts[1]) + } else { + maskedDir = prefix + "/Users/" + maskedUsername + } + } + } + + // Convert back to native path separators + maskedDir = filepath.FromSlash(maskedDir) + + return filepath.Join(maskedDir, maskedFilename) } // This exists to trick the ssh library into continuing to try @@ -653,15 +718,15 @@ func createClientConfig(connCtx context.Context, sshKeywords *wconfig.ConnKeywor // TODO: Update if we decide to support PKCS11Provider and SecurityKeyProvider agentPath := strings.TrimSpace(utilfn.SafeDeref(sshKeywords.SshIdentityAgent)) if !utilfn.SafeDeref(sshKeywords.SshIdentitiesOnly) && agentPath != "" { - blocklogger.Debugf(connCtx, "[ssh-agent] attempting to connect to agent at %q\n", agentPath) + blocklogger.Debugf(connCtx, "[ssh-agent] attempting to connect to agent at %q\n", filepath.Base(agentPath)) conn, err := dialIdentityAgent(agentPath) if err != nil { - blocklogger.Infof(connCtx, "[ssh-agent] ERROR failed to connect to agent at %q: %v\n", agentPath, err) + blocklogger.Infof(connCtx, "[ssh-agent] ERROR failed to connect to agent at %q: %v\n", filepath.Base(agentPath), err) if runtime.GOOS == "windows" { blocklogger.Infof(connCtx, "[ssh-agent] hint: ensure OpenSSH Authentication Agent service is running (Get-Service ssh-agent)\n") } } else { - blocklogger.Infof(connCtx, "[ssh-agent] successfully connected to agent at %q\n", agentPath) + blocklogger.Infof(connCtx, "[ssh-agent] successfully connected to agent at %q\n", filepath.Base(agentPath)) agentClient = agent.NewClient(conn) blocklogger.Debugf(connCtx, "[ssh-agent] requesting key list from agent...\n") var signerErr error @@ -674,7 +739,7 @@ func createClientConfig(connCtx context.Context, sshKeywords *wconfig.ConnKeywor for i, signer := range authSockSigners { pubKey := signer.PublicKey() fingerprint := ssh.FingerprintSHA256(pubKey) - blocklogger.Debugf(connCtx, "[ssh-agent] key[%d]: type=%s fingerprint=%s\n", i, pubKey.Type(), fingerprint) + blocklogger.Debugf(connCtx, "[ssh-agent] key[%d]: type=%s fingerprint=%s\n", i, pubKey.Type(), MaskString(fingerprint)) } } } @@ -750,14 +815,14 @@ func connectInternal(ctx context.Context, networkAddr string, clientConfig *ssh. var err error if currentClient == nil { d := net.Dialer{Timeout: clientConfig.Timeout} - blocklogger.Infof(ctx, "[conndebug] ssh dial %s\n", networkAddr) + blocklogger.Infof(ctx, "[conndebug] ssh dial %s\n", MaskString(networkAddr)) clientConn, err = d.DialContext(ctx, "tcp", networkAddr) if err != nil { blocklogger.Infof(ctx, "[conndebug] ERROR dial error: %v\n", err) return nil, err } } else { - blocklogger.Infof(ctx, "[conndebug] ssh dial (from client) %s\n", networkAddr) + blocklogger.Infof(ctx, "[conndebug] ssh dial (from client) %s\n", MaskString(networkAddr)) clientConn, err = currentClient.DialContext(ctx, "tcp", networkAddr) if err != nil { blocklogger.Infof(ctx, "[conndebug] ERROR dial error: %v\n", err) @@ -769,12 +834,12 @@ func connectInternal(ctx context.Context, networkAddr string, clientConfig *ssh. blocklogger.Infof(ctx, "[conndebug] ERROR ssh auth/negotiation: %s\n", SimpleMessageFromPossibleConnectionError(err)) return nil, err } - blocklogger.Infof(ctx, "[conndebug] successful ssh connection to %s\n", networkAddr) + blocklogger.Infof(ctx, "[conndebug] successful ssh connection to %s\n", MaskString(networkAddr)) return ssh.NewClient(c, chans, reqs), nil } func ConnectToClient(connCtx context.Context, opts *SSHOpts, currentClient *ssh.Client, jumpNum int32, connFlags *wconfig.ConnKeywords) (*ssh.Client, int32, error) { - blocklogger.Infof(connCtx, "[conndebug] ConnectToClient %s (jump:%d)...\n", opts.String(), jumpNum) + blocklogger.Infof(connCtx, "[conndebug] ConnectToClient %s (jump:%d)...\n", MaskString(opts.String()), jumpNum) debugInfo := &ConnectionDebugInfo{ CurrentClient: currentClient, NextOpts: opts, @@ -793,7 +858,7 @@ func ConnectToClient(connCtx context.Context, opts *SSHOpts, currentClient *ssh. var sshConfigKeywords *wconfig.ConnKeywords if utilfn.SafeDeref(internalSshConfigKeywords.ConnIgnoreSshConfig) { - blocklogger.Debugf(connCtx, "[ssh-config] loading config for host %q (ignoresshconfig=true, using defaults only)\n", opts.SSHHost) + blocklogger.Debugf(connCtx, "[ssh-config] loading config for host %q (ignoresshconfig=true, using defaults only)\n", MaskString(opts.SSHHost)) var err error sshConfigKeywords, err = findSshDefaults(opts.SSHHost) if err != nil { @@ -801,7 +866,7 @@ func ConnectToClient(connCtx context.Context, opts *SSHOpts, currentClient *ssh. return nil, debugInfo.JumpNum, ConnectionError{ConnectionDebugInfo: debugInfo, Err: err} } } else { - blocklogger.Debugf(connCtx, "[ssh-config] loading config for host %q (using ssh_config + internal)\n", opts.SSHHost) + blocklogger.Debugf(connCtx, "[ssh-config] loading config for host %q (using ssh_config + internal)\n", MaskString(opts.SSHHost)) var err error sshConfigKeywords, err = findSshConfigKeywords(opts.SSHHost) if err != nil { From 8ca09fd82cb66e69a480c050028ef70cb724c14a Mon Sep 17 00:00:00 2001 From: Andy_Allan <58987282+andya1lan@users.noreply.github.com> Date: Sun, 4 Jan 2026 16:32:01 +0800 Subject: [PATCH 4/5] Revert "fix(wshrpc): update StreamCancelFn signature to include context parameter" This reverts commit 43fb6e8d538656d1d3cec50be59d26dd13c1b085. --- pkg/wshrpc/wshrpctypes.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/wshrpc/wshrpctypes.go b/pkg/wshrpc/wshrpctypes.go index d85ce20c8e..14ffb14779 100644 --- a/pkg/wshrpc/wshrpctypes.go +++ b/pkg/wshrpc/wshrpctypes.go @@ -373,7 +373,7 @@ type RpcOpts struct { NoResponse bool `json:"noresponse,omitempty"` Route string `json:"route,omitempty"` - StreamCancelFn func(ctx context.Context) error `json:"-"` // this is an *output* parameter, set by the handler + StreamCancelFn func() `json:"-"` // this is an *output* parameter, set by the handler } const ( From 380b99ca1a05664872c319ec3f92a00e76d733a8 Mon Sep 17 00:00:00 2001 From: Andy_Allan <58987282+andya1lan@users.noreply.github.com> Date: Sun, 4 Jan 2026 22:30:58 +0800 Subject: [PATCH 5/5] fix: handle filepath.Dir returning "." in maskIdentityFile for files directly under user home --- pkg/remote/sshclient.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/pkg/remote/sshclient.go b/pkg/remote/sshclient.go index cb0e862628..074680882a 100644 --- a/pkg/remote/sshclient.go +++ b/pkg/remote/sshclient.go @@ -162,7 +162,12 @@ func maskIdentityFile(path string) string { if len(parts) >= 3 { maskedUsername := MaskString(parts[2]) if len(parts) >= 4 { - maskedDir = "/home/" + maskedUsername + "/" + filepath.Dir(parts[3]) + subDir := filepath.Dir(parts[3]) + if subDir == "." { + maskedDir = "/home/" + maskedUsername + } else { + maskedDir = "/home/" + maskedUsername + "/" + subDir + } } else { maskedDir = "/home/" + maskedUsername } @@ -176,7 +181,12 @@ func maskIdentityFile(path string) string { if len(parts) >= 1 { maskedUsername := MaskString(parts[0]) if len(parts) >= 2 { - maskedDir = prefix + "/Users/" + maskedUsername + "/" + filepath.Dir(parts[1]) + subDir := filepath.Dir(parts[1]) + if subDir == "." { + maskedDir = prefix + "/Users/" + maskedUsername + } else { + maskedDir = prefix + "/Users/" + maskedUsername + "/" + subDir + } } else { maskedDir = prefix + "/Users/" + maskedUsername }