screenでSSH_AUTH_SOCKが陳腐化するのをLD_PRELOADでごり押ししてみた話

\<ins\>2018/04/26追記

# 結局2年たってどうなった

`find_agent` というシェル関数を書いて、都度都度手動解決するように。
自動で切り替えるのはたしかに便利なんだけど、これが必要なタイミングって少ないし、デバッグしててLD_PRELOADがときどき邪魔になることあるしで、あまりメリットがなかった……

SSH_AUTH_SOCKの接続確認にはgithubを使ってしまっています。
普段使っている鍵はだいたいgithubにもいれているので、というか、sshで困るのだいたいgit pushのタイミングなので
ソケットファイルはパス名を適当にグロブで引っ掛けるように

```bash
find_agent () {
    local GLOBS=("/tmp/com.apple.launchd.*/Listeners" "/tmp/ssh-*/agent.*");
    for g in "${GLOBS[@]}"; do
        for c in ${g}; do
            SSH_AUTH_SOCK="$c";
            [ ! -S "${SSH_AUTH_SOCK}" ] && continue;
            ssh -T git@github.com;
            [ $? -eq 1 ] && return 0;
        done;
    done
}
```
\</ins\>

以下2016/02/12の記述

----

# github
https://github.com/takei-yuya/alt_ssh_auth_sock

スキー旅行で秋田に来たけど、宿で暇してたので書いてみた。

# これはなに？

sshの接続先でscreenを使っているときに、デタッチ/アタッチでssh-agent転送に使うSSH_AUTH_SOCKの参照がズレてしまう問題を防ぐお話。

つまりこういうこと

```shell-session
local $ # ssh-agent転送を有効にしてhostAに接続
local $ ssh -A hostA

hostA $ # 接続先で自動でSSH_AUTH_SOCK環境変数が設定される
hostA $ declare -p SSH_AUTH_SOCK
declare -x SSH_AUTH_SOCK="/tmp/ssh-XXXXXXXXXX/agent.11111"
hostA $ # このソケットはこのセッションのsshdに繋がっている
hostA $ ps -p 11111
  PID TTY          TIME CMD
11111 ?        00:00:00 sshd
hostA $ # hostA上に鍵がなくても、ソケットを通じて鍵情報が転送されるので
hostA $ # 別のホストに鍵認証で接続できる
hostA $ ssh hostB  
hostB $ exit

hostA $ # screenはscreenを起動した時の環境変数を保存する
hostA $ screen -S ssh_test
hostA(screen) $ declare -p SSH_AUTH_SOCK
declare -x SSH_AUTH_SOCK="/tmp/ssh-XXXXXXXXXX/agent.11111"
hostA(screen) $ ^A^D  # デタッチ

hostA $ # このソケットはSSHのセッションが切れると無効になる
hostA $ # 再接続してみる
hostA $ exit
local $ ssh -A hostA
hostA $ # セッションが変わったのでソケットのパスが変わる
hostA $ declare -p SSH_AUTH_SOCK
declare -x SSH_AUTH_SOCK="/tmp/ssh-YYYYYYYYYY/agent.33333"
hostA $ # 古いセッションのsshdは既にいなくなっている
hostA $ ps -p 11111
  PID TTY          TIME CMD

hostA $ # しかし、screenのセッションでは環境変数が古いままなので……
hostA $ screen -x ssh_test
hostA(screen) $ declare -p SSH_AUTH_SOCK
declare -x SSH_AUTH_SOCK="/tmp/ssh-XXXXXXXXXX/agent.11111"
hostA(screen) $ # 古いssh-agent転送は繋がらない……。
hostA(screen) $ ssh hostB
Permission denied (publickey).
```

ということで、長くなりましたが、こういう現象。
これをどうにかしたい。

screen上のセッションで環境変数が更新されない、というのがそもそもの原因。

# どうする？

LD_PRELOADという機構を使ってみる。

ざっくりというと、プロセスの起動時に動的ライブラリを無理矢理差し込む機構らしい。
これを使うことで、system callやライブラリ関数の呼び出しをフックしたり横取り四十万したりすることができる。

狙う関数は `getenv` 。 つまり環境変数を無理矢理外部からいじろうというアイデア。

現在の `SSH_AUTH_SOCK` を確認して、ソケットに接続できないようなら、別のソケットを試す。試すソケットの候補は、別の環境変数からファイルグロブパターンで指定する。

つまりこう

```shell-session
hostA(screen) $ ssh hostB
Permission denied (publickey).
hostA(screen) $ export LD_PRELOAD="/path/to/injection_lib"
hostA(screen) $ export ALT_SSH_AUTH_SOCK="/tmp/ssh-*/agent.*"
hostA(screen) $ ssh hostB
```

# こうなった

github: https://github.com/takei-yuya/alt_ssh_auth_sock

