目的
SSH接続について解説してある記事は五万とありますが、その説明は認証や暗号化の仕組みについてであり、どうやって遠隔で操作しているかについてまで言及している記事は多くありません。
そこで今回はC言語を用いてWindows10からUbuntuを操作する機能を実装してみました。
※接続の認証や暗号化は一切していません。
動作
まずは動作する様子をお見せします。
WindowsからUbuntuを操作しています。
Windows(操作側)
C:\Users\test\kaihatsu>client.exe 192.168.68.245 2223
Connected to 192.168.68.245:2223
Type commands; 'exit' to quit.
> ls
server
server.c
> pwd
/home/test/kaihatsu/remote
>
C:\Users\test\kaihatsu>
Ubuntu(被操作側)
test@test-ThinkPad-X280:~/kaihatsu/remote$ sshd
sshd re-exec requires execution with an absolute path
test@test-ThinkPad-X280:~/kaihatsu/remote$ sudo ./server 2223
Listening on port 2223
実装
Windows10(操作側)
// client_win.c
// コンパイル: x86_64-w64-mingw32-gcc client_win.c -o client.exe -lws2_32
// 使用法: client.exe 192.168.68.XXX 2222
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#pragma comment(lib, "Ws2_32.lib")
/*
s:受信するソケット
buf:受信データを保存するバッファ
len:受信したいバイト数
ssize_t:受信したバイト数を返す(失敗時は負数)
*/
ssize_t recv_all(SOCKET s, void *buf, size_t len) {
size_t got = 0; // 今まで受信したバイト数
char *p = buf; // p:バッファを文字単位で扱いやすくするためのポインタ
while (got < len) { // まだ受信すべきバイトがある間ループ
// p + got:未受信部分の先頭
// len - got:未受信バイト数
int r = recv(s, p + got, (int)(len - got), 0); // 残りのデータを受信
if (r <= 0) return r; // 受信に失敗、または接続が閉じられた場合は即座に終了
got += r; // 実際に受信できたバイト数を加算
}
return got;
}
/*
s:送信するソケット
buf:送るデータの先頭アドレス
len:送るデータのバイト数
ssize_t:送信したバイト数を返す(失敗時は負数)
*/
ssize_t send_all(SOCKET s, const void *buf, size_t len) {
size_t sent = 0; // 今まで送ったバイト数
const char *p = buf; // bufを文字単位で扱いやすくするためのポインタ
while (sent < len) { // まだ送っていないデータがある間ループ
// p + sent:未送信部分の先頭
// len - sent:未送信バイト数
int r = send(s, p + sent, (int)(len - sent), 0);
if (r <= 0) return r; // 送信に失敗したら即座に終了
sent += r; // 実際に送れたバイト数を加算
}
return sent;
}
int main(int argc, char **argv) {
if (argc < 3) { printf("Usage: %s <server-ip> <port>\n", argv[0]); return 1; } // コマンド引数が足りない場合の処理
const char *ip = argv[1]; // 引数からUbuntuのIPアドレス取り出し
const char *port = argv[2]; // 引数からUbuntuのポート番号取り出し
// Winsockライブラリ初期化
WSADATA w;
if (WSAStartup(MAKEWORD(2,2), &w) != 0) { printf("WSAStartup failed\n"); return 1; }
struct addrinfo hints, *res = NULL;
memset(&hints,0,sizeof(hints)); // 構造体の中身を0で初期化
hints.ai_family = AF_INET; // IPv4使用
hints.ai_socktype = SOCK_STREAM; // TCP使用
// IP及びポートを結果をaddrinfo構造体へ変換し結果をresへ格納
if (getaddrinfo(ip, port, &hints, &res) != 0) { printf("getaddrinfo failed\n"); WSACleanup(); return 1; }
// ソケット生成 getaddrinfo()が返した値を使用する
SOCKET s = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (s == INVALID_SOCKET) {
printf("socket failed\n");
freeaddrinfo(res); // getaddrinfoで確保したメモリを解放
WSACleanup(); // Winsock を終了
return 1; // プログラムを異常終了
}
// TCP 接続を開始する
if (connect(s, res->ai_addr, (int)res->ai_addrlen) != 0) {
printf("connect failed\n");
closesocket(s); // ソケットを閉じる
freeaddrinfo(res); // getaddrinfoで確保したメモリを解放
WSACleanup(); // Winsock を終了
return 1; // プログラムを異常終了
}
freeaddrinfo(res); // もう使わないので解放
printf("Connected to %s:%s\nType commands; 'exit' to quit.\n", ip, port);
char line[4096];//入力した文字列(コマンド)を一時保存する場所
while (1) { // exitと入力するまで、コマンド入力→送信→結果受信→表示を繰り返す。
printf("> ");
if (!fgets(line, sizeof(line), stdin)) //標準入力から1行読み込む
break; //入力がない場合(EOF)やエラーならループ終了。
size_t len = strlen(line);
if (len > 0 && (line[len-1] == '\n' || line[len-1] == '\r')) {
line[len-1] = '\0';
if (len >= 2 && line[len-2] == '\r') line[len-2] = '\0';
}
// これから送るコマンドの長さをサーバに送る。
uint32_t n = (uint32_t)strlen(line);
uint32_t n_net = htonl(n);
if (send_all(s, &n_net, sizeof(n_net)) != sizeof(n_net)) break;
// コマンド本体を送信
if (n > 0) {
if (send_all(s, line, n) != (ssize_t)n) break;
}
// Ubuntuからこれから送る出力の長さを受け取る。
uint32_t rlen_net;
if (recv_all(s, &rlen_net, sizeof(rlen_net)) != sizeof(rlen_net)) break;
uint32_t rlen = ntohl(rlen_net);
// サーバが何も返さなかった場合
if (rlen == 0) {
if (strcmp(line, "exit") == 0) break;
printf("(no output)\n");
continue;
}
char *buf = malloc(rlen + 1);// 出力バッファを確保
if (!buf) break;
// 受信
if (recv_all(s, buf, rlen) != (ssize_t)rlen) {
free(buf);
break;
}
buf[rlen] = '\0'; // 文字列終端 \0 を付加
printf("%s", buf); // ここでUbuntuから返って来た文字列を画面へ出力
if (rlen == 0 || buf[rlen-1] != '\n') printf("\n"); // 最後に改行がなければ自動で追加。
free(buf);
}
uint32_t zero = 0;
send_all(s, &zero, sizeof(zero));
closesocket(s);// ソケットを閉じる
WSACleanup(); // Winsock を終了
return 0;
}
Ubuntu(被操作側)
// server.c
// Build: gcc server.c -o server
// Run: sudo ./server 2222 (ポートは自由に変えてください)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#define BACKLOG 5
#define MAX_OUTPUT (1<<20) // 1 MiB max output per command
ssize_t recv_all(int fd, void *buf, size_t len) {
size_t got = 0;
char *p = buf;
while (got < len) {
ssize_t r = recv(fd, p + got, len - got, 0);
if (r <= 0) return r;
got += r;
}
return got;
}
ssize_t send_all(int fd, const void *buf, size_t len) {
size_t sent = 0;
const char *p = buf;
while (sent < len) {
ssize_t s = send(fd, p + sent, len - sent, 0);
if (s <= 0) return s;
sent += s;
}
return sent;
}
/*
c ソケット番号
*/
int handle_client(int c) {
while (1) {
// コマンドの長さを受け取る
uint32_t n_net;
if (recv_all(c, &n_net, sizeof(n_net)) != sizeof(n_net)) return -1;
uint32_t n = ntohl(n_net); // 受信した長さをホストバイト順に変換
if (n == 0) { // 何も入力せずENTERを押した場合→終了
return 0;
}
if (n > 65536) { // コマンドが長すぎたらエラー
fprintf(stderr, "command too long\n");
return -1;
}
// コマンド本体を受け取る
char *cmd = malloc(n + 1);
if (!cmd) return -1;
if (recv_all(c, cmd, n) != (ssize_t)n) { free(cmd); return -1; }
cmd[n] = '\0';
// コマンドが "exit" の場合は接続を閉じる
if (strcmp(cmd, "exit") == 0) {
free(cmd);
uint32_t outlen_net = htonl(0);
send_all(c, &outlen_net, sizeof(outlen_net));
return 0;
}
FILE *fp = popen(cmd, "r"); // コマンドを実行する
if (!fp) { // popen が失敗した場合
const char *err = "popen failed\n";
uint32_t outlen_net = htonl((uint32_t)strlen(err));
send_all(c, &outlen_net, sizeof(outlen_net));
send_all(c, err, strlen(err));
free(cmd);
continue;
}
// コマンドの出力を読み取ってバッファに溜める処理
char *outbuf = malloc(MAX_OUTPUT);
if (!outbuf) { pclose(fp); free(cmd); return -1; } // バッファ確保失敗
// 出力を読み取るループ
size_t total = 0; // 読み込んだ合計バイト数
while (!feof(fp) && total < MAX_OUTPUT) {
size_t r = fread(outbuf + total, 1, MAX_OUTPUT - total, fp); // fread で少しずつ出力を outbuf に読み込む
total += r; // これまでに読み取ったバイト数を合計する
if (r == 0) break; // もう読み取るデータがない場合にループを抜ける
}
pclose(fp);
//出力の長さを送る
uint32_t outlen = (uint32_t)total;
uint32_t outlen_net2 = htonl(outlen);
if (send_all(c, &outlen_net2, sizeof(outlen_net2)) != sizeof(outlen_net2)) {
free(cmd); free(outbuf); return -1;
}
// 実際の出力を送る
if (outlen > 0) {
if (send_all(c, outbuf, outlen) != (ssize_t)outlen) {
free(cmd); free(outbuf); return -1;
}
}
free(cmd);
free(outbuf);
}
}
int main(int argc, char **argv) {
if (argc < 2) { fprintf(stderr, "Usage: %s <port>\n", argv[0]); return 1; } // コマンド引数が足りない場合の処理
int port = atoi(argv[1]);
int s = socket(AF_INET, SOCK_STREAM, 0);//ソケット生成 IPv4 TCP
if (s < 0) { perror("socket"); return 1; }
// アドレス再利用を許可 終了直後同じポートでサーバを再起動可能になる
int opt = 1;
setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
struct sockaddr_in addr; // IPv4 用のソケットアドレス構造体を作成
memset(&addr,0,sizeof(addr)); // 構造体を0で初期化
addr.sin_family = AF_INET; // IPv4
addr.sin_addr.s_addr = INADDR_ANY; // 自分のパソコンのIPが複数あっても全部のIPで接続を受け付ける
addr.sin_port = htons(port); // ポート番号を設定
// ソケットにIP及びポート番号を設定
if (bind(s, (struct sockaddr*)&addr, sizeof(addr)) < 0) { perror("bind"); close(s); return 1; }
// 設定したソケットで接続要求を待ち受ける
if (listen(s, BACKLOG) < 0) { perror("listen"); close(s); return 1; }
printf("Listening on port %d\n", port);
while (1) { // 無限ループで常に接続を待つ
struct sockaddr_in cli;
socklen_t clilen = sizeof(cli);
int c = accept(s, (struct sockaddr*)&cli, &clilen); // クライアントが接続してきたら 新しいソケット c を作る
if (c < 0) { perror("accept"); continue; }
char ipstr[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &cli.sin_addr, ipstr, sizeof(ipstr));
printf("Connection from %s:%d\n", ipstr, ntohs(cli.sin_port));
// 子プロセスを作ってクライアントを処理
if (fork() == 0) {
close(s); // 親ソケットsは子で不要なので閉じる
handle_client(c); // コマンドの送受信
close(c);
_exit(0);
} else {
// parent
close(c);
}
}
close(s);
return 0;
}
コードの説明
Windows(操作側)での流れ
-
Winsock初期化
WSAStartup()でTCP/IPライブラリを使えるようにする。 -
接続先の情報解決
getaddrinfo()でIP+ポート → struct addrinfoに変換。 -
ソケット生成・接続
socket()でソケット作成
connect()でUbuntuサーバに接続 -
コマンド送信ループ
標準入力からコマンドを取得
長さを先に送信(4バイト、ネットワークバイト順)
実際のコマンドを送信
サーバから返ってくる出力の長さを受信
出力を受信して画面に表示 -
終了処理
長さ0のデータを送信
ソケットを閉じる、WSACleanup()で後片付け
Ubuntu(被操作側)での流れ
-
ソケット生成・バインド・待機
socket(),bind(),listen()
クライアントが来るのを待つ(accept()) -
クライアント接続処理
-
子プロセス(
fork())でコマンド処理
長さを受け取る(4バイト)
コマンド本体を受け取る
popen()でコマンド実行
出力をまとめて送信(長さ+内容) -
接続終了
長さ0の送信でクライアントに終了を通知
子プロセス終了


