• 作成:

Nix-on-DroidをセットアップしてAndroidの最良のSSHクライアントを手に入れた

背景

AIエージェントを常用するようになった

Claude CodeのようなAIエージェントを日常的に使うようになったことで、 Androidでのプログラミング作業が現実的になりました。

所有しているGalaxy Z Fold7はインナーディスプレイが7.6インチあるので、読む分には不都合はそれほどありません。問題は画面内にソフトウェアキーボードを出して入力する場面で、これはさすがに辛くなります。しかしAIエージェントが多くの入力を代わりに行ってくれるようになったので、自分がキーボードで打つのを絶対に求められるということは大幅に減りました。まだ存在はするのでその時は大人しくPCで行いますが。

動機

Nix-on-Droidをセットアップした直接の動機は、 Androidで使える最良のSSHクライアントが欲しかったからです。

他の選択肢の検討

Claude公式の仮想環境

Anthropicは公式に、 claude.aiからリモートの仮想環境(Claude web)に接続してClaude CodeをAndroidのClaude appから操作できるソリューションを提供しています。

しかし試してみると、グローバルにインストールされているソフトウェアが貧弱すぎて使い物になりませんでした。 GitHub CLIすら入っておらず、当然Nix FlakesでセットアップしてあるNix環境も使えません。自分のdotfilesで管理している設定も反映されません。

自宅サーバに直接SSH接続してClaude Codeを使う方が圧倒的に快適です。

ConnectBot

シンプルにSSH接続だけをするオープンソースの選択肢として、 ConnectBotがあります。

しかしConnectBotはGPG形式の認証鍵に対応していません。自分はGPGを導入して署名用サブキーを端末ごとに分けて運用することにしました - ncaq に書いたようにSSH認証にGPGの認証サブキーを使っているため、 ConnectBotではSSH鍵認証ができません。

SSH秘密鍵形式に変換しようとしましたが、 RSAではない楕円曲線暗号(ed25519)形式の変換方法が分からなかったのでこの方向は断念しました。

AndroidのLinuxターミナル

Android 15からLinuxターミナル機能が実験的にサポートされています。これはVirtual Machine Manager APIを使ってLinux VMを動かすもので、より本格的なLinux環境が手に入りそうでした。

しかし現時点ではSnapdragon搭載端末には対応していないようで、 Galaxy Z Fold7はSnapdragon 8 Elite搭載のため試せませんでした。

Nix-on-Droidを選んだ理由

Nix-on-Droidを選んだ理由は主に以下の通りです。

  • OpenSSHをそのまま使えるため、GPGエージェント経由のSSH認証も問題なく動作する
  • home-managerで宣言的に設定管理できる
  • 普段のNix環境を流用できるので、ちょっとしたデータフォーマット変換などをサーバに接続せずに端末内で完結できる
  • 端末を買い替えた際も設定を再現しやすい

Nix-on-Droidとは

nix-community/nix-on-droidは、 Termuxベースの環境にNixとhome-managerを組み込んで、 Android端末でNixパッケージとhome-manager設定を使えるようにするプロジェクトです。

systemd-nspawnのようなコンテナでもなく、 AndroidのLinuxサブシステムでもなく、 prootというユーザーランドエミュレーションのレイヤの上でNixが動いています。

NixOSモジュールは使えませんが、 home-managerはそのまま使えるので既存のdotfiles設定をほぼ流用できます。

事前準備

パッケージのクリーンアップ

fix: パッケージのクリーンアップと正確性の向上 by ncaq · Pull Request #500 · ncaq/dotfiles

まず先に、既存のhome-manager設定をaarch64-linux向けにビルドしようとしたところ、パッケージの構成が雑然としていてarm向けに整理する必要がありました。

  • home/packageディレクトリをhome/core等に分割して構成を整理
  • ネイティブLinux向けGUIパッケージをhome/native-linuxに分離
  • home.usernamelib.mkDefaultに変更してNix-on-Droidの固定ユーザー名制約に対応

arm対応

feat: arm対応 by ncaq · Pull Request #517 · ncaq/dotfiles

xmonadなどのX11関連設定やGUIアプリをx86_64環境のみに制限しました。また、CIにarm向けのhome-managerビルドを追加して、 aarch64-linuxでもビルドが通ることを継続的に確認できるようにしました。

実装