```cpp
#define _GNU_SOURCE  // for RTLD_NEXT
#include <stdlib.h>
#include <dlfcn.h>
#include <string.h>

#include <glob.h>

#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>

// もとのgetenvを保存するためのstatic変数
static char* (*_original_getenv)(const char *name) = NULL;

// GNU拡張機能を使うと、main関数前に実行する関数が作れるらしい
static void _alt_ssh_auth_sock_init() __attribute__((constructor));
static void _alt_ssh_auth_sock_init() {
  // 元のgetenvを保存
  _original_getenv = dlsym(RTLD_NEXT, "getenv");
}

// ソケットが生きているか確認する。……ただconnectしているだけだけど。
int check_socket(const char* socket_file_path) {
  struct sockaddr_un addr;
  addr.sun_family = AF_UNIX;
  strncpy(addr.sun_path, socket_file_path, sizeof(addr.sun_path) / sizeof(char));

  int fd = socket(AF_UNIX, SOCK_STREAM, 0);
  int ret = connect(fd, (struct sockaddr *)&addr, sizeof(struct sockaddr_un));
  close(fd);
  return ret;
}

// getenvの挙動を上書きする
char* getenv(const char *name) {
  if (!_original_getenv) {
    // なんか__attribute__((constructor))が効かないことがあるので念ため
    _alt_ssh_auth_sock_init();
  }

  if (strcmp(name, "SSH_AUTH_SOCK") != 0) {
    // SSH_AUTH_SOCK以外はそのまま元の関数にぶんなげ
    return _original_getenv(name);
  }

  char* ssh_auth_sock = _original_getenv("SSH_AUTH_SOCK");
  if (!ssh_auth_sock) {
    // SSH_AUTH_SOCKが定義されてない場合はそのままreturn。
    return ssh_auth_sock; // == NULL
  }

  char* alt_ssh_auth_sock = _original_getenv("ALT_SSH_AUTH_SOCK");
  if (!alt_ssh_auth_sock) {
    // 代替ソケットのパターンが与えてられなかった場合はなにもできないのであきらめてそのまま返す
    return ssh_auth_sock;
  }

  if (check_socket(ssh_auth_sock) == 0) {
    // 現在のソケットが生きていたらグロブ展開をせずそのままreturn
    return ssh_auth_sock;
  }

  // グロブパターンを展開
  glob_t pglob;
  if (glob(alt_ssh_auth_sock, GLOB_NOSORT, NULL, &pglob) != 0) {
    globfree(&pglob);
  }

  int i;
  for (i = 0; i < pglob.gl_pathc; ++i) {
    if (check_socket(pglob.gl_pathv[i]) == 0) {
      // 生きているソケットを発見したら
      break;
    }
  }
  if (i < pglob.gl_pathc) {
    // SSH_AUTH_SOCKを上書き
    setenv("SSH_AUTH_SOCK", pglob.gl_pathv[i], 1);
    ssh_auth_sock = _original_getenv("SSH_AUTH_SOCK");
  }
  globfree(&pglob);

  return ssh_auth_sock;
}
```

短いので全掲。
詳細に関してはコメントに書いた通りですが、もともとのgetenvを保存した上で、上書きするgetenv内から使っています。

ビルド方法はこんな

```bash
cc -Wall -fPIC -shared -o libaltsshauthsock.so alt_ssh_auth_sock.c -ldl
```

sonameとか必要かも。この辺ちょっと適当。

# どう使う？

とりあえず、bashrcかなにかで環境変数設定してしまえばいいんじゃないかなと思います。

```shell-session
$ echo 'export LD_PRELOAD="/path/to/libaltsshauthsock.so"' >> ~/.bashrc
$ echo 'export ALT_SSH_AUTH_SOCK="/tmp/ssh-*/agent.*"' >> ~/.bashrc
```

あとは普段どおりscreenを使うだけ。
アタッチ/デタッチやセッション切断を繰り返しても、何もしないでsshやgitが使えるようになる、はず

# 次どうする？

TODO:

- Macに対応する
    - Macの場合LD_PRELOADじゃなくて、DYLD_INSERT_LIBRARIES という環境変数を使うらしい
        - 加えて名前空間？の問題があって、 DYLD_FORCE_FLAT_NAMESPACE という環境変数も設定する必要があるらしい
    - Makefileでdylibを作るようにする
    - ……でもMacにssh繋ぐ機会あまり無いんだよなぁ……。
- 毎回ソケット繋ぐのコストでかくね？
    - でかいと思うからどうにかしたい。
    - 多分正攻法は、ソケットの死活管理しながら、1対多のソケット配管するデーモン立てて、そのデーモンに繋がるソケットをSSH_AUTH_SOCKに設定する、だと思う。
    - gdbを使いプロセスにアッタチしてsetenvを蹴って、無理矢理そのプロセスの環境変数を書き換えてしまう、なんて手を試してみたけどさすがに力技すぎる気がする。
- もちっと真っ当な実装にする
    - エラーチェックぐらいはしようか。
- スキーする
    - スキーしに秋田にきたのでスキーする。開発するためじゃないので。