microsoft / vscode-remote-release

Visual Studio Code Remote Development: Open any folder in WSL, in a Docker container, or on a remote machine using SSH and take advantage of VS Code's full feature set.
https://aka.ms/vscode-remote
Other
3.69k stars 296 forks source link

remote - containers with Docker Desktop not forwarding ssh-agent from Git for Windows #6719

Open amurzeau opened 2 years ago

amurzeau commented 2 years ago

Start script in git bash: ssh-agent sh -c 'ssh-add ~/.ssh/id_rsa; ./code.exe'

Log of ssh-add -l in powershell (local command to check that SSH_AUTH_SOCK and ssh are working, key content replaced with 0s):

PS C:\Users\user> ssh-add -l
3072 SHA256:trq++00000000000000000000000000000000000000 user@host (RSA)

Starting a devcontainer using Docker Desktop give this message:

[17741 ms] Start: Launching Remote-Containers helper.
[17741 ms] ssh-agent: SSH_AUTH_SOCK in container (/tmp/vscode-ssh-auth-b40f9ddddb69d8c4a063d651337d9b53229c20df.sock) forwarded to local host (C:/Users/user/AppData/Local/Temp/ssh-EXqkeSdl9JlB/agent.2428).

C:/Users/user/AppData/Local/Temp/ssh-EXqkeSdl9JlB/agent.2428 match the SSH_AUTH_SOCK set by ssh-agent.

When trying to ssh-add -l in a terminal inside the devcontainer, I get this error:

root@86b574330cde:/workspaces/repository# ssh-add -l    
error fetching identities: communication with agent failed

And at the same time, the devcontainer logs shows this:

[50200 ms] Container server: Remote to local stream terminated with error: {
  message: 'Socket is closed',
  name: 'Error',
  stack: 'Error [ERR_SOCKET_CLOSED]: Socket is closed\n' +
    '\tat Socket._writeGeneric (net.js:775:8)\n' +
    '\tat Socket._write (net.js:797:8)\n' +
    '\tat writeOrBuffer (internal/streams/writable.js:358:12)\n' +
    '\tat Socket.Writable.write (internal/streams/writable.js:303:10)\n' +
    '\tat c:\\VSCode\\data\\extensions\\ms-vscode-remote.remote-containers-0.209.5\\dist\\extension\\extension.js:16:4913\n' +
    '\tat source (c:\\VSCode\\data\\extensions\\ms-vscode-remote.remote-containers-0.209.5\\dist\\extension\\extension.js:11:22821)\n' +
    '\tat c:\\VSCode\\data\\extensions\\ms-vscode-remote.remote-containers-0.209.5\\dist\\extension\\extension.js:16:4763\n' +
    '\tat e (c:\\VSCode\\data\\extensions\\ms-vscode-remote.remote-containers-0.209.5\\dist\\extension\\extension.js:11:13646)\n' +
    '\tat SI.exports (c:\\VSCode\\data\\extensions\\ms-vscode-remote.remote-containers-0.209.5\\dist\\extension\\extension.js:11:13695)\n' +
    '\tat c:\\VSCode\\data\\extensions\\ms-vscode-remote.remote-containers-0.209.5\\dist\\extension\\extension.js:16:4748\n' +
    '\tat processTicksAndRejections (internal/process/task_queues.js:75:11)'
}

Steps to Reproduce:

  1. Have a SSH key as ~/.ssh/id_rsa in Git bash for Windows
  2. Start Code.exe from a git bash console using ssh-agent sh -c 'ssh-add ~/.ssh/id_rsa; ./code.exe'
  3. Check that the ssh-agent is working correctly using the Powershell terminal in VSCode and running ssh-add -l, it shows the added key
  4. Open a devcontainer with a Docker Desktop container
  5. Open the terminal and run ssh-add -l

I expect Git for Windows' ssh-agent to be forwarded inside the devcontainer as it is with Powershell OpenSSH Win32 ssh-agent. I can't use the later because the ssh-agent Windows' service is disabled by the enterprise and I don't have admin rights to change that.

