Skip to content

Commit

Permalink
Handle Cygwin / Git Bash sockets forwarding on Windows
Browse files Browse the repository at this point in the history
This is used to forward ssh-agent connection to Git Bash' ssh-agent.

Here is the explanation of what is required to connect to a cygwin / git
bash unix domain socket on Windows:

- Port parsing:

  - Git Bash' unix sockets requires connecting to the port whose number
    is in the socket file along with a cookie.

  - The socket file contains something like `!<socket >63488 s 44693F4F-E2572CA5-537862AB-248DFDEF`

  - The port here is `63488` and the cookie is `44693F4F-E2572CA5-537862AB-248DFDEF`.

  - So I retrieve the port and cookie using a regex and convert it to a
    number.

    - If the file content does not match the regex, I assume this is a
      GPG socket and use the existing code to parse it.

  - When I have the port and the cookie, I connect to `127.0.0.1:<port>`,
    then I do the following handshake.

- Cygwin / Git Bash socket Handshake:

  -  The handshake consists in:

    -  the client must send the cookie as 16 raw bytes

      - The cookie is formatted in the socket file as 4 32 bits hex
        integers. They must be send to the ssh-agent server in little
        endian as 16 raw bytes (this means according to the above
        example: `0x4F 0x3F 0x69 0x44  0xA5 0x72 ...`).

    -  the server send back the same 16 bytes if the cookie is valid,
       else closes the connection (so the client must skip these 16
       bytes, as done in `skipHeader`)

    - the client must send pid and user id and user effective id
      information in a 12 bytes packet

      - I set the pid to a real value from process.pid, but ssh-agent
        ignores it

      - user id and user effective id are both set to 0

    - the server send back the same information, but about the server, I
      just ignore these 12 bytes too in `skipHeader`; this is a function
      that just skip the handshake data).

As the server send back data in the handshake phase (16 + 12 bytes), I
need to skip them through the use of `skipHeader`.

Then actual data transfer can take place.

See also:
https://stackoverflow.com/questions/23086038/what-mechanism-is-used-by-msys-cygwin-to-emulate-unix-domain-sockets
https://github.com/abourget/secrets-bridge/blob/094959a1553943e0727f6524289e12e8aab697bf/pkg/agentfwd/agentconn_windows.go#L15

Fix: #62
  • Loading branch information
amurzeau committed Jul 23, 2022
1 parent 3c67d2c commit 9245a1c
Showing 1 changed file with 140 additions and 10 deletions.
150 changes: 140 additions & 10 deletions src/spec-common/cliHost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import * as os from 'os';
import { readLocalFile, writeLocalFile, mkdirpLocal, isLocalFile, renameLocal, readLocalDir, isLocalFolder } from '../spec-utils/pfs';
import { URI } from 'vscode-uri';
import { ExecFunction, getLocalUsername, plainExec, plainPtyExec, PtyExecFunction } from './commonUtils';
import { Duplex } from 'pull-stream';
import { Abort, Duplex, Sink, Source, SourceCallback } from 'pull-stream';

const toPull = require('stream-to-pull-stream');

Expand Down Expand Up @@ -87,29 +87,159 @@ function createLocalCLIHostFromExecFunctions(localCwd: string, exec: ExecFunctio
};
}

