2026-04-26 10:45:39 +08:00
//go:build windows
package starssh
import (
2026-05-27 13:10:35 +08:00
"bytes"
2026-04-26 10:45:39 +08:00
"context"
2026-05-27 13:10:35 +08:00
"encoding/binary"
2026-04-26 10:45:39 +08:00
"errors"
2026-05-27 13:10:35 +08:00
"fmt"
"io"
2026-04-26 10:45:39 +08:00
"net"
"os"
2026-05-27 13:10:35 +08:00
"path/filepath"
"strconv"
2026-04-26 10:45:39 +08:00
"strings"
"time"
"github.com/Microsoft/go-winio"
"golang.org/x/sys/windows"
)
const defaultWindowsSSHAgentPipe = ` \\.\pipe\openssh-ssh-agent `
2026-05-27 13:10:35 +08:00
var errInvalidGPGSocketInfo = errors . New ( "invalid gpg agent socket file" )
type gpgSocketInfo struct {
port uint16
nonce [ ] byte
cygwin bool
}
func defaultSSHAgentEndpoint ( ) ( resolvedSSHAgentEndpoint , error ) {
return resolvedSSHAgentEndpoint {
Endpoint : defaultWindowsSSHAgentPipe ,
Source : "platform-default" ,
Network : "windows-pipe" ,
} , nil
}
func defaultSSHAgentNetwork ( endpoint string ) string {
if _ , ok := normalizeWindowsSSHAgentPipe ( endpoint ) ; ok {
return "windows-pipe"
2026-04-26 10:45:39 +08:00
}
2026-05-27 13:10:35 +08:00
if isAgentSSHSocketPath ( endpoint ) {
return "gpg-socket"
}
return "unix"
2026-04-26 10:45:39 +08:00
}
2026-05-27 13:10:35 +08:00
func dialResolvedSSHAgent ( resolved resolvedSSHAgentEndpoint , timeout time . Duration ) ( net . Conn , error ) {
if pipePath , ok := normalizeWindowsSSHAgentPipe ( resolved . Endpoint ) ; ok {
return dialWindowsNamedPipe ( pipePath , timeout , resolved . Source == "platform-default" )
2026-04-26 10:45:39 +08:00
}
2026-05-27 13:10:35 +08:00
if isAgentSSHSocketPath ( resolved . Endpoint ) {
return dialWindowsGPGSocketFile ( resolved . Endpoint , timeout )
2026-04-26 10:45:39 +08:00
}
2026-05-27 13:10:35 +08:00
return dialWindowsUnixAgent ( resolved . Endpoint , timeout )
2026-04-26 10:45:39 +08:00
}
func dialWindowsNamedPipe ( path string , timeout time . Duration , unavailableOnNotFound bool ) ( net . Conn , error ) {
ctx := context . Background ( )
cancel := func ( ) { }
if timeout > 0 {
ctx , cancel = context . WithTimeout ( ctx , timeout )
}
defer cancel ( )
2026-05-27 13:10:35 +08:00
return dialWindowsNamedPipeContext ( ctx , path , unavailableOnNotFound )
2026-04-26 10:45:39 +08:00
}
func normalizeWindowsSSHAgentPipe ( endpoint string ) ( string , bool ) {
trimmed := strings . TrimSpace ( endpoint )
if trimmed == "" {
return "" , false
}
normalized := trimmed
if strings . HasPrefix ( normalized , "//./pipe/" ) {
normalized = ` \\.\pipe\ ` + strings . TrimPrefix ( normalized , "//./pipe/" )
}
if strings . HasPrefix ( normalized , ` \\.\pipe\ ` ) {
return normalized , true
}
return "" , false
}
func isWindowsPipeUnavailable ( err error ) bool {
return errors . Is ( err , windows . ERROR_FILE_NOT_FOUND ) || errors . Is ( err , windows . ERROR_PATH_NOT_FOUND )
}
2026-05-27 13:10:35 +08:00
func dialWindowsUnixAgent ( endpoint string , timeout time . Duration ) ( net . Conn , error ) {
if timeout > 0 {
return net . DialTimeout ( "unix" , endpoint , timeout )
}
return net . Dial ( "unix" , endpoint )
}
func dialWindowsGPGSocketFile ( path string , timeout time . Duration ) ( net . Conn , error ) {
ctx := context . Background ( )
cancel := func ( ) { }
if timeout > 0 {
ctx , cancel = context . WithTimeout ( ctx , timeout )
}
defer cancel ( )
return dialWindowsGPGSocketFileDepth ( ctx , strings . TrimSpace ( path ) , 0 )
}
func dialWindowsGPGSocketFileDepth ( ctx context . Context , path string , depth int ) ( net . Conn , error ) {
if path == "" {
return nil , fmt . Errorf ( "gpg agent endpoint is empty" )
}
if depth > 8 {
return nil , fmt . Errorf ( "gpg agent socket redirect loop at %s" , path )
}
data , err := os . ReadFile ( path )
if err != nil {
return nil , err
}
if target , ok := parseGPGAssuanSocketRedirect ( data ) ; ok {
target = resolveGPGSocketRedirectTarget ( path , target )
if pipePath , ok := normalizeWindowsSSHAgentPipe ( target ) ; ok {
return dialWindowsNamedPipeContext ( ctx , pipePath , false )
}
return dialWindowsGPGSocketFileDepth ( ctx , target , depth + 1 )
}
info , err := parseGPGSocketInfo ( path , data )
if err != nil {
return nil , err
}
return dialWindowsGPGSocketInfo ( ctx , info )
}
func dialWindowsGPGSocketInfo ( ctx context . Context , info gpgSocketInfo ) ( net . Conn , error ) {
var dialer net . Dialer
conn , err := dialer . DialContext ( ctx , "tcp" , net . JoinHostPort ( "127.0.0.1" , strconv . Itoa ( int ( info . port ) ) ) )
if err != nil {
return nil , err
}
if deadline , ok := ctx . Deadline ( ) ; ok {
if err := conn . SetDeadline ( deadline ) ; err != nil {
_ = conn . Close ( )
return nil , err
}
}
if _ , err := conn . Write ( info . nonce ) ; err != nil {
_ = conn . Close ( )
return nil , err
}
if info . cygwin {
var nonce [ 16 ] byte
if _ , err := io . ReadFull ( conn , nonce [ : ] ) ; err != nil {
_ = conn . Close ( )
return nil , err
}
var credential [ 8 ] byte
binary . LittleEndian . PutUint32 ( credential [ : 4 ] , uint32 ( os . Getpid ( ) ) )
if _ , err := conn . Write ( credential [ : ] ) ; err != nil {
_ = conn . Close ( )
return nil , err
}
if _ , err := io . ReadFull ( conn , credential [ : ] ) ; err != nil {
_ = conn . Close ( )
return nil , err
}
}
_ = conn . SetDeadline ( time . Time { } )
return conn , nil
}
func resolveGPGSocketRedirectTarget ( source string , target string ) string {
target = strings . TrimSpace ( target )
if target == "" || filepath . IsAbs ( target ) {
return target
}
if _ , ok := normalizeWindowsSSHAgentPipe ( target ) ; ok {
return target
}
return filepath . Join ( filepath . Dir ( source ) , target )
}
func parseGPGSocketInfo ( path string , data [ ] byte ) ( gpgSocketInfo , error ) {
if info , ok := parseGPGAssuanSocketInfo ( data ) ; ok {
return info , nil
}
if info , ok := parseGPGCygwinSocketInfo ( data ) ; ok {
return info , nil
}
return gpgSocketInfo { } , fmt . Errorf ( "%w %s: expected GnuPG port/nonce socket file; if SSH_AUTH_SOCK was set to this file, restart gpg-agent to recreate it" , errInvalidGPGSocketInfo , path )
}
func parseGPGAssuanSocketRedirect ( data [ ] byte ) ( string , bool ) {
text := strings . ReplaceAll ( string ( data ) , "\r\n" , "\n" )
text = strings . TrimSuffix ( text , "\n" )
lines := strings . Split ( text , "\n" )
if len ( lines ) != 2 || lines [ 0 ] != "%Assuan%" {
return "" , false
}
target , ok := strings . CutPrefix ( lines [ 1 ] , "socket=" )
if ! ok || strings . TrimSpace ( target ) == "" {
return "" , false
}
return os . ExpandEnv ( target ) , true
}
func parseGPGAssuanSocketInfo ( data [ ] byte ) ( gpgSocketInfo , bool ) {
newline := bytes . IndexByte ( data , '\n' )
if newline <= 0 || len ( data ) - newline - 1 != 16 {
return gpgSocketInfo { } , false
}
port64 , err := strconv . ParseUint ( strings . TrimSpace ( string ( data [ : newline ] ) ) , 10 , 16 )
if err != nil || port64 == 0 {
return gpgSocketInfo { } , false
}
nonce := make ( [ ] byte , 16 )
copy ( nonce , data [ newline + 1 : ] )
return gpgSocketInfo { port : uint16 ( port64 ) , nonce : nonce } , true
}
func parseGPGCygwinSocketInfo ( data [ ] byte ) ( gpgSocketInfo , bool ) {
if ! bytes . HasPrefix ( data , [ ] byte ( "!<socket >" ) ) {
return gpgSocketInfo { } , false
}
fields := strings . Fields ( strings . TrimRight ( string ( data [ 10 : ] ) , "\x00" ) )
if len ( fields ) != 3 || fields [ 1 ] != "s" {
return gpgSocketInfo { } , false
}
port64 , err := strconv . ParseUint ( fields [ 0 ] , 10 , 16 )
if err != nil || port64 == 0 {
return gpgSocketInfo { } , false
}
hexParts := strings . Split ( fields [ 2 ] , "-" )
if len ( hexParts ) != 4 {
return gpgSocketInfo { } , false
}
nonce := make ( [ ] byte , 0 , 16 )
for _ , part := range hexParts {
if len ( part ) != 8 {
return gpgSocketInfo { } , false
}
value , err := strconv . ParseUint ( part , 16 , 32 )
if err != nil {
return gpgSocketInfo { } , false
}
var chunk [ 4 ] byte
binary . LittleEndian . PutUint32 ( chunk [ : ] , uint32 ( value ) )
nonce = append ( nonce , chunk [ : ] ... )
}
return gpgSocketInfo { port : uint16 ( port64 ) , nonce : nonce , cygwin : true } , true
}
func isAgentSSHSocketPath ( endpoint string ) bool {
normalized := strings . ToLower ( strings . TrimSpace ( endpoint ) )
return strings . HasSuffix ( normalized , "s.gpg-agent.ssh" )
}
func dialWindowsNamedPipeContext ( ctx context . Context , path string , unavailableOnNotFound bool ) ( net . Conn , error ) {
if ctx == nil {
ctx = context . Background ( )
}
conn , err := winio . DialPipeContext ( ctx , path )
if err != nil && unavailableOnNotFound && isWindowsPipeUnavailable ( err ) {
return nil , errSSHAgentUnavailable
}
if err != nil {
return nil , err
}
return conn , nil
}