Vino-serverからRemminaに接続する

こんにちは。今回は東京大学電気系の実験である大規模ソフトウェアを手探るという実験のなかで取り組んだ内容について書いていきたいと思います。

Remminaへの逆接続を行う(サーバ->クライアント)

私は同じ学科の友人とペアを組み上の課題について取り組みました。 まずRemminaって何というところから説明していきたいと思います。 Remminaというのはデスクトップ共有ソフトの一つで、簡単に言うと、見たいさきのPCのIPアドレスを指定して接続しに行くとそのPCの画面を自分側でも見ることができるというソフトです。これはVNC接続と呼ばれるそうです。もう少し具体的に説明します。

f:id:nicolen22:20191024105202p:plain
図1 Remmina接続の構造

上の図1のような形になります。ここでVino-serverが登場します。文字通りVNC接続のサーバとしての働きを持ち、これは画面共有される側のPC内で動いているソフトでRemmina側に画面データを送る働きをしています。PCの設定で画面共有をONにしていると常に裏で動いていることが確認できます。まずこれが動いていることが前提です。この下で以下の様な流れになります。

(1) Remmina側で相手先のIPアドレスGUI上で指定します。私たちはIPv4で指定しましたがコードを読む限りIPv6でも問題ないと思われます。

(2)Remmina側で接続というボタンを押すとプログラム上ではconnectが呼ばれます。すなわちVino-server側に接続しに行くことですね。ここからわかるようにRemminはクライアントとして働きます.。

(3)connectの通信が届くとVino-serverはacceptします。(当然PC使用者の許可が必要です) ソケットを作ってそこでTCP通信を行うという前期実験でも行なった流れとほぼ同じです。

(4)acceptまで問題なく処理されるとVino-serverは自らの画面データをRemmina側に常時送信していきます。

(5)Remmina側は1つのウィンドウ内で相手の画面を閲覧できます。

上のような流れが構造です。そして私達が取り組んだのは小見出しで述べたように connectをVino-server側からRemminaでaccpetする接続を作ろうということです。 動機としては、実際に学校でRemminaを使う場面を考えてみます。教員が学生のPC画面を見たいという場面が多々あると思います。このときにRemmina側から毎回毎回IPアドレスを指定して接続するようだと明らかに手間も時間もかかります。しかし逆向きですと大勢の学生が教員のIPアドレスだけを指定して接続するだけで教員は学生の画面を見ることができます。つまり効率化できます。これを実現するには上記で述べた逆向きの接続が必要になってくることになります。

つまり以下の図2のような構造を実現したいということになります。

f:id:nicolen22:20191104123825p:plain
図2 実現したい構造

では具体的に取り組んだことを細かく書いていきます。

具体的な取り組み

まずRemminaとVino-serverのソースを取ってくる必要がありますがgithubにありますのでググってgit cloneしてください。(どちらもC言語で書かれております)

環境 Ubuntu 18.04.3 LTS

メモリ 16GB

Remmina build方法

github.com

Remminaに関しては公式がbuild方法を出しているのでこれを脳死で実行しましょう()。

実行の仕方はremminaがあるフォルダに移動して

./remmina

これで大丈夫です。

-Vino-server build方法

$sudo apt install gnome-common

$sudo apt install libnotify-dev

$cd (フォルダの場所)/vino-master

$./autogen.sh

$make

$sudo make install

実行 ./vino-server (vino-serverというファイルのあるディレクトリで)

これで実行できます。Vino-serverは画面共有がONになっていると裏でもともとのが走っているので画面共有をOFFにしてから実行してください。(OFFにして実行しないとエラーメッセージが出るのでわかります)

実装までの流れ及び方針

ここでRemminaを触っていると気づくのですがListenermodeといういかにもな名前の選択肢があることがわかります。そのモードではGUIからポート番号を指定して接続を押します。 いかにも外部から何かしらの接続を待った状態 (accept待ち)が起きているのではないかと考えられます。ただこのモードはもともと正規版のRemminaにはない機能だったのでまだ正しく動くのかわからない状態でした。 ですのでとりあえずここを手探っていきたいと考えます。 がここから闇に入ります。まず動かし方が全くわかりません。どうやってListenermodeにつなげようとしてもいくら試してもできません。英語でググってもソースコードを読んでもいまいち接続の方法がわかりませんでした。 仕方ないのでVino-server側のmain関数のはじめにとりあえずTCPのクライアント機能を持たせてRemminaのListernermodeにつなげてみようとしました。するとつながることはデバッガを走らせながら実行することで確認できましたが、データの送信ができません。またncでつなげても全く同じでした。 つまり接続はできるけどその後の処理に正しく進んでくれないということがわかりました。

tcp通信に関しては以下のサイトを参考にしてコードを書くとよいかと思われます。

リンク