Does this issue occur when you try this locally?: No Does this issue occur when you try this locally and all extensions are disabled?: No

amurzeau commented 2 years ago

Workaround: Use something like https://github.com/tprasadtp/pipe-ssh-pageant or https://github.com/amurzeau/ssh-agent-bridge to redirect requests on \.\pipe\openssh-ssh-agent made by commands in the container through VSCode's own ssh agent bridge to either pageant or git bash' agent.


But I also successfully made VSCode forward ssh-agent requests to git bash' ssh-agent with this change. The following is a diff between:

The extension.js file is here: %USERPROFILE%\.vscode\extensions\ms-vscode-remote.remote-containers-0.234.0\dist\extension\extension.js

The modification consists in changing existing code that trigger when

I don't know what was the purpose of the original code, but it seemed close enough to the implementation of Git Bash' ssh-agent given what it does and when it is triggered.

I've changed the way the handshake is made based on what Git Bash' ssh-agent expect:

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

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

--- orig.js 2022-05-24 23:26:59.410007100 +0200
+++ fixed.js    2022-05-24 23:28:30.064495500 +0200
@@ -41675,25 +41675,64 @@
         connect: n
     }
 }
+function unixSocketCookieToBuffer(guid) {
+    var bytes = [];
+    guid.split('-').map((number, index) => {
+        var bytesInChar = number.match(/.{1,2}/g).reverse();
+        bytesInChar.map((byte) => {
+            bytes.push(parseInt(byte, 16));
+        });
+    });
+    return Buffer.from(bytes);
+}
+function skipUnixSocketHeader() {
+    var headerSize = 16 + 12;
+    var Through = fT();
+    return Through(function (buf) {
+        if (buf.length > headerSize) {
+            var removeSize = buf.length - headerSize;
+            buf.copy(buf, 0, removeSize);
+            headerSize = 0;
+            this.queue(buf);
+        } else {
+            headerSize = headerSize - buf.length;
+        }
+    })
+}
 function Z9(t) {
     if (process.platform !== "win32" || t.startsWith("\\\\.\\pipe\\"))
         return kD.duplex(Kp.connect(t));
     let e = new Kp.Socket;
-    return (async() => {
-        let r = await Oe(t),
-        n = r.indexOf(10),
-        i = parseInt(r.slice(0, n).toString(), 10),
-        o = r.slice(n + 1);
-        e.connect(i, "127.0.0.1", () => {
-            e.write(o, s => {
-                s && (console.error(s), e.destroy())
+    (async() => {
+        let unixDomainSocketFileData = await Oe(t),
+        str = unixDomainSocketFileData.toString(),
+        params = str.match(/!<socket >(\d+)( s)? ([A-Fa-f0-9-]+)/),
+        portStr = params[1],
+        unixDomainSocketCookie = unixSocketCookieToBuffer(params[3]),
+        port = parseInt(portStr, 10);
+        e.connect(port, "127.0.0.1", () => {
+            e.write(unixDomainSocketCookie, s => {
+                if (s) {
+                    console.error(s);
+                    e.destroy();
+                } else {
+                    var buf = Buffer.alloc(12);
+                    buf.writeUInt32LE(process.pid, 0);
+                    e.write(buf, s => {
+                        s && (console.error(s), e.destroy())
+                    });
+                }
             })
         })
     })().catch(r => {
         console.error(r),
         e.destroy()
-    }),
-    kD.duplex(e)
+    });
+    var connection = kD.duplex(e);
+    return {
+        source: skipUnixSocketHeader()(connection.source),
+        sink: connection.sink
+    }
 }
 function AD(t, e, r) {
     return t === "linux" ? e === r : e.toLowerCase() === r.toLowerCase()
amurzeau commented 2 years ago

I think that function is also here: https://github.com/devcontainers/cli/blob/839ef66ae95820b41b3faec09764e6d30fc8abb4/src/spec-common/cliHost.ts#L88