feat: TermuxベースのNix-on-Droidで動作 by ncaq · Pull Request #498 · ncaq/dotfiles

flake.nixへのnix-on-droid追加

nix-on-droid = {
  url = "github:nix-community/nix-on-droid/release-24.05";
  inputs = {
    nixpkgs.follows = "nixpkgs";
    home-manager.follows = "home-manager";
  };
};

nixpkgshome-managerのinputsを自前のものに追従させることで、依存関係の一貫性を保っています。

nix-on-droid/default.nix

nix-on-droid.lib.nixOnDroidConfiguration {
  inherit pkgs;
  modules = [
    {
      system.stateVersion = "24.05";
      nix.extraOptions = ''
        experimental-features = flakes nix-command
      '';
      environment = {
        etcBackupExtension = ".bak";
        # Tailscale前提のDNS設定。
        etc."resolv.conf".source = ./resolv.conf;
      };
      android-integration = {
        am.enable = true;
        termux-open.enable = true;
        termux-open-url.enable = true;
        xdg-open.enable = true;
        # ...
      };
      terminal.font = "${pkgs.firge-nerd-font}/share/fonts/firge-nerd/FirgeNerdConsole-Regular.ttf";
      # Androidホストのタイムゾーンは自動的に引き継がれないので明示的に設定。
      time.timeZone = "Asia/Tokyo";
      user.shell = "${pkgs.zsh}/bin/zsh";
      home-manager = {
        extraSpecialArgs = {
          isTermux = true;
          isWSL = false;
          # ...
        };
        config = ../home;
      };
    }
  ];
  home-manager-path = home-manager.outPath;
}

ポイント

ユーザー名の固定

Nix-on-Droidのuser.userNameは現状"nix-on-droid"固定でread-onlyです。これに合わせてhome.usernamelib.mkDefaultにして、プラットフォームの制約が優先されるようにしました。

DNSの設定

Nix-on-DroidはホストAndroidのDNS設定を自動的に引き継ぎません。自宅ネットワークはTailscale前提で組んでいるため、 resolv.confを明示的にTailscaleのDNS(100.100.100.100)に向けています。

フォントの設定

Termuxのターミナルフォントは1つしか指定できません。 Nix-on-Droidではterminal.fontで設定できます。普段のLinux環境と同じFirgeNerdフォントを指定しました。

isTermuxフラグによる環境分岐

既存のhome-manager設定を流用するにあたって、 isTermuxというフラグをextraSpecialArgsで渡して環境分岐しています。名前がisNixOnDroidでなくisTermuxなのは、ランタイムの実態がTermux相当だからです。

systemdがないことへの対応

Termux環境ではsystemdが動いていません。 systemdサービスに依存している設定はスキップされるだけなので、使わない機能であればそのまま放置で問題ありません。ただし必要な機能については代替の設定が必要です。

gpg-agentの起動

通常環境ではservices.gpg-agentでsystemdサービスとして管理していますが、 Termux環境ではzshの初期化スクリプトでシェル起動時に立ち上げています。

if !isTermux then
  {
    services.gpg-agent = {
      enable = true;
      enableSshSupport = true;
      pinentry.package = pkgs.pinentry-qt;
      sshKeys = [ sshKeygrip ];
    };
  }
else
  {
    programs.zsh.initContent = ''
      export GPG_TTY=$(tty)
      gpgconf --launch gpg-agent
      export SSH_AUTH_SOCK=$(gpgconf --list-dirs agent-ssh-socket)
    '';
    home.file.".gnupg/gpg-agent.conf".text = ''
      enable-ssh-support
      pinentry-program ${pkgs.pinentry-curses}/bin/pinentry-curses
    '';
    home.file.".gnupg/sshcontrol".text = ''
      ${sshKeygrip}
    '';
  }

ここが一番重要な部分です。 Termux環境でもGPGエージェント経由でSSH認証できるので、 GPGサブキーをそのままSSH鍵として使えます。

sops-nixの対応

sops-nixは通常、 systemdサービス経由でシークレットを復号化します。 Termux環境ではlib.mkForceで標準のactivation hookを上書きして、 sops-install-secretsを直接実行するようにしました。

home.activation.sops-nix = lib.mkForce (
  lib.hm.dag.entryAfter [ "writeBoundary" ] ''
    export XDG_RUNTIME_DIR="${cfg.defaultSecretsMountPoint}"
    export SOPS_GPG_EXEC="${cfg.gnupg.package}/bin/gpg"
    $DRY_RUN_CMD ${sops-install-secrets}/bin/sops-install-secrets -ignore-passwd ${manifest}
  ''
);