ここからVino-server側のプログラムをひたすら読んでいくことになります。どうやら通常用ではListenするためのソケットを作り それを使ってacceptするという構造が見えます。またaccepした後にrfbNewClient()という関数が呼ばれそこからIPアドレスの種類やパスワードに関する情報などを通信し最終的に画面データの送信が行われるという構造が見えてきました。ここで私はそれなら、ソケットを作るところでもともとあるListen用に加えてconnect用のソケットも作ってconnectできればrfbNewClient()という関数に飛べば良いのではないかと考えました。 この方針で実装し、コマンドライン引数からIPアドレスとポート番号を取れるようにして動かしたところ、やはり接続はできましたがそのあとの通信ができませんでした。つまりはじめと同じです。

発表前日まで途方に暮れていましたがチームメイトの方がほんとに頼りになってくれて、なんと動くように実装してくれました。どうしたかということですがこのrfbNewClient()という関数にはvino-serverの情報を持つ構造体(以下server構造体)の中の一部の変数が引数に与えられていました。最初の方針ではこのserver構造体が正しく実体を伴っておらず、つまり引数によくわからないものを入れていたというのが考えられました。 それならserver構造体が初期化されたあとにconnectしに行けば良いはずと考え、vino_server_new()みたいな名前のそれらしい関数にbreakpointをつけてデバッグするとname_acqired()という関数にたどり着きました。この関数が終わった時server構造体は初期化されることがわかりました。ここまできたらこの関数の最後にconnectそしてrfbNewClient()を呼べば行けるはず!ということでやってみると無事動きました!!ほんとに感謝です。 もう少し言うと単にname_acquired()の最後に置くだけではrfbNewClient()の引数のserver構造体がこのname_acquired()という関数のヘッダーファイルにないため一旦そのserver構造体が定義されているヘッダーファイルのあるファイルに飛んでそこで上の2つの関数を呼ぶという形になります。(実はここで半日程度止まりました笑) ということで我々のやりたかった逆向きの接続を完了できました。 相方いわく若干バグらしき状況があるみたいですが普通に使用する分では問題ないと思います。

具体的なコードですが、変更と言うより追加していった感じでして、細かいところをあげれば結構多いのでこのブログで載せるとかえって見づらくなると思われるのでconnect周りの必要最小限の処理だけ記載して詳しくは以下のリンクを参照してください。(tcp接続に関しては上で貼ったリンクのコードを基に今回使う機能だけを残して実装しております。) なんとか間に合わせた感じなのでかなり汚いコードになっていますが、vino-main.cとvino-server.cというファイルだけを見ていただくて、vino_connect_from_vino_server()という関数を作りname_acquired()関数の最後でconnect処理を行い、その後にrfbNewClient()を呼ぶ形が見て取れると思います。

まずコマンドライン引数からIPアドレスとポート番号を指定します。(グローバル変数にしてしまいごめんなさい)

#ifdef DEBUG
char  ip_add[256];
int port_num;
#endif // DEBUG

int
main (int argc, char **argv)
{
  if(argc==3){   
    strcpy(ip_add,argv[1]);
    port_num=atoi(argv[2]);
  }
...
g_main_loop_run (vino.main_loop);
...
return 0;
}

次にconnectまわりの関数を作りました。

struct client_info {
    unsigned short sv_port;
    char *sv_ipaddr;
};
typedef struct client_info cl_info_t;


static int
tcp_send(int sd, struct sockaddr_in *sv_addr, 
             char *errmsg)
{
    int rc = 0;
   
    rc = connect(sd, (struct sockaddr *)sv_addr, sizeof(*sv_addr));
    if(rc != 0){
        sprintf(errmsg, "(line:%d) %s", __LINE__, strerror(errno));
        return(-1);
    }
    return(0);
}

int sd = 0;
static int
tcp_client(cl_info_t *info, char *errmsg)
{
    struct sockaddr_in sv_addr = {0};
    
    int rc = 0;

    sd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
    if(sd < 0){
        sprintf(errmsg, "(line:%d) %s", __LINE__, strerror(errno));
        return(-1);
    }

    memset(&sv_addr, 0, sizeof(sv_addr));
    sv_addr.sin_family = AF_INET;
    sv_addr.sin_addr.s_addr = inet_addr(info->sv_ipaddr);
    sv_addr.sin_port = htons(info->sv_port);

    rc = tcp_send(sd, &sv_addr,  errmsg);

    return( rc );
}


static int
initialize(int argc, char * ipaddres ,int port, cl_info_t *info, char *errmsg)
{
    if(argc != 3){
        sprintf(errmsg, "Usage: %s <ip-addr> <port>",ipaddres );
        return(-1);
    }

    memset(info, 0, sizeof(cl_info_t));
    info->sv_ipaddr = ipaddres;
    info->sv_port   = port;
  

    return(0);
}