function connectLocal(socketPath: string) {
if (process.platform !== 'win32' || socketPath.startsWith('\\\\.\\pipe\\')) {
return toPull.duplex(net.connect(socketPath));
// Parse a Cygwin socket cookie string to a raw Buffer
function cygwinUnixSocketCookieToBuffer(cookie: string) {
let bytes: number[] = [];

cookie.split('-').map((number: string) => {
const bytesInChar = number.match(/.{2}/g);
if (bytesInChar !== null) {
bytesInChar.reverse().map((byte) => {
bytes.push(parseInt(byte, 16));
});
}
});
return Buffer.from(bytes);
}

// The cygwin/git bash ssh-agent server will reply us with the cookie back (16 bytes)
// + identifiers (12 bytes), skip them while forwarding data from ssh-agent to the client
function skipHeader(headerSize: number, cb: SourceCallback<Buffer>, abort: Abort, data?: Buffer): number {
if (abort || data === undefined) {
cb(abort);
return headerSize;
}

const socket = new net.Socket();
if (headerSize === 0) {
// Fast path avoiding data buffer manipulation
cb(null, data);
} else if (data.length > headerSize) {
// We need to remove part of the data to forward
let removeSize = data.length - headerSize;
data.copy(data, 0, removeSize);
headerSize = 0;
cb(null, data);
} else {
// We need to remove all forwarded data
headerSize = headerSize - data.length;
cb(null, Buffer.of());
}

// Return the updated headerSize
return headerSize;
}

// Function to handle the Cygwin/Gpg4win socket filtering
// These sockets need an handshake before forwarding client and server data
function handleUnixSocketOnWindows(socket: net.Socket, socketPath: string): Duplex<Buffer, Buffer> {
let headerSize = 0;
let pendingSourceCallbacks: { abort: Abort; cb: SourceCallback<Buffer> }[] = [];
let pendingSinkCalls: Source<Buffer>[] = [];
let connectionDuplex: Duplex<Buffer, Buffer> | undefined = undefined;

(async () => {
const buf = await readLocalFile(socketPath);
const i = buf.indexOf(0xa);
const port = parseInt(buf.slice(0, i).toString(), 10);
const guid = buf.slice(i + 1);
const str = buf.toString();

// Try to parse cygwin socket data
const cygwinSocketParameters = str.match(/!<socket >(\d+)( s)? ((([A-Fa-f0-9]{2}){4}-?){4})/);

let port: number;
let handshake: Buffer;

if (cygwinSocketParameters !== null) {
// Cygwin / MSYS / Git Bash unix socket on Windows
const portStr = cygwinSocketParameters[1];
const guidStr = cygwinSocketParameters[3];
port = parseInt(portStr, 10);
const guid = cygwinUnixSocketCookieToBuffer(guidStr);

let identifierData = Buffer.alloc(12);
identifierData.writeUInt32LE(process.pid, 0);

handshake = Buffer.concat([guid, identifierData]);

// Recv header size = GUID (16 bytes) + identifiers (3 * 4 bytes)
headerSize = 16 + 3 * 4;
} else {
// Gpg4Win unix socket
const i = buf.indexOf(0xa);
port = parseInt(buf.slice(0, i).toString(), 10);
handshake = buf.slice(i + 1);

// No header will be received from Gpg4Win agent
headerSize = 0;
}

socket.connect(port, '127.0.0.1', () => {
socket.write(guid, err => {
connectionDuplex = toPull.duplex(socket);

// Call pending source calls, if the pull-stream connection was pull-ed
// before we got connected to the ssh-agent/gpg-agent server
for (let callback of pendingSourceCallbacks) {
(connectionDuplex as Duplex<Buffer, Buffer>).source(callback.abort, function (abort, data) {
headerSize = skipHeader(headerSize, callback.cb, abort, data);
});
}

// Write handshake data to the ssh-agent/gpg-agent server
socket.write(handshake, err => {
if (err) {
console.error(err);
socket.destroy();
}

// Call pending sink calls after the handshake is completed
// to send what the client sent to us
for (let callback of pendingSinkCalls) {
(connectionDuplex as Duplex<Buffer, Buffer>).sink(callback);
}
});
});
})()
.catch(err => {
console.error(err);
socket.destroy();
});
return toPull.duplex(socket);

// pull-stream source that remove the first <headerSize> bytes
let source: Source<Buffer> = function (abort: Abort, cb: SourceCallback<Buffer>) {
if (connectionDuplex !== undefined) {
connectionDuplex.source(abort, function (abort, data) {
headerSize = skipHeader(headerSize, cb, abort, data);
});
} else {
pendingSourceCallbacks.push({ abort: abort, cb: cb });
}
};

// pull-stream sink. No filtering done, but we need to store calls in case
// the connection to the upstram ssh-agent/gpg-agent is not yet connected
let sink: Sink<Buffer> = function (source: Source<Buffer>) {
if (connectionDuplex !== undefined) {
connectionDuplex.sink(source);
} else {
pendingSinkCalls.push(source);
}
};

return {
source: source,
sink: sink
};
}

// Connect to a ssh-agent or gpg-agent, supporting multiple platforms
function connectLocal(socketPath: string) {
if (process.platform !== 'win32' || socketPath.startsWith('\\\\.\\pipe\\')) {
// Simple case: direct forwarding
return toPull.duplex(net.connect(socketPath));
}

// More complex case: we need to do an handshake to support Cygwin / Git Bash
// sockets or Gpg4Win sockets

const socket = new net.Socket();

return handleUnixSocketOnWindows(socket, socketPath);
}

0 comments on commit 9245a1c

Please sign in to comment.