また、Termux環境では$XDG_RUNTIME_DIRが設定されていないため、シークレットの配置場所を明示的に~/.local/state/sops-nix/secrets.dに指定しています。

その他の無効化

  • gtk/dconf: Termux環境ではdconfにアクセスできないので無効化
  • attic-push: attic watch-storeのsystemdサービスはTermux環境では無効化
  • keybase: systemdサービスを無効にしてパッケージのみインストール

install.shの対応

# gitがPATHにない場合gitをPATHに追加して再実行。
if ! command -v git &>/dev/null; then
  exec nix shell 'nixpkgs#git' --command "$0" "$@"
fi

if [ -f /etc/NIXOS ]; then
  sudo nixos-rebuild switch --flake ".#$(hostname)"
elif [ -n "${TERMUX_VERSION:-}" ]; then
  nix-on-droid switch --flake "."
else
  # ...
fi

TERMUX_VERSION環境変数はTermux環境で自動的に設定されているので、これを使って環境を判別しています。

Nix-on-Droidの初期状態ではgitがインストールされていないため、 gitが見つからない場合はnix shell nixpkgs#gitで一時的にgitを用意してから再実行するようにしています。

tmuxのAndroid向け調整

feat(tmux): Tmuxのネストトグルキーを変更 by ncaq · Pull Request #537 · ncaq/dotfiles

tmuxの設定をAndroidフレンドリーにする · Issue #530 · ncaq/dotfiles

実際にAndroidで使ってみると、 tmuxのネストトグルキー(F12など)がAndroidのソフトウェアキーボードでは押しにくいことが分かりました。 AndroidでSSHする場合はほとんどの操作がサーバ側のtmuxで完結するため、 Androidで押しやすいキーに変更しました。

sops-nixインストール時の問題修正

fix: Nix-on-Droid環境でインストールする時にsops-nixが失敗することを再回避 by ncaq · Pull Request #595 · ncaq/dotfiles

新しい端末などで改めてインストールする際に、 sops-nixのactivation hookがDAGの関係で正しく実行されない問題がありました。 entryAfter [ "writeBoundary" ]を適切に設定して修正しました。

Claude Codeについて

本来の目的のひとつがClaude CodeをAndroidから使うことでした。実際に試してみると、 prootエミュレーションとの相性か何かでClaude Codeはうまく動きませんでした。

本家Termuxなら動いたという記事があるので動作自体は可能なようです。今回のNix-on-Droidはprootベースなので、そこが原因かもしれません。

ただそもそもスマートフォンで開発するにしても、 Snapdragon 8 EliteのCPU性能は十分としても、端末の16GBメモリよりサーバの64GBメモリで作業した方が快適です。なのでClaude Codeはサーバ側で動かすという運用に落ち着きました。

感想

Nix-on-Droidを使ってみて、既存のhome-manager設定をisTermuxフラグで分岐するだけで、ほぼそのまま流用できたのは思ったより楽でした。

特にGPGエージェント経由のSSH認証がそのまま使えたのは大きいです。 GPGの認証サブキーはSSHの秘密鍵とは仕組みが異なるため、 OpenSSHクライアントを直接使えない環境では認証が面倒になりがちです。 Nix-on-DroidではOpenSSHをそのまま使えるので、 gpg-agentをSSH_AUTH_SOCKに設定するだけで完全に動作しました。

まとめ

Nix-on-Droidをセットアップし、 AndroidをNix環境込みのSSHクライアントにしました。

  • ConnectBot等は楕円曲線暗号のGPG認証鍵に対応していないのでNix-on-Droidを選択
  • isTermuxフラグで既存のhome-manager設定を分岐して流用
  • systemdがない環境でgpg-agentをzshから起動してSSH認証を実現
  • sops-nixのactivation hookを直接実行方式に置き換え

セットアップ後にClaude Codeの公式リモートコントロール機能が発表されたので、今後そのUX次第ではこの方法の役割が変わるかもしれません。とはいえNix環境がAndroidで使えること自体は便利です。端末を買い替えた際もinstall.sh一発で環境が再現できるのは継続して有用です。

主なソースコードはncaq/dotfilesにあります。