gboolean vino_connect_from_vino_server(int port_num,VinoServer *server){

    if(port_num != 0){
      int rc = 0;
      cl_info_t info = {0};
      char errmsg[256];
      
      rc = initialize(3, ip_add,port_num, &info, errmsg);
      if(rc != 0){
          fprintf(stderr, "Error: %s\n", errmsg);
          return(-1);   
      }
      rc = tcp_client(&info, errmsg);
      if(rc != 0){
          fprintf(stderr, "Error: %s\n", errmsg);
          return(-1);
      }
    rfbNewClient(server->priv->rfb_screen,sd);
    }
  return TRUE;
}

そして、name_acquired()という関数から上の関数を最後に呼び出します。

name_acquired (GDBusConnection *connection,
               const gchar     *name,
               gpointer         user_data)
{
  VinoApplication *vino = user_data;
  gboolean view_only;
  gint i;
  gboolean reject = FALSE;

 ...

  /* Name is acquired.  Start up the servers and register them with the
   * listeners.
   */
  if ((view_only = !vino_input_init (vino->display)))
    g_warning (_("Your XServer does not support the XTest extension — "
                 "remote desktop access will be view-only\n"));
  for (i = 0; i < vino->n_screens; i++)
    {
      VinoServer *server;
      /* The server is initially "on-hold" while we set everything up. */
      server = vino_server_new (gdk_display_get_screen (vino->display,
                                                        i),
                                view_only);
       
      ...

      #ifdef DEBUG
      vino_connect_from_vino_server(port_num,server);
      #endif // DEBUG
      
    }
       
}

変更部分のコード

苦労したところ

上でも少し述べておりますが、通信まわりを触った経験は少ししかなかったためいろいろな場所で苦労しました。これを読んでくれた方に参考になれば幸いです。

デバッグがしづらい

これは通信周りですので1つのコードだけでデバッグするわけではありません。つまりクライアント、サーバ側ともにデバッガを立ち上げて実行しなければなりません。この時点で大変ですが、適切な位置にbreakpointを貼らないとすぐに接続されてしまい肝心の接続周りが見えません。このあたりに苦労しましたが、例えばconnectという関数やacceptという関数は必ず関わってきますのでまずこの名前からbreakpointを貼ることで かなり近づけることができます。そこからは1行ずつ進めていけば良いかと思われます。

・実装が変更と言うより追加になる

通常ですと、機能を変えるためには既存のコードの一部を変更、もしくは数行のコードを追加という作業がメインとなると思われます。これも変更箇所が多岐に及べば大変難しいですが、通信ですとソケットを作ってconnect...という仕様を作り新たなプロトコルで接続させるコード追加となります。そのためまずはどのような構造にするかという紙の上での議論から始まりました。そのうえで既存のコードではどのように実装されているかを調べ、その形に合わせ込めるように構造を考えなければなりません。私達も実験時間の3割ほどはそれに費やしたといいても過言ではないです。これに関してですが、サーバ、クライアントがconnect,acceptを行うわけですがその処理が行われた後をみると少しわかりやすくなると思われます。つまり接続された後は似ている処理をしているわけで、ここが新たな接続方針の終着点にしていけばよいからです。

課題

時間が限られていたということと動いたのが最終日の前日だったこともあって、やりたかったけどつめきれていないこともありました。

・Remmina側でVino-serverの画像データ送信を止める

これはRemminaへの多数接続状況では使っていない(あまり動かしていない)PCの画面の送信を止めることができればRemmina側のPCのメモリ削減につながると思われます。これも実装しようとしてみました。Vino-server側で画像を送ると思われる関数にbreakpointを貼りまくって何度もデバッガを走らせるとその関数部分にたどり着きました。rfbSendRectEncodingTight()という関数です。 emaccsデバッガでこの関数にbreakpointを貼って何度もcontinueをしていくと画像送信が少しずつされていることがわかりました。

これを標準入力でqが入るとこの関数が呼ばれずすなわち画像送信がストップし、rで また再開するみたいなプログラムは作れ、実際に動くことは確認できました。 しかし現状Vino-server側での操作できる状態であって、これではそんなに使い物にならないですよね。笑 また相方が言うには送信に使われている関数の中身を書き換えるやり方は良くないみたいなのでコードとしてもイマイチなので再考が必要と考えられます。

終わりに

本実験では大規模ソフトウェアを手探るという内容でしたが10日間の7日くらいはほんとに何もコードをかけませんでした。しかし例えばデバッグの仕方や、大規模なソフトウェアの構造がどういうものなのか またbuildの仕方までわからないことだらけだったのですがそれらを少しだけですができるようになったといえる実験でした。普段は競技プログラミングとかの短いコードを書くというのがメインでしたのでこのような 普段ひとりではとっつきにくいことをやることができ、かつ悩んだりすることも楽しかったりして最高の実験でした。

最後までたくさんのアドバイスをしていただいたり何時間もつきっきりで見ていただいた先生そしてTAさんにはほんとに感謝しかありません。ありがとうございます。