452 lines
15 KiB
C
452 lines
15 KiB
C
/*
|
|
* remchild — proof-of-concept remote child client.
|
|
*
|
|
* Self-contained Linux client: no runtime dependencies beyond libc. It finds
|
|
* the server, identifies itself, sends heartbeats on the documented schedule,
|
|
* and answers ping tests. Audio, STT, and video come later; see plan.md.
|
|
*
|
|
* Build: cc -O2 -o remchild remchild/client/main.c
|
|
* Run: ./remchild --server 192.168.1.10:8799
|
|
*/
|
|
|
|
#include <arpa/inet.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <ifaddrs.h>
|
|
#include <net/if.h>
|
|
#include <netinet/in.h>
|
|
#include <netinet/tcp.h>
|
|
#include <poll.h>
|
|
#include <signal.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/socket.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
|
|
#if defined(__linux__)
|
|
#include <netpacket/packet.h>
|
|
#endif
|
|
|
|
#include "words.h"
|
|
|
|
#define DISCOVERY_PORT 8798
|
|
#define DISCOVERY_WINDOW_MS 5000
|
|
#define DISCOVERY_MAX 8
|
|
|
|
#define HEARTBEAT_FAST_MS 10000
|
|
#define HEARTBEAT_MID_MS 30000
|
|
#define HEARTBEAT_SLOW_MS 300000
|
|
#define FAST_UNTIL_MS 60000
|
|
#define MID_UNTIL_MS 360000
|
|
#define LINE_MAX 4096
|
|
|
|
static long heartbeat_interval(long elapsed_ms) {
|
|
if (elapsed_ms < FAST_UNTIL_MS) return HEARTBEAT_FAST_MS;
|
|
if (elapsed_ms < MID_UNTIL_MS) return HEARTBEAT_MID_MS;
|
|
return HEARTBEAT_SLOW_MS;
|
|
}
|
|
|
|
static long now_ms(void) {
|
|
struct timespec ts;
|
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
|
return (long)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
|
|
}
|
|
|
|
/* FNV-1a over the machine bytes: stable across restarts. */
|
|
static unsigned long long fnv1a(const unsigned char *data, size_t len) {
|
|
unsigned long long hash = 1469598103934665603ULL;
|
|
for (size_t i = 0; i < len; i++) {
|
|
hash ^= data[i];
|
|
hash *= 1099511628211ULL;
|
|
}
|
|
return hash;
|
|
}
|
|
|
|
/* First non-loopback MAC. Returns 0 when no usable interface exists. */
|
|
static int machine_mac(unsigned char *out6) {
|
|
struct ifaddrs *list = NULL;
|
|
int found = 0;
|
|
|
|
if (getifaddrs(&list) != 0) return 0;
|
|
|
|
for (struct ifaddrs *it = list; it && !found; it = it->ifa_next) {
|
|
#if defined(__linux__)
|
|
if (!it->ifa_addr || it->ifa_addr->sa_family != AF_PACKET) continue;
|
|
if (it->ifa_flags & IFF_LOOPBACK) continue;
|
|
|
|
struct sockaddr_ll *addr = (struct sockaddr_ll *)it->ifa_addr;
|
|
if (addr->sll_halen != 6) continue;
|
|
|
|
int all_zero = 1;
|
|
for (int i = 0; i < 6; i++) {
|
|
out6[i] = addr->sll_addr[i];
|
|
if (out6[i]) all_zero = 0;
|
|
}
|
|
if (!all_zero) found = 1;
|
|
#endif
|
|
}
|
|
|
|
freeifaddrs(list);
|
|
return found;
|
|
}
|
|
|
|
/* Fallback identity: /etc/machine-id. */
|
|
static int machine_id(unsigned char *out, size_t cap) {
|
|
FILE *file = fopen("/etc/machine-id", "r");
|
|
if (!file) return 0;
|
|
|
|
size_t len = fread(out, 1, cap, file);
|
|
fclose(file);
|
|
|
|
while (len && (out[len - 1] == '\n' || out[len - 1] == '\r')) len--;
|
|
return (int)len;
|
|
}
|
|
|
|
/* Two words from the two lists. Same word on both sides bumps the second. */
|
|
static void name_for_id(const unsigned char *id, size_t len, char *out, size_t cap) {
|
|
unsigned long long hash = fnv1a(id, len);
|
|
int first = (int)(hash % REMCHILD_WORD_COUNT);
|
|
int second = (int)((hash / REMCHILD_WORD_COUNT) % REMCHILD_WORD_COUNT);
|
|
|
|
if (second == first) second = (second + 1) % REMCHILD_WORD_COUNT;
|
|
snprintf(out, cap, "%s %s", REMCHILD_FIRST[first], REMCHILD_SECOND[second]);
|
|
}
|
|
|
|
struct Discovered {
|
|
char ip[64];
|
|
char name[64];
|
|
char id[64];
|
|
int port;
|
|
};
|
|
|
|
/*
|
|
* Ask the LAN who is there. Probes go out as broadcast and to loopback; a
|
|
* server answers with its beacon, and the address comes from the reply.
|
|
* Waits up to DISCOVERY_WINDOW_MS. Returns the number of servers found.
|
|
*/
|
|
static int discover(struct Discovered *found, int cap) {
|
|
int fd = socket(AF_INET, SOCK_DGRAM, 0);
|
|
if (fd < 0) return 0;
|
|
|
|
int one = 1;
|
|
setsockopt(fd, SOL_SOCKET, SO_BROADCAST, &one, sizeof(one));
|
|
|
|
struct sockaddr_in local = {0};
|
|
local.sin_family = AF_INET;
|
|
local.sin_addr.s_addr = htonl(INADDR_ANY);
|
|
if (bind(fd, (struct sockaddr *)&local, sizeof(local)) != 0) {
|
|
close(fd);
|
|
return 0;
|
|
}
|
|
|
|
struct sockaddr_in broadcast = {0};
|
|
broadcast.sin_family = AF_INET;
|
|
broadcast.sin_port = htons(DISCOVERY_PORT);
|
|
broadcast.sin_addr.s_addr = htonl(INADDR_BROADCAST);
|
|
|
|
struct sockaddr_in loopback = broadcast;
|
|
loopback.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
|
|
|
const char *probe = "{\"type\":\"veskslop-remchild-probe\",\"version\":\"0.1\"}";
|
|
long deadline = now_ms() + DISCOVERY_WINDOW_MS;
|
|
long next_probe = 0;
|
|
int count = 0;
|
|
|
|
while (now_ms() < deadline) {
|
|
if (now_ms() >= next_probe) {
|
|
sendto(fd, probe, strlen(probe), 0, (struct sockaddr *)&broadcast, sizeof(broadcast));
|
|
sendto(fd, probe, strlen(probe), 0, (struct sockaddr *)&loopback, sizeof(loopback));
|
|
next_probe = now_ms() + 1000;
|
|
}
|
|
|
|
struct pollfd pfd = {fd, POLLIN, 0};
|
|
if (poll(&pfd, 1, 200) <= 0) continue;
|
|
if (!(pfd.revents & POLLIN)) continue;
|
|
|
|
char buffer[1024];
|
|
struct sockaddr_in from;
|
|
socklen_t from_len = sizeof(from);
|
|
ssize_t got = recvfrom(fd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&from, &from_len);
|
|
if (got <= 0) continue;
|
|
buffer[got] = 0;
|
|
|
|
if (!strstr(buffer, "veskslop-remchild") || strstr(buffer, "probe")) continue;
|
|
|
|
char ip[64];
|
|
if (!inet_ntop(AF_INET, &from.sin_addr, ip, sizeof(ip))) continue;
|
|
|
|
int port = 0;
|
|
char name[64] = "veskslop";
|
|
char id[64] = "";
|
|
char *match = strstr(buffer, "\"port\":");
|
|
if (match) port = atoi(match + 7);
|
|
match = strstr(buffer, "\"name\":\"");
|
|
if (match) sscanf(match + 8, "%63[^\"]", name);
|
|
match = strstr(buffer, "\"id\":\"");
|
|
if (match) sscanf(match + 6, "%63[^\"]", id);
|
|
if (port <= 0) continue;
|
|
|
|
// One server can answer from both loopback and its LAN address; the
|
|
// beacon id identifies it.
|
|
int duplicate = 0;
|
|
for (int i = 0; i < count; i++) {
|
|
if (id[0] && found[i].id[0] && !strcmp(found[i].id, id)) duplicate = 1;
|
|
else if (!id[0] && !found[i].id[0] && !strcmp(found[i].name, name) && found[i].port == port) duplicate = 1;
|
|
}
|
|
if (duplicate || count >= cap) continue;
|
|
|
|
snprintf(found[count].ip, sizeof(found[count].ip), "%s", ip);
|
|
snprintf(found[count].name, sizeof(found[count].name), "%s", name);
|
|
snprintf(found[count].id, sizeof(found[count].id), "%s", id);
|
|
found[count].port = port;
|
|
count++;
|
|
}
|
|
|
|
close(fd);
|
|
return count;
|
|
}
|
|
|
|
static int connect_to(const char *host, int port) {
|
|
struct sockaddr_in addr = {0};
|
|
addr.sin_family = AF_INET;
|
|
addr.sin_port = htons((unsigned short)port);
|
|
|
|
if (inet_pton(AF_INET, host, &addr.sin_addr) != 1) return -1;
|
|
|
|
int fd = socket(AF_INET, SOCK_STREAM, 0);
|
|
if (fd < 0) return -1;
|
|
|
|
if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) != 0) {
|
|
close(fd);
|
|
return -1;
|
|
}
|
|
|
|
int one = 1;
|
|
setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one));
|
|
return fd;
|
|
}
|
|
|
|
static void send_line(int fd, const char *line) {
|
|
char frame[1024];
|
|
int len = snprintf(frame, sizeof(frame), "%s\n", line);
|
|
|
|
if (len <= 0 || send(fd, frame, (size_t)len, MSG_NOSIGNAL) != len) {
|
|
fprintf(stderr, "[remchild] send failed: %s\n", strerror(errno));
|
|
}
|
|
}
|
|
|
|
int main(int argc, char **argv) {
|
|
char host[128] = "";
|
|
int port = 8799;
|
|
int once = 0;
|
|
|
|
for (int i = 1; i < argc; i++) {
|
|
if (!strcmp(argv[i], "--server") && i + 1 < argc) {
|
|
char *value = argv[++i];
|
|
char *colon = strrchr(value, ':');
|
|
if (colon) {
|
|
*colon = 0;
|
|
port = atoi(colon + 1);
|
|
}
|
|
snprintf(host, sizeof(host), "%s", value);
|
|
} else if (!strcmp(argv[i], "--once")) {
|
|
once = 1;
|
|
} else if (!strcmp(argv[i], "--help")) {
|
|
printf("usage: remchild [--server host:port] [--once]\n");
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
signal(SIGPIPE, SIG_IGN);
|
|
int interactive = isatty(STDIN_FILENO);
|
|
|
|
if (!host[0]) {
|
|
struct Discovered found[DISCOVERY_MAX];
|
|
printf("[remchild] looking for a server on the LAN for %d seconds...\n", DISCOVERY_WINDOW_MS / 1000);
|
|
fflush(stdout);
|
|
int count = discover(found, DISCOVERY_MAX);
|
|
|
|
if (count == 1) {
|
|
printf("[remchild] found %s at %s:%d\n", found[0].name, found[0].ip, found[0].port);
|
|
snprintf(host, sizeof(host), "%s", found[0].ip);
|
|
port = found[0].port;
|
|
} else if (count > 1) {
|
|
printf("[remchild] found %d servers:\n", count);
|
|
for (int i = 0; i < count; i++) {
|
|
printf(" %d) %s at %s:%d\n", i + 1, found[i].name, found[i].ip, found[i].port);
|
|
}
|
|
printf("pick one [1]: ");
|
|
fflush(stdout);
|
|
char pick_line[32];
|
|
int pick = 1;
|
|
if (fgets(pick_line, sizeof(pick_line), stdin)) pick = atoi(pick_line);
|
|
if (pick < 1 || pick > count) pick = 1;
|
|
printf("[remchild] using %s at %s:%d\n", found[pick - 1].name, found[pick - 1].ip, found[pick - 1].port);
|
|
snprintf(host, sizeof(host), "%s", found[pick - 1].ip);
|
|
port = found[pick - 1].port;
|
|
} else {
|
|
printf("[remchild] no server answered discovery.\n");
|
|
printf("Server address (host:port): ");
|
|
fflush(stdout);
|
|
char line[128];
|
|
if (!fgets(line, sizeof(line), stdin)) return 1;
|
|
line[strcspn(line, "\n")] = 0;
|
|
char *colon = strrchr(line, ':');
|
|
if (colon) {
|
|
*colon = 0;
|
|
port = atoi(colon + 1);
|
|
}
|
|
snprintf(host, sizeof(host), "%s", line);
|
|
}
|
|
}
|
|
|
|
unsigned char id[32];
|
|
size_t id_len = 0;
|
|
unsigned char mac[6];
|
|
|
|
if (machine_mac(mac)) {
|
|
memcpy(id, mac, 6);
|
|
id_len = 6;
|
|
} else {
|
|
int got = machine_id(id, sizeof(id));
|
|
if (got <= 0) {
|
|
fprintf(stderr, "[remchild] no MAC and no /etc/machine-id; cannot identify this host\n");
|
|
return 1;
|
|
}
|
|
id_len = (size_t)got;
|
|
}
|
|
|
|
char name[64];
|
|
name_for_id(id, id_len, name, sizeof(name));
|
|
|
|
char id_hex[17];
|
|
snprintf(id_hex, sizeof(id_hex), "%016llx", fnv1a(id, id_len));
|
|
|
|
printf("[remchild] identity %s (name: %s)\n", id_len == 6 ? "MAC" : "machine-id", name);
|
|
printf("[remchild] connecting to %s:%d\n", host, port);
|
|
fflush(stdout);
|
|
|
|
int fd = connect_to(host, port);
|
|
if (fd < 0) {
|
|
if (errno == ECONNREFUSED) {
|
|
fprintf(stderr, "[remchild] could not reach %s:%d: no server is listening there.\n", host, port);
|
|
} else {
|
|
fprintf(stderr,
|
|
"[remchild] could not reach %s:%d (%s).\n"
|
|
"[remchild] A firewall may block TCP %d on the server or on this host.\n",
|
|
host, port, strerror(errno), port);
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
printf("[remchild] connected. type p + Enter for a ping test, q + Enter to quit.\n");
|
|
|
|
char hello[512];
|
|
snprintf(hello, sizeof(hello),
|
|
"{\"type\":\"hello\",\"id\":\"%s\",\"name\":\"%s\",\"platform\":\"linux\","
|
|
"\"version\":\"0.1\",\"heartbeatMs\":%ld}",
|
|
id_hex, name, heartbeat_interval(0));
|
|
send_line(fd, hello);
|
|
|
|
long started = now_ms();
|
|
long last_beat = started;
|
|
int ping_seq = 0;
|
|
long ping_sent_at = 0;
|
|
char buffer[LINE_MAX];
|
|
size_t used = 0;
|
|
int answered_ping = 0;
|
|
|
|
if (once) {
|
|
ping_seq = 1;
|
|
ping_sent_at = now_ms();
|
|
send_line(fd, "{\"type\":\"ping\",\"seq\":1}");
|
|
}
|
|
|
|
for (;;) {
|
|
long now = now_ms();
|
|
long interval = heartbeat_interval(now - started);
|
|
|
|
if (now - last_beat >= interval) {
|
|
char beat[128];
|
|
snprintf(beat, sizeof(beat), "{\"type\":\"heartbeat\",\"ts\":%ld,\"heartbeatMs\":%ld}",
|
|
(long)time(NULL), interval);
|
|
send_line(fd, beat);
|
|
last_beat = now;
|
|
}
|
|
|
|
struct pollfd fds[2] = {{fd, POLLIN, 0}, {STDIN_FILENO, interactive ? POLLIN : 0, 0}};
|
|
int ready = poll(fds, interactive ? 2 : 1, 500);
|
|
if (ready < 0 && errno != EINTR) break;
|
|
|
|
if (ready > 0 && (fds[0].revents & (POLLERR | POLLHUP))) {
|
|
printf("[remchild] server closed the connection\n");
|
|
break;
|
|
}
|
|
|
|
if (ready > 0 && (fds[0].revents & POLLIN)) {
|
|
ssize_t got = recv(fd, buffer + used, sizeof(buffer) - used - 1, 0);
|
|
if (got <= 0) {
|
|
printf("[remchild] server closed the connection\n");
|
|
break;
|
|
}
|
|
used += (size_t)got;
|
|
buffer[used] = 0;
|
|
|
|
char *line = buffer;
|
|
char *newline;
|
|
while ((newline = strchr(line, '\n'))) {
|
|
*newline = 0;
|
|
if (strstr(line, "\"type\":\"welcome\"")) {
|
|
printf("[remchild] welcome: %s\n", line);
|
|
} else if (strstr(line, "\"type\":\"pong\"")) {
|
|
if (ping_sent_at) printf("[remchild] pong seq=%d rtt=%ldms\n", ping_seq, now_ms() - ping_sent_at);
|
|
answered_ping = 1;
|
|
} else if (strstr(line, "\"type\":\"notice\"")) {
|
|
int missed = 0;
|
|
char *value = strstr(line, "\"missed\":");
|
|
if (value) missed = atoi(value + 9);
|
|
printf("[remchild] notice: the server missed %d heartbeat(s); the connection may be unstable.\n",
|
|
missed);
|
|
} else if (strstr(line, "\"type\":\"ping\"")) {
|
|
int seq = 0;
|
|
char *value = strstr(line, "\"seq\":");
|
|
if (value) seq = atoi(value + 6);
|
|
char pong[64];
|
|
snprintf(pong, sizeof(pong), "{\"type\":\"pong\",\"seq\":%d}", seq);
|
|
send_line(fd, pong);
|
|
} else {
|
|
printf("[remchild] server: %s\n", line);
|
|
}
|
|
line = newline + 1;
|
|
}
|
|
|
|
used = strlen(line);
|
|
memmove(buffer, line, used + 1);
|
|
}
|
|
|
|
if (ready > 0 && (fds[1].revents & POLLIN)) {
|
|
char input[128];
|
|
if (!fgets(input, sizeof(input), stdin)) break;
|
|
input[strcspn(input, "\n")] = 0;
|
|
|
|
if (!strcmp(input, "q")) break;
|
|
if (!strcmp(input, "p")) {
|
|
ping_seq++;
|
|
ping_sent_at = now_ms();
|
|
char ping[64];
|
|
snprintf(ping, sizeof(ping), "{\"type\":\"ping\",\"seq\":%d}", ping_seq);
|
|
send_line(fd, ping);
|
|
} else if (strcmp(input, "")) {
|
|
printf("[remchild] commands: p = ping test, q = quit\n");
|
|
}
|
|
}
|
|
|
|
if (once && (answered_ping || now - started > 5000)) break;
|
|
}
|
|
|
|
close(fd);
|
|
return 0;
|
|
}
|