From 2db62a2a2731742b39194c71e7f6f0399c09a0aa Mon Sep 17 00:00:00 2001 From: Compot Developer Date: Tue, 13 Oct 2026 22:49:02 +0000 Subject: [PATCH] Review: vtun-core (snapshot of main for code review) --- include/vtesse/version.h | 7 + include/vtesse/vtun_client.h | 55 +++ include/vtesse/vtun_proto.h | 73 ++++ tests/test_client.c | 501 ++++++++++++++++++++++++++ tests/test_proto.c | 173 +++++++++ tests/unit.h | 49 +++ vtun/vtun_client.c | 672 +++++++++++++++++++++++++++++++++++ vtun/vtun_proto.c | 193 ++++++++++ 8 files changed, 1723 insertions(+) create mode 100644 include/vtesse/version.h create mode 100644 include/vtesse/vtun_client.h create mode 100644 include/vtesse/vtun_proto.h create mode 100644 tests/test_client.c create mode 100644 tests/test_proto.c create mode 100644 tests/unit.h create mode 100644 vtun/vtun_client.c create mode 100644 vtun/vtun_proto.c diff --git a/include/vtesse/version.h b/include/vtesse/version.h new file mode 100644 index 0000000..e839c7e --- /dev/null +++ b/include/vtesse/version.h @@ -0,0 +1,7 @@ +#ifndef VTESSE_VERSION_H +#define VTESSE_VERSION_H + +/* MAJOR.MM. Bumping it in a push to main publishes release vMAJOR.MM (CI). */ +#define VTESSE_VERSION "1.00" + +#endif diff --git a/include/vtesse/vtun_client.h b/include/vtesse/vtun_client.h new file mode 100644 index 0000000..24503ac --- /dev/null +++ b/include/vtesse/vtun_client.h @@ -0,0 +1,55 @@ +/* + * VTun client (TCP transport, type tun / ether, no compression/encryption). + * + * Non-blocking, single-threaded, no global state. Exposes tunnel_ops_t. + * Name resolution uses getaddrinfo() and may block briefly at connect time. + */ +#ifndef VTESSE_VTUN_CLIENT_H +#define VTESSE_VTUN_CLIENT_H + +#include + +#include "netcore/tunnel.h" +#include "vtesse/vtun_proto.h" + +enum vt_auth { + VT_AUTH_XOR = 0 /* servers built without OpenSSL */ +}; + +typedef struct vt_cfg { + const char *host; + uint16_t port; + const char *session; + const char *password; + int auth; /* enum vt_auth */ + + /* Optional tuning; 0 selects the default. */ + int handshake_timeout_ms; /* per step, default 15000 (CHAL step >= 10000) */ + int ka_interval_ms; /* own ECHO_REQ period, default 10000 */ + int ka_count; /* dead after ka_count * interval silence, default 5 */ + int data_delay_ms; /* OK FLAGS -> TUNNEL_EV_UP delay, default 500 */ + int reconnect_min_ms; /* default 1000 */ + int reconnect_max_ms; /* default 30000 */ + int out_buffer_size; /* default 65536 */ + int no_reconnect; /* 1: report FAILED instead of retrying */ +} vt_cfg_t; + +typedef struct vt vt_t; + +/* Copies the configuration. Returns NULL and sets *err on invalid config / OOM. */ +vt_t *vt_create(const vt_cfg_t *cfg, int *err); +void vt_destroy(vt_t *vt); + +/* Start connecting (first attempt happens immediately). */ +int vt_start(vt_t *vt); + +/* Tunnel interface. */ +const tunnel_ops_t *vt_tunnel_ops(void); +tunnel_t vt_tunnel(vt_t *vt); + +/* Diagnostics. */ +const char *vt_greeting(const vt_t *vt); /* last server greeting, "" if none */ +const vt_flags_t *vt_session_flags(const vt_t *vt); /* flags of the last session */ +int vt_is_up(const vt_t *vt); + +#endif diff --git a/include/vtesse/vtun_proto.h b/include/vtesse/vtun_proto.h new file mode 100644 index 0000000..f7db928 --- /dev/null +++ b/include/vtesse/vtun_proto.h @@ -0,0 +1,73 @@ +/* + * VTun wire format helpers (verified against VTun 3.0.2). + * Pure functions, no I/O, explicit byte order. + */ +#ifndef VTESSE_VTUN_PROTO_H +#define VTESSE_VTUN_PROTO_H + +#include +#include + +#define VT_MESG_SIZE 50 /* every control message, NUL padded */ +#define VT_CHAL_SIZE 16 +#define VT_CHAL_TEXT_SIZE (VT_CHAL_SIZE * 2 + 3) /* "<" + 32 + ">" + NUL */ +#define VT_FRAME_SIZE 2048 /* max payload we send */ +#define VT_FRAME_OVERHEAD 100 /* receivers accept up to SIZE + OVERHEAD */ +#define VT_FRAME_MAX_RX (VT_FRAME_SIZE + VT_FRAME_OVERHEAD) +#define VT_FSIZE_MASK 0x0fff +#define VT_FLAGS_MASK 0xf000 +#define VT_CONN_CLOSE 0x1000 +#define VT_ECHO_REQ 0x2000 +#define VT_ECHO_REP 0x4000 +#define VT_BAD_FRAME 0x8000 /* never on the wire in 3.0.2 */ +#define VT_SESSION_MAX 42 /* "HOST: " + name + "\n" must fit 49 bytes */ + +/* Session names: 1..42 printable characters, no space, ':', control chars. */ +int vt_session_name_valid(const char *name); + +/* Build "\n" padded with NUL to 50 bytes. -1 if longer than 49. */ +int vt_mesg_build(uint8_t out[VT_MESG_SIZE], const char *prefix, const char *arg); + +/* Challenge text "<" + 32 chars 'a'..'p' + ">" <-> 16 bytes. */ +void vt_chal_encode(const uint8_t chal[VT_CHAL_SIZE], char out[VT_CHAL_TEXT_SIZE]); +/* Finds '<' in msg; requires exactly 32 chars 'a'..'p' followed by '>'. 0 on success. */ +int vt_chal_decode(const char *msg, uint8_t chal[VT_CHAL_SIZE]); + +/* XOR challenge response (servers built without OpenSSL). pwd must be non-empty. */ +void vt_chal_xor(uint8_t chal[VT_CHAL_SIZE], const char *pwd); + +typedef struct vt_flags { + char transport; /* 'T' or 'U', 0 if absent */ + char type; /* 'u' tun, 'e' ether, 't' tty, 'p' pipe, 0 if absent */ + int keepalive; + int shaper; /* S, 0 if absent */ + int zlib; /* C present */ + int lzo; /* L present */ + int zlevel; + int encrypt; /* E or E present */ + int cipher; /* 0 = legacy */ +} vt_flags_t; + +/* Parse "OK FLAGS: <...>". Same grammar as VTun cf2bf: unknown characters, + * a missing number after C/L/S or a missing '>' are errors. 0 on success. */ +int vt_flags_parse(const char *msg, vt_flags_t *f); + +/* Policy check: TCP, type tun or ether, no compression, no encryption. + * Returns TUNNEL_OK or a TUNNEL_ERR_* code. */ +int vt_flags_check(const vt_flags_t *f); + +/* Format flags back as "<...>" in VTun server order (for logs/tests). */ +void vt_flags_format(const vt_flags_t *f, char *out, size_t size); + +static inline void vt_hdr_put(uint8_t b[2], uint16_t v) +{ + b[0] = (uint8_t)(v >> 8); + b[1] = (uint8_t)v; +} + +static inline uint16_t vt_hdr_get(const uint8_t b[2]) +{ + return (uint16_t)((b[0] << 8) | b[1]); +} + +#endif diff --git a/tests/test_client.c b/tests/test_client.c new file mode 100644 index 0000000..e8861c6 --- /dev/null +++ b/tests/test_client.c @@ -0,0 +1,501 @@ +/* + * vtun_client scenarios against a scripted VTun server running in a child + * process on a loopback socket. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "netcore/loop.h" +#include "unit.h" +#include "vtesse/vtun_client.h" + +#define PASSWORD "testpassword-0123456789" +#define SESSION "02BE0C000001" + +static const uint8_t server_chal[VT_CHAL_SIZE] = { + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, + 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff +}; + +/* ---------- scripted server (child process, blocking I/O) ---------- */ + +static void die(const char *what) +{ + fprintf(stderr, "server: %s\n", what); + _exit(1); +} + +static void sleep_ms(int ms) +{ + struct timespec ts; + + ts.tv_sec = ms / 1000; + ts.tv_nsec = (long)(ms % 1000) * 1000000L; + nanosleep(&ts, NULL); +} + +static void srv_write(int fd, const void *p, size_t n) +{ + if (write(fd, p, n) != (ssize_t)n) + die("write"); +} + +static void srv_read(int fd, void *p, size_t n) +{ + size_t got = 0; + + while (got < n) { + ssize_t r = read(fd, (uint8_t *)p + got, n - got); + if (r <= 0) + die("read"); + got += (size_t)r; + } +} + +static void srv_mesg(int fd, const char *text) +{ + uint8_t m[VT_MESG_SIZE]; + + memset(m, 0, sizeof(m)); + memcpy(m, text, strlen(text)); + srv_write(fd, m, sizeof(m)); +} + +static void srv_flag(int fd, uint16_t flag) +{ + uint8_t h[2]; + + vt_hdr_put(h, flag); + srv_write(fd, h, 2); +} + +/* Full handshake; final_reply is sent after a correct CHAL response. */ +static void srv_handshake(int fd, const char *final_reply) +{ + uint8_t m[VT_MESG_SIZE], expect[VT_CHAL_SIZE], got[VT_CHAL_SIZE]; + char text[VT_CHAL_TEXT_SIZE], line[VT_MESG_SIZE + 16]; + int i; + + srv_mesg(fd, "VTUN server ver 3.X 10/13/2026\n"); + srv_read(fd, m, sizeof(m)); + if (memcmp(m, "HOST: " SESSION "\n", 19) != 0) + die("bad HOST"); + for (i = 19; i < VT_MESG_SIZE; i++) + if (m[i]) + die("HOST not NUL padded"); + vt_chal_encode(server_chal, text); + snprintf(line, sizeof(line), "OK CHAL: %s\n", text); + srv_mesg(fd, line); + srv_read(fd, m, sizeof(m)); + m[VT_MESG_SIZE - 1] = 0; + if (strncmp((char *)m, "CHAL: ", 6) != 0 || vt_chal_decode((char *)m, got) != 0) + die("bad CHAL"); + memcpy(expect, server_chal, sizeof(expect)); + vt_chal_xor(expect, PASSWORD); + if (memcmp(expect, got, sizeof(got)) != 0) + die("wrong XOR response"); + srv_mesg(fd, final_reply); +} + +/* Read frames until a data frame arrives; flag frames are recorded. */ +static size_t srv_read_data(int fd, uint8_t *buf, size_t cap, int *seen_req, int *seen_rep) +{ + for (;;) { + uint8_t h[2]; + uint16_t hdr, len; + + srv_read(fd, h, 2); + hdr = vt_hdr_get(h); + if (hdr & VT_FLAGS_MASK) { + if (hdr & VT_ECHO_REQ) + (*seen_req)++; + if (hdr & VT_ECHO_REP) + (*seen_rep)++; + continue; + } + len = hdr & VT_FSIZE_MASK; + if (len > cap) + die("frame too large"); + srv_read(fd, buf, len); + return len; + } +} + +typedef void (*script_fn)(int listen_fd); + +static pid_t start_server(script_fn script, uint16_t *port) +{ + struct sockaddr_in a; + socklen_t l = sizeof(a); + int one = 1, lfd = socket(AF_INET, SOCK_STREAM, 0); + pid_t pid; + + memset(&a, 0, sizeof(a)); + a.sin_family = AF_INET; + a.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)); + if (bind(lfd, (struct sockaddr *)&a, sizeof(a)) < 0 || listen(lfd, 4) < 0 || + getsockname(lfd, (struct sockaddr *)&a, &l) < 0) { + perror("listen"); + exit(1); + } + *port = ntohs(a.sin_port); + pid = fork(); + if (pid == 0) { + alarm(10); + script(lfd); + _exit(0); + } + close(lfd); + return pid; +} + +static int server_status(pid_t pid) +{ + int st = 0; + + waitpid(pid, &st, 0); + return WIFEXITED(st) ? WEXITSTATUS(st) : 128; +} + +/* ---------- client side ---------- */ + +typedef struct rec { + int events[32]; + int errs[32]; + int nev; + uint8_t rx[8][64]; + size_t rxlen[8]; + int nrx; +} rec_t; + +static void on_event(void *user, int ev, int err) +{ + rec_t *r = user; + + if (r->nev < 32) { + r->events[r->nev] = ev; + r->errs[r->nev++] = err; + } +} + +static void on_rx(void *user, const uint8_t *p, size_t n) +{ + rec_t *r = user; + + if (r->nrx < 8 && n <= 64) { + memcpy(r->rx[r->nrx], p, n); + r->rxlen[r->nrx++] = n; + } +} + +static int last_event(const rec_t *r) +{ + return r->nev ? r->events[r->nev - 1] : 0; +} + +static vt_t *make_client(uint16_t port, rec_t *r, int no_reconnect) +{ + vt_cfg_t cfg; + vt_t *vt; + int err; + tunnel_t t; + + memset(&cfg, 0, sizeof(cfg)); + cfg.host = "127.0.0.1"; + cfg.port = port; + cfg.session = SESSION; + cfg.password = PASSWORD; + cfg.auth = VT_AUTH_XOR; + cfg.handshake_timeout_ms = 400; + cfg.ka_interval_ms = 200; + cfg.ka_count = 3; + cfg.data_delay_ms = 200; + cfg.reconnect_min_ms = 100; + cfg.no_reconnect = no_reconnect; + vt = vt_create(&cfg, &err); + if (!vt) { + fprintf(stderr, "vt_create: %s\n", tunnel_strerror(err)); + exit(1); + } + memset(r, 0, sizeof(*r)); + t = vt_tunnel(vt); + t.ops->set_event_cb(vt, on_event, r); + t.ops->set_rx_cb(vt, on_rx, r); + return vt; +} + +/* Run the loop until pred() is true or timeout_ms passes. */ +static int run_until(vt_t *vt, int (*pred)(vt_t *, rec_t *), rec_t *r, int timeout_ms) +{ + nc_loop_t loop; + tunnel_t t = vt_tunnel(vt); + nc_source_t s = nc_source_from_tunnel(&t); + uint64_t end = nc_now_ms() + (uint64_t)timeout_ms; + + nc_loop_init(&loop); + nc_loop_add(&loop, &s); + while (nc_now_ms() < end) { + if (pred(vt, r)) + return 1; + nc_loop_run_once(&loop, 20); + } + return pred(vt, r); +} + +static int pred_up(vt_t *vt, rec_t *r) { (void)r; return vt_is_up(vt); } +static int pred_failed(vt_t *vt, rec_t *r) { (void)vt; return last_event(r) == TUNNEL_EV_FAILED; } +static int pred_rx2(vt_t *vt, rec_t *r) { (void)vt; return r->nrx >= 2; } +static int pred_ups2(vt_t *vt, rec_t *r) +{ + int i, n = 0; + + (void)vt; + for (i = 0; i < r->nev; i++) + n += r->events[i] == TUNNEL_EV_UP; + return n >= 2; +} + +/* ---------- scenarios ---------- */ + +static void script_happy(int lfd) +{ + int fd = accept(lfd, NULL, NULL), req = 0, rep = 0; + uint8_t buf[64], big[3000]; + size_t n; + + srv_handshake(fd, "OK FLAGS: \n"); + srv_flag(fd, VT_ECHO_REQ); + /* data frame split across two writes */ + srv_write(fd, "\x00\x05", 2); + sleep_ms(30); + srv_write(fd, "hello", 5); + /* oversized frame (3000 > 2148) must be skipped, framing preserved */ + memset(big, 0xab, sizeof(big)); + srv_write(fd, "\x0b\xb8", 2); + srv_write(fd, big, sizeof(big)); + srv_write(fd, "\x00\x05" "after", 7); + /* client data frame after the data delay */ + n = srv_read_data(fd, buf, sizeof(buf), &req, &rep); + if (n != 4 || memcmp(buf, "ping", 4) != 0) + die("bad client frame"); + if (req < 1 || rep < 1) + die("missing ECHO_REQ/ECHO_REP from client"); + srv_flag(fd, VT_CONN_CLOSE); + sleep_ms(100); + close(fd); +} + +static void test_happy_path(void) +{ + uint16_t port; + pid_t pid = start_server(script_happy, &port); + rec_t r; + vt_t *vt = make_client(port, &r, 1); + tunnel_t t = vt_tunnel(vt); + uint64_t t0; + + t0 = nc_now_ms(); + vt_start(vt); + CHECK_EQ_INT(t.ops->send(vt, (const uint8_t *)"early", 5), -1); /* not up */ + /* frames are delivered while UP is still held back by the data delay */ + CHECK(run_until(vt, pred_rx2, &r, 2000)); + CHECK(!vt_is_up(vt)); + CHECK_EQ_INT(t.ops->send(vt, (const uint8_t *)"early", 5), -1); + CHECK_EQ_INT(r.nrx, 2); + CHECK(r.rxlen[0] == 5 && memcmp(r.rx[0], "hello", 5) == 0); + CHECK(r.rxlen[1] == 5 && memcmp(r.rx[1], "after", 5) == 0); + + CHECK(run_until(vt, pred_up, &r, 2000)); + CHECK(nc_now_ms() - t0 >= 190); /* data delay */ + CHECK_EQ_INT(t.ops->link_type(vt), TUNNEL_LINK_L2); + CHECK_EQ_STR(vt_greeting(vt), "VTUN server ver 3.X 10/13/2026"); + CHECK_EQ_INT(vt_session_flags(vt)->type, 'e'); + CHECK_EQ_INT(t.ops->send(vt, (const uint8_t *)"ping", 4), 0); + CHECK(run_until(vt, pred_failed, &r, 2000)); + CHECK_EQ_INT(r.errs[r.nev - 1], TUNNEL_ERR_PEER_CLOSED); + CHECK_EQ_INT(r.events[0], TUNNEL_EV_CONNECTING); + CHECK_EQ_INT(r.events[1], TUNNEL_EV_UP); + CHECK_EQ_INT(server_status(pid), 0); + vt_destroy(vt); +} + +static void script_err(int lfd) +{ + int fd = accept(lfd, NULL, NULL); + + srv_handshake(fd, "ERR\n"); + sleep_ms(100); + close(fd); +} + +static void script_compress(int lfd) +{ + int fd = accept(lfd, NULL, NULL); + + srv_handshake(fd, "OK FLAGS: \n"); + sleep_ms(100); + close(fd); +} + +static void script_bad_greeting(int lfd) +{ + int fd = accept(lfd, NULL, NULL); + + srv_mesg(fd, "SSH-2.0-OpenSSH\n"); + sleep_ms(100); + close(fd); +} + +static void script_silent(int lfd) +{ + int fd = accept(lfd, NULL, NULL); + + sleep_ms(1500); + close(fd); +} + +static void expect_failure(script_fn script, int want_err, int no_reconnect) +{ + uint16_t port; + pid_t pid = start_server(script, &port); + rec_t r; + vt_t *vt = make_client(port, &r, no_reconnect); + + vt_start(vt); + CHECK(run_until(vt, pred_failed, &r, 3000)); + CHECK_EQ_INT(r.errs[r.nev - 1], want_err); + vt_destroy(vt); + kill(pid, SIGTERM); + server_status(pid); +} + +static void test_auth_error_is_final(void) +{ + /* ERR must not be retried even with reconnect enabled */ + expect_failure(script_err, TUNNEL_ERR_AUTH, 0); +} + +static void test_compression_refused(void) +{ + expect_failure(script_compress, TUNNEL_ERR_COMPRESSION, 0); +} + +static void test_bad_greeting(void) +{ + expect_failure(script_bad_greeting, TUNNEL_ERR_PROTOCOL, 0); +} + +static void test_handshake_timeout(void) +{ + expect_failure(script_silent, TUNNEL_ERR_TIMEOUT, 1); +} + +static void script_keepalive_dead(int lfd) +{ + int fd = accept(lfd, NULL, NULL); + + srv_handshake(fd, "OK FLAGS: \n"); + sleep_ms(2000); /* never answer */ + close(fd); +} + +static void test_keepalive_timeout(void) +{ + uint16_t port; + pid_t pid = start_server(script_keepalive_dead, &port); + rec_t r; + vt_t *vt = make_client(port, &r, 1); + uint64_t t0; + + t0 = nc_now_ms(); + vt_start(vt); + CHECK(run_until(vt, pred_up, &r, 2000)); + CHECK_EQ_INT(vt_tunnel(vt).ops->link_type(vt), TUNNEL_LINK_L3); + CHECK(run_until(vt, pred_failed, &r, 3000)); + CHECK_EQ_INT(r.errs[r.nev - 1], TUNNEL_ERR_KEEPALIVE); + CHECK(nc_now_ms() - t0 >= 550); /* 3 x 200 ms of silence after OK FLAGS */ + vt_destroy(vt); + kill(pid, SIGTERM); + server_status(pid); +} + +static void script_reconnect(int lfd) +{ + int fd = accept(lfd, NULL, NULL); + + close(fd); /* first attempt: dropped at once */ + fd = accept(lfd, NULL, NULL); + srv_handshake(fd, "OK FLAGS: \n"); + sleep_ms(300); /* client reports UP after the data delay */ + srv_flag(fd, VT_CONN_CLOSE); /* e.g. taken over by killold */ + fd = accept(lfd, NULL, NULL); + srv_handshake(fd, "OK FLAGS: \n"); + sleep_ms(1000); + close(fd); +} + +static void test_reconnect(void) +{ + uint16_t port; + pid_t pid = start_server(script_reconnect, &port); + rec_t r; + vt_t *vt = make_client(port, &r, 0); + int i, downs = 0; + + vt_start(vt); + CHECK(run_until(vt, pred_ups2, &r, 4000)); + for (i = 0; i < r.nev; i++) { + if (r.events[i] == TUNNEL_EV_DOWN) { + downs++; + CHECK_EQ_INT(r.errs[i], TUNNEL_ERR_PEER_CLOSED); + } + CHECK(r.events[i] != TUNNEL_EV_FAILED); + } + CHECK_EQ_INT(downs, 2); + vt_tunnel(vt).ops->close(vt); + vt_destroy(vt); + kill(pid, SIGTERM); + server_status(pid); +} + +static void test_config_validation(void) +{ + vt_cfg_t cfg; + int err; + + memset(&cfg, 0, sizeof(cfg)); + cfg.host = "127.0.0.1"; + cfg.port = 1; + cfg.session = "bad name"; + cfg.password = "x"; + CHECK(vt_create(&cfg, &err) == NULL); + CHECK_EQ_INT(err, TUNNEL_ERR_CONFIG); + cfg.session = SESSION; + cfg.password = ""; + CHECK(vt_create(&cfg, &err) == NULL); + CHECK_EQ_INT(err, TUNNEL_ERR_CONFIG); +} + +int main(void) +{ + signal(SIGPIPE, SIG_IGN); + RUN(test_config_validation); + RUN(test_happy_path); + RUN(test_auth_error_is_final); + RUN(test_compression_refused); + RUN(test_bad_greeting); + RUN(test_handshake_timeout); + RUN(test_keepalive_timeout); + RUN(test_reconnect); + return UNIT_SUMMARY(); +} diff --git a/tests/test_proto.c b/tests/test_proto.c new file mode 100644 index 0000000..098192c --- /dev/null +++ b/tests/test_proto.c @@ -0,0 +1,173 @@ +#include + +#include "netcore/tunnel.h" +#include "unit.h" +#include "vtesse/vtun_proto.h" + +/* Captured from the stock VTun 3.0.2 client on the bench (bench/vectors). */ +#define REF_PASSWORD "c79e01d2e49d80eafd192c043c5142bd5a65b5a9" +#define REF_CHAL "OK CHAL: \n" +#define REF_RESP "" + +static void test_mesg_build(void) +{ + uint8_t m[VT_MESG_SIZE]; + char name43[44]; + int i; + + CHECK_EQ_INT(vt_mesg_build(m, "HOST: ", "02BE0C000001"), 0); + CHECK(memcmp(m, "HOST: 02BE0C000001\n", 19) == 0); + for (i = 19; i < VT_MESG_SIZE; i++) + CHECK_EQ_INT(m[i], 0); + + memset(name43, 'A', 43); + name43[43] = '\0'; + CHECK_EQ_INT(vt_mesg_build(m, "HOST: ", name43), -1); /* 6 + 43 + 1 > 49 */ + name43[42] = '\0'; + CHECK_EQ_INT(vt_mesg_build(m, "HOST: ", name43), 0); /* exactly 49 */ + CHECK_EQ_INT(m[48], '\n'); + CHECK_EQ_INT(m[49], 0); +} + +static void test_session_name(void) +{ + char n[44]; + + CHECK(vt_session_name_valid("02BE0C000001")); + CHECK(!vt_session_name_valid("")); + CHECK(!vt_session_name_valid("a b")); + CHECK(!vt_session_name_valid("a:b")); + CHECK(!vt_session_name_valid("a\nb")); + memset(n, 'x', 43); + n[43] = '\0'; + CHECK(!vt_session_name_valid(n)); + n[42] = '\0'; + CHECK(vt_session_name_valid(n)); +} + +static void test_chal_codec(void) +{ + uint8_t c[VT_CHAL_SIZE], d[VT_CHAL_SIZE]; + char text[VT_CHAL_TEXT_SIZE]; + int i; + + for (i = 0; i < VT_CHAL_SIZE; i++) + c[i] = (uint8_t)(i * 17 + 3); + c[0] = 0x00; + c[1] = 0xff; + vt_chal_encode(c, text); + CHECK_EQ_INT(strlen(text), 34); + CHECK(strncmp(text, "", d), -1); + CHECK_EQ_INT(vt_chal_decode("OK CHAL: no bracket", d), -1); + CHECK_EQ_INT(vt_chal_decode("OK CHAL: ", d), -1); /* 'q' */ + CHECK_EQ_INT(vt_chal_decode("OK CHAL: ", d), -1); /* 33 chars */ +} + +static void test_xor_reference(void) +{ + uint8_t c[VT_CHAL_SIZE]; + char text[VT_CHAL_TEXT_SIZE]; + + CHECK_EQ_INT(vt_chal_decode(REF_CHAL, c), 0); + vt_chal_xor(c, REF_PASSWORD); + vt_chal_encode(c, text); + CHECK_EQ_STR(text, REF_RESP); +} + +static void test_xor_password_lengths(void) +{ + uint8_t c[VT_CHAL_SIZE], z[VT_CHAL_SIZE]; + int i; + + /* short password repeats */ + memset(c, 0, sizeof(c)); + vt_chal_xor(c, "ab"); + for (i = 0; i < VT_CHAL_SIZE; i++) + CHECK_EQ_INT(c[i], i % 2 ? 'b' : 'a'); + + /* only the first 16 characters matter */ + memset(c, 0x55, sizeof(c)); + memset(z, 0x55, sizeof(z)); + vt_chal_xor(c, "0123456789abcdefXYZ"); + vt_chal_xor(z, "0123456789abcdef"); + CHECK(memcmp(c, z, sizeof(c)) == 0); +} + +static void test_flags(void) +{ + vt_flags_t f; + char out[64]; + + CHECK_EQ_INT(vt_flags_parse("OK FLAGS: \n", &f), 0); + CHECK_EQ_INT(f.transport, 'T'); + CHECK_EQ_INT(f.type, 'e'); + CHECK_EQ_INT(f.keepalive, 1); + CHECK_EQ_INT(vt_flags_check(&f), TUNNEL_OK); + vt_flags_format(&f, out, sizeof(out)); + CHECK_EQ_STR(out, ""); + + CHECK_EQ_INT(vt_flags_parse("OK FLAGS: \n", &f), 0); + CHECK_EQ_INT(vt_flags_check(&f), TUNNEL_OK); + + CHECK_EQ_INT(vt_flags_parse("OK FLAGS: \n", &f), 0); + CHECK_EQ_INT(f.shaper, 512); + CHECK_EQ_INT(vt_flags_check(&f), TUNNEL_OK); + vt_flags_format(&f, out, sizeof(out)); + CHECK_EQ_STR(out, ""); + + CHECK_EQ_INT(vt_flags_parse("OK FLAGS: \n", &f), 0); + CHECK_EQ_INT(vt_flags_check(&f), TUNNEL_ERR_COMPRESSION); + CHECK_EQ_INT(vt_flags_parse("OK FLAGS: \n", &f), 0); + CHECK_EQ_INT(vt_flags_check(&f), TUNNEL_ERR_COMPRESSION); + + CHECK_EQ_INT(vt_flags_parse("OK FLAGS: \n", &f), 0); + CHECK_EQ_INT(f.cipher, 1); + CHECK_EQ_INT(vt_flags_check(&f), TUNNEL_ERR_ENCRYPTION); + CHECK_EQ_INT(vt_flags_parse("OK FLAGS: \n", &f), 0); /* legacy form */ + CHECK_EQ_INT(f.cipher, 0); + CHECK_EQ_INT(vt_flags_check(&f), TUNNEL_ERR_ENCRYPTION); + + CHECK_EQ_INT(vt_flags_parse("OK FLAGS: \n", &f), 0); + CHECK_EQ_INT(vt_flags_check(&f), TUNNEL_ERR_UNSUPPORTED_TRANSPORT); + CHECK_EQ_INT(vt_flags_parse("OK FLAGS: \n", &f), 0); + CHECK_EQ_INT(vt_flags_check(&f), TUNNEL_ERR_UNSUPPORTED_TYPE); + CHECK_EQ_INT(vt_flags_parse("OK FLAGS: \n", &f), 0); + CHECK_EQ_INT(vt_flags_check(&f), TUNNEL_ERR_UNSUPPORTED_TYPE); + CHECK_EQ_INT(vt_flags_parse("OK FLAGS: \n", &f), 0); /* F reserved */ + + CHECK_EQ_INT(vt_flags_parse("OK FLAGS: \n", &f), -1); /* unknown */ + CHECK_EQ_INT(vt_flags_parse("OK FLAGS: ' */ + CHECK_EQ_INT(vt_flags_parse("OK FLAGS: TeK\n", &f), -1); /* no '<' */ + CHECK_EQ_INT(vt_flags_parse("OK FLAGS: \n", &f), -1); /* C without level */ + CHECK_EQ_INT(vt_flags_parse("OK FLAGS: \n", &f), -1); /* S without speed */ +} + +static void test_header(void) +{ + uint8_t b[2]; + + vt_hdr_put(b, 1514); + CHECK_EQ_INT(b[0], 0x05); + CHECK_EQ_INT(b[1], 0xea); + CHECK_EQ_INT(vt_hdr_get(b), 1514); + vt_hdr_put(b, VT_ECHO_REQ); + CHECK_EQ_INT(b[0], 0x20); + CHECK_EQ_INT(b[1], 0x00); + CHECK_EQ_INT(vt_hdr_get(b) & VT_FSIZE_MASK, 0); +} + +int main(void) +{ + RUN(test_mesg_build); + RUN(test_session_name); + RUN(test_chal_codec); + RUN(test_xor_reference); + RUN(test_xor_password_lengths); + RUN(test_flags); + RUN(test_header); + return UNIT_SUMMARY(); +} diff --git a/tests/unit.h b/tests/unit.h new file mode 100644 index 0000000..7d45b16 --- /dev/null +++ b/tests/unit.h @@ -0,0 +1,49 @@ +/* Minimal unit test helpers. */ +#ifndef VTESSE_TESTS_UNIT_H +#define VTESSE_TESTS_UNIT_H + +#include +#include + +static int unit_failures; +static int unit_checks; + +#define CHECK(cond) do { \ + unit_checks++; \ + if (!(cond)) { \ + unit_failures++; \ + fprintf(stderr, "%s:%d: CHECK failed: %s\n", __FILE__, __LINE__, #cond); \ + } \ +} while (0) + +#define CHECK_EQ_INT(a, b) do { \ + long long va_ = (long long)(a), vb_ = (long long)(b); \ + unit_checks++; \ + if (va_ != vb_) { \ + unit_failures++; \ + fprintf(stderr, "%s:%d: %s == %s failed: %lld != %lld\n", \ + __FILE__, __LINE__, #a, #b, va_, vb_); \ + } \ +} while (0) + +#define CHECK_EQ_STR(a, b) do { \ + const char *sa_ = (a), *sb_ = (b); \ + unit_checks++; \ + if (strcmp(sa_, sb_) != 0) { \ + unit_failures++; \ + fprintf(stderr, "%s:%d: %s == %s failed: \"%s\" != \"%s\"\n", \ + __FILE__, __LINE__, #a, #b, sa_, sb_); \ + } \ +} while (0) + +#define RUN(test) do { \ + int before_ = unit_failures; \ + test(); \ + printf("%s %s\n", unit_failures == before_ ? "PASS" : "FAIL", #test); \ +} while (0) + +#define UNIT_SUMMARY() ( \ + printf("%d checks, %d failure(s)\n", unit_checks, unit_failures), \ + unit_failures ? 1 : 0) + +#endif diff --git a/vtun/vtun_client.c b/vtun/vtun_client.c new file mode 100644 index 0000000..46152ba --- /dev/null +++ b/vtun/vtun_client.c @@ -0,0 +1,672 @@ +#include "vtesse/vtun_client.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "netcore/loop.h" + +#ifndef MSG_NOSIGNAL +#define MSG_NOSIGNAL 0 +#endif + +#define DEF_HANDSHAKE_MS 15000 +#define MIN_CHAL_STEP_MS 10000 /* multi killold: the server may wait ~5 s */ +#define DEF_KA_INTERVAL_MS 10000 +#define DEF_KA_COUNT 5 +#define DEF_DATA_DELAY_MS 500 /* the server brings the tap up asynchronously */ +#define DEF_RECONNECT_MIN 1000 +#define DEF_RECONNECT_MAX 30000 +#define DEF_OUTBUF 65536 +#define STABLE_SESSION_MS 30000 /* reset backoff only after a stable session */ +#define INBUF_SIZE (2 + VT_FRAME_MAX_RX + 2048) +#define LINK_MTU 1500 + +enum vt_state { + ST_STOPPED = 0, + ST_WAIT_RECONNECT, + ST_CONNECTING, + ST_WAIT_GREETING, + ST_WAIT_CHAL, + ST_WAIT_FLAGS, + ST_ESTABLISHED +}; + +struct vt { + char *host, *session, *password; + uint16_t port; + vt_cfg_t cfg; + + int state; + int fd; + + uint8_t inbuf[INBUF_SIZE]; + size_t in_len; + size_t skip; /* bytes of an oversized frame still to discard */ + + uint8_t *out; + size_t out_cap, out_off, out_len; + + uint64_t deadline; /* handshake step deadline */ + uint64_t reconnect_at; + uint64_t up_at; + uint64_t data_ok_at; + int up_emitted; /* UP is reported once data may flow */ + uint64_t last_rx; + uint64_t last_ka_tx; + int backoff_ms; + + char greeting[VT_MESG_SIZE]; + vt_flags_t flags; + + tunnel_rx_cb rx_cb; + void *rx_user; + tunnel_event_cb ev_cb; + void *ev_user; +}; + +static void emit(vt_t *vt, int event, int err) +{ + if (vt->ev_cb) + vt->ev_cb(vt->ev_user, event, err); +} + +static int fatal_error(int err) +{ + switch (err) { + case TUNNEL_ERR_CONFIG: + case TUNNEL_ERR_AUTH: + case TUNNEL_ERR_PROTOCOL: + case TUNNEL_ERR_UNSUPPORTED_TYPE: + case TUNNEL_ERR_UNSUPPORTED_TRANSPORT: + case TUNNEL_ERR_COMPRESSION: + case TUNNEL_ERR_ENCRYPTION: + return 1; + default: + return 0; + } +} + +static void close_socket(vt_t *vt) +{ + if (vt->fd >= 0) + close(vt->fd); + vt->fd = -1; + vt->in_len = vt->skip = 0; + vt->out_off = vt->out_len = 0; +} + +/* Tear down the connection; retry later unless the error is final. */ +static void fail(vt_t *vt, int err) +{ + int was_up = vt->state == ST_ESTABLISHED; + uint64_t now = nc_now_ms(); + + close_socket(vt); + if (fatal_error(err) || vt->cfg.no_reconnect) { + vt->state = ST_STOPPED; + emit(vt, TUNNEL_EV_FAILED, err); + return; + } + if (was_up && now - vt->up_at >= STABLE_SESSION_MS) + vt->backoff_ms = vt->cfg.reconnect_min_ms; + vt->state = ST_WAIT_RECONNECT; + vt->reconnect_at = now + (uint64_t)vt->backoff_ms; + vt->backoff_ms *= 2; + if (vt->backoff_ms > vt->cfg.reconnect_max_ms) + vt->backoff_ms = vt->cfg.reconnect_max_ms; + emit(vt, TUNNEL_EV_DOWN, err); +} + +/* ---- output buffer ---- */ + +static void flush(vt_t *vt) +{ + while (vt->out_len > 0) { + ssize_t n = send(vt->fd, vt->out + vt->out_off, vt->out_len, MSG_NOSIGNAL); + if (n < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) + return; + fail(vt, TUNNEL_ERR_IO); + return; + } + vt->out_off += (size_t)n; + vt->out_len -= (size_t)n; + } + vt->out_off = 0; +} + +/* Append a+b atomically (both or nothing). */ +static int queue2(vt_t *vt, const uint8_t *a, size_t al, const uint8_t *b, size_t bl) +{ + if (vt->out_len + al + bl > vt->out_cap) + return -1; + if (vt->out_off + vt->out_len + al + bl > vt->out_cap) { + memmove(vt->out, vt->out + vt->out_off, vt->out_len); + vt->out_off = 0; + } + memcpy(vt->out + vt->out_off + vt->out_len, a, al); + vt->out_len += al; + if (bl) { + memcpy(vt->out + vt->out_off + vt->out_len, b, bl); + vt->out_len += bl; + } + return 0; +} + +static int queue_mesg(vt_t *vt, const char *prefix, const char *arg) +{ + uint8_t m[VT_MESG_SIZE]; + + if (vt_mesg_build(m, prefix, arg) < 0 || queue2(vt, m, sizeof(m), NULL, 0) < 0) + return -1; + return 0; +} + +static void queue_flag(vt_t *vt, uint16_t flag) +{ + uint8_t h[2]; + + vt_hdr_put(h, flag); + queue2(vt, h, 2, NULL, 0); /* a full buffer means the link is stuck anyway */ +} + +/* ---- connection ---- */ + +static void start_connect(vt_t *vt) +{ + struct addrinfo hints, *res = NULL, *ai; + char port[8]; + int fd = -1, one = 1; + + vt->state = ST_CONNECTING; + emit(vt, TUNNEL_EV_CONNECTING, TUNNEL_OK); + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_INET; /* VTun 3.0.x is IPv4 only */ + hints.ai_socktype = SOCK_STREAM; + snprintf(port, sizeof(port), "%u", (unsigned)vt->port); + if (getaddrinfo(vt->host, port, &hints, &res) != 0 || !res) { + fail(vt, TUNNEL_ERR_RESOLVE); + return; + } + for (ai = res; ai; ai = ai->ai_next) { + fd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol); + if (fd < 0) + continue; + fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK); + fcntl(fd, F_SETFD, FD_CLOEXEC); + setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one)); + if (connect(fd, ai->ai_addr, ai->ai_addrlen) == 0 || errno == EINPROGRESS) + break; + close(fd); + fd = -1; + } + freeaddrinfo(res); + if (fd < 0) { + fail(vt, TUNNEL_ERR_CONNECT); + return; + } + vt->fd = fd; + vt->deadline = nc_now_ms() + (uint64_t)vt->cfg.handshake_timeout_ms; +} + +static void establish(vt_t *vt) +{ + uint64_t now = nc_now_ms(); + + vt->state = ST_ESTABLISHED; + vt->up_at = now; + vt->data_ok_at = now + (uint64_t)vt->cfg.data_delay_ms; + vt->last_rx = now; + vt->last_ka_tx = now; + vt->up_emitted = 0; + queue_flag(vt, VT_ECHO_REQ); /* like the stock client */ + /* TUNNEL_EV_UP follows from on_timer after data_delay_ms: the server brings + * the tap up asynchronously and drops the session if data arrives earlier. */ +} + +/* One 50-byte control message during the handshake. */ +static void handle_mesg(vt_t *vt, char *m) +{ + uint8_t chal[VT_CHAL_SIZE]; + char text[VT_CHAL_TEXT_SIZE]; + uint64_t step; + int err; + + m[VT_MESG_SIZE - 1] = '\0'; + switch (vt->state) { + case ST_WAIT_GREETING: + if (strncmp(m, "VTUN", 4) != 0) { + fail(vt, TUNNEL_ERR_PROTOCOL); + return; + } + snprintf(vt->greeting, sizeof(vt->greeting), "%s", m); + vt->greeting[strcspn(vt->greeting, "\r\n")] = '\0'; + queue_mesg(vt, "HOST: ", vt->session); + vt->state = ST_WAIT_CHAL; + vt->deadline = nc_now_ms() + (uint64_t)vt->cfg.handshake_timeout_ms; + break; + + case ST_WAIT_CHAL: + if (strncmp(m, "OK", 2) != 0 || vt_chal_decode(m, chal) != 0) { + fail(vt, strncmp(m, "ERR", 3) == 0 ? TUNNEL_ERR_AUTH : TUNNEL_ERR_PROTOCOL); + return; + } + vt_chal_xor(chal, vt->password); + vt_chal_encode(chal, text); + queue_mesg(vt, "CHAL: ", text); + vt->state = ST_WAIT_FLAGS; + step = (uint64_t)vt->cfg.handshake_timeout_ms; + if (step < MIN_CHAL_STEP_MS) + step = MIN_CHAL_STEP_MS; + vt->deadline = nc_now_ms() + step; + break; + + case ST_WAIT_FLAGS: + if (strncmp(m, "OK", 2) != 0) { + fail(vt, strncmp(m, "ERR", 3) == 0 ? TUNNEL_ERR_AUTH : TUNNEL_ERR_PROTOCOL); + return; + } + if (vt_flags_parse(m, &vt->flags) != 0) { + fail(vt, TUNNEL_ERR_PROTOCOL); + return; + } + err = vt_flags_check(&vt->flags); + if (err != TUNNEL_OK) { + fail(vt, err); + return; + } + establish(vt); + break; + + default: + break; + } +} + +/* Frames after OK FLAGS. Returns bytes consumed. */ +static size_t handle_frames(vt_t *vt, const uint8_t *p, size_t len) +{ + size_t used = 0; + + while (vt->state == ST_ESTABLISHED) { + uint16_t hdr, flen, fl; + + if (vt->skip) { + size_t n = len - used < vt->skip ? len - used : vt->skip; + used += n; + vt->skip -= n; + if (vt->skip) + break; + continue; + } + if (len - used < 2) + break; + hdr = vt_hdr_get(p + used); + fl = hdr & VT_FLAGS_MASK; + flen = hdr & VT_FSIZE_MASK; + if (fl) { + used += 2; + if (fl & VT_CONN_CLOSE) { + fail(vt, TUNNEL_ERR_PEER_CLOSED); + break; + } + if (fl & VT_ECHO_REQ) + queue_flag(vt, VT_ECHO_REP); + continue; /* ECHO_REP and unknown flags carry no payload */ + } + if (flen > VT_FRAME_MAX_RX) { + used += 2; + vt->skip = flen; + continue; + } + if (len - used < 2u + flen) + break; + if (flen && vt->rx_cb) + vt->rx_cb(vt->rx_user, p + used + 2, flen); + used += 2u + flen; + } + return used; +} + +static void process_input(vt_t *vt) +{ + size_t used = 0; + + while (vt->state >= ST_WAIT_GREETING && vt->state <= ST_WAIT_FLAGS && + vt->in_len - used >= VT_MESG_SIZE) { + char m[VT_MESG_SIZE]; + + memcpy(m, vt->inbuf + used, VT_MESG_SIZE); + used += VT_MESG_SIZE; + handle_mesg(vt, m); + } + if (vt->state == ST_ESTABLISHED) + used += handle_frames(vt, vt->inbuf + used, vt->in_len - used); + if (vt->fd < 0) + return; /* failed while processing; buffers already reset */ + if (used) { + memmove(vt->inbuf, vt->inbuf + used, vt->in_len - used); + vt->in_len -= used; + } +} + +/* ---- tunnel_ops ---- */ + +static int op_get_fd(void *ctx) +{ + return ((vt_t *)ctx)->fd; +} + +static int op_wants_write(void *ctx) +{ + vt_t *vt = ctx; + + return vt->fd >= 0 && (vt->state == ST_CONNECTING || vt->out_len > 0); +} + +static void op_on_writable(void *ctx) +{ + vt_t *vt = ctx; + + if (vt->fd < 0) + return; + if (vt->state == ST_CONNECTING) { + int err = 0; + socklen_t l = sizeof(err); + + if (getsockopt(vt->fd, SOL_SOCKET, SO_ERROR, &err, &l) < 0 || err) { + fail(vt, TUNNEL_ERR_CONNECT); + return; + } + vt->state = ST_WAIT_GREETING; + vt->deadline = nc_now_ms() + (uint64_t)vt->cfg.handshake_timeout_ms; + return; + } + flush(vt); +} + +static void op_on_readable(void *ctx) +{ + vt_t *vt = ctx; + + if (vt->fd < 0 || vt->state == ST_CONNECTING) + return; + for (;;) { + ssize_t n; + + if (vt->in_len == sizeof(vt->inbuf)) { + fail(vt, TUNNEL_ERR_PROTOCOL); /* cannot happen with valid framing */ + return; + } + n = recv(vt->fd, vt->inbuf + vt->in_len, sizeof(vt->inbuf) - vt->in_len, 0); + if (n == 0) { + fail(vt, TUNNEL_ERR_PEER_CLOSED); /* retriable, also during the handshake */ + return; + } + if (n < 0) { + if (errno == EAGAIN || errno == EWOULDBLOCK) + break; + if (errno == EINTR) + continue; + fail(vt, TUNNEL_ERR_IO); + return; + } + vt->in_len += (size_t)n; + vt->last_rx = nc_now_ms(); + process_input(vt); + if (vt->fd < 0) + return; + } + if (vt->out_len) + flush(vt); +} + +static int ms_until(uint64_t t, uint64_t now) +{ + if (t <= now) + return 0; + return t - now > 0x7fffffff ? 0x7fffffff : (int)(t - now); +} + +static int op_next_timeout_ms(void *ctx) +{ + vt_t *vt = ctx; + uint64_t now = nc_now_ms(); + int a, b; + + switch (vt->state) { + case ST_WAIT_RECONNECT: + return ms_until(vt->reconnect_at, now); + case ST_CONNECTING: + case ST_WAIT_GREETING: + case ST_WAIT_CHAL: + case ST_WAIT_FLAGS: + return ms_until(vt->deadline, now); + case ST_ESTABLISHED: + a = ms_until(vt->last_ka_tx + (uint64_t)vt->cfg.ka_interval_ms, now); + b = ms_until(vt->last_rx + (uint64_t)vt->cfg.ka_interval_ms * (uint64_t)vt->cfg.ka_count, now); + if (a > b) + a = b; + if (!vt->up_emitted && ms_until(vt->data_ok_at, now) < a) + a = ms_until(vt->data_ok_at, now); + return a; + default: + return -1; + } +} + +static void op_on_timer(void *ctx) +{ + vt_t *vt = ctx; + uint64_t now = nc_now_ms(); + + switch (vt->state) { + case ST_WAIT_RECONNECT: + if (now >= vt->reconnect_at) + start_connect(vt); + break; + case ST_CONNECTING: + case ST_WAIT_GREETING: + case ST_WAIT_CHAL: + case ST_WAIT_FLAGS: + if (now >= vt->deadline) + fail(vt, vt->state == ST_CONNECTING ? TUNNEL_ERR_CONNECT : TUNNEL_ERR_TIMEOUT); + break; + case ST_ESTABLISHED: + if (now - vt->last_rx >= (uint64_t)vt->cfg.ka_interval_ms * (uint64_t)vt->cfg.ka_count) { + fail(vt, TUNNEL_ERR_KEEPALIVE); + break; + } + if (now - vt->last_ka_tx >= (uint64_t)vt->cfg.ka_interval_ms) { + vt->last_ka_tx = now; + queue_flag(vt, VT_ECHO_REQ); + flush(vt); + } + if (vt->state == ST_ESTABLISHED && !vt->up_emitted && now >= vt->data_ok_at) { + vt->up_emitted = 1; + emit(vt, TUNNEL_EV_UP, TUNNEL_OK); + } + break; + default: + break; + } +} + +static void op_set_rx_cb(void *ctx, tunnel_rx_cb cb, void *user) +{ + vt_t *vt = ctx; + + vt->rx_cb = cb; + vt->rx_user = user; +} + +static void op_set_event_cb(void *ctx, tunnel_event_cb cb, void *user) +{ + vt_t *vt = ctx; + + vt->ev_cb = cb; + vt->ev_user = user; +} + +static int op_send(void *ctx, const uint8_t *pkt, size_t len) +{ + vt_t *vt = ctx; + uint8_t h[2]; + + if (vt->state != ST_ESTABLISHED || !vt->up_emitted || len == 0 || len > VT_FRAME_SIZE) + return -1; + vt_hdr_put(h, (uint16_t)len); + if (queue2(vt, h, 2, pkt, len) < 0) + return -1; + flush(vt); + return 0; +} + +static int op_link_type(void *ctx) +{ + vt_t *vt = ctx; + + if (vt->state != ST_ESTABLISHED || !vt->up_emitted) + return TUNNEL_LINK_NONE; + return vt->flags.type == 'e' ? TUNNEL_LINK_L2 : TUNNEL_LINK_L3; +} + +static int op_mtu(void *ctx) +{ + (void)ctx; + return LINK_MTU; +} + +static void op_close(void *ctx) +{ + vt_t *vt = ctx; + + if (vt->state == ST_ESTABLISHED && vt->fd >= 0) { + uint8_t h[2]; + + + flush(vt); + vt_hdr_put(h, VT_CONN_CLOSE); + if (vt->fd >= 0 && vt->out_len == 0) + (void)!send(vt->fd, h, 2, MSG_NOSIGNAL); + } + close_socket(vt); + vt->state = ST_STOPPED; +} + +static const tunnel_ops_t vt_ops = { + op_get_fd, op_wants_write, op_on_readable, op_on_writable, + op_next_timeout_ms, op_on_timer, op_set_rx_cb, op_set_event_cb, + op_send, op_link_type, op_mtu, op_close +}; + +/* ---- public ---- */ + +const tunnel_ops_t *vt_tunnel_ops(void) +{ + return &vt_ops; +} + +tunnel_t vt_tunnel(vt_t *vt) +{ + tunnel_t t; + + t.ops = &vt_ops; + t.ctx = vt; + return t; +} + +vt_t *vt_create(const vt_cfg_t *cfg, int *err) +{ + vt_t *vt; + + *err = TUNNEL_ERR_CONFIG; + if (!cfg || !cfg->host || !*cfg->host || !cfg->port || + !vt_session_name_valid(cfg->session) || !cfg->password || !*cfg->password || + cfg->auth != VT_AUTH_XOR) + return NULL; + + *err = TUNNEL_ERR_NOMEM; + vt = calloc(1, sizeof(*vt)); + if (!vt) + return NULL; + vt->cfg = *cfg; + vt->host = strdup(cfg->host); + vt->session = strdup(cfg->session); + vt->password = strdup(cfg->password); + vt->port = cfg->port; + vt->cfg.host = vt->cfg.session = vt->cfg.password = NULL; + if (!vt->cfg.handshake_timeout_ms) + vt->cfg.handshake_timeout_ms = DEF_HANDSHAKE_MS; + if (!vt->cfg.ka_interval_ms) + vt->cfg.ka_interval_ms = DEF_KA_INTERVAL_MS; + if (!vt->cfg.ka_count) + vt->cfg.ka_count = DEF_KA_COUNT; + if (!vt->cfg.data_delay_ms) + vt->cfg.data_delay_ms = DEF_DATA_DELAY_MS; + if (!vt->cfg.reconnect_min_ms) + vt->cfg.reconnect_min_ms = DEF_RECONNECT_MIN; + if (!vt->cfg.reconnect_max_ms) + vt->cfg.reconnect_max_ms = DEF_RECONNECT_MAX; + if (!vt->cfg.out_buffer_size) + vt->cfg.out_buffer_size = DEF_OUTBUF; + vt->out_cap = (size_t)vt->cfg.out_buffer_size; + vt->out = malloc(vt->out_cap); + vt->fd = -1; + vt->state = ST_STOPPED; + vt->backoff_ms = vt->cfg.reconnect_min_ms; + if (!vt->host || !vt->session || !vt->password || !vt->out || + vt->out_cap < VT_MESG_SIZE + 2 + VT_FRAME_SIZE) { + vt_destroy(vt); + return NULL; + } + *err = TUNNEL_OK; + return vt; +} + +void vt_destroy(vt_t *vt) +{ + if (!vt) + return; + close_socket(vt); + if (vt->password) + memset(vt->password, 0, strlen(vt->password)); + free(vt->host); + free(vt->session); + free(vt->password); + free(vt->out); + free(vt); +} + +int vt_start(vt_t *vt) +{ + if (vt->state != ST_STOPPED) + return -1; + vt->backoff_ms = vt->cfg.reconnect_min_ms; + start_connect(vt); + return 0; +} + +const char *vt_greeting(const vt_t *vt) +{ + return vt->greeting; +} + +const vt_flags_t *vt_session_flags(const vt_t *vt) +{ + return &vt->flags; +} + +int vt_is_up(const vt_t *vt) +{ + return vt->state == ST_ESTABLISHED && vt->up_emitted; +} diff --git a/vtun/vtun_proto.c b/vtun/vtun_proto.c new file mode 100644 index 0000000..ca8344f --- /dev/null +++ b/vtun/vtun_proto.c @@ -0,0 +1,193 @@ +#include "vtesse/vtun_proto.h" + +#include +#include + +#include "netcore/tunnel.h" + +static const char chal_chars[] = "abcdefghijklmnop"; + +int vt_session_name_valid(const char *name) +{ + size_t len; + const char *p; + + if (!name) + return 0; + len = strlen(name); + if (len == 0 || len > VT_SESSION_MAX) + return 0; + for (p = name; *p; p++) { + unsigned char c = (unsigned char)*p; + if (c <= ' ' || c >= 0x7f || c == ':') + return 0; + } + return 1; +} + +int vt_mesg_build(uint8_t out[VT_MESG_SIZE], const char *prefix, const char *arg) +{ + size_t pl = strlen(prefix), al = arg ? strlen(arg) : 0; + + if (pl + al + 1 > VT_MESG_SIZE - 1) + return -1; + memset(out, 0, VT_MESG_SIZE); + memcpy(out, prefix, pl); + if (al) + memcpy(out + pl, arg, al); + out[pl + al] = '\n'; + return 0; +} + +void vt_chal_encode(const uint8_t chal[VT_CHAL_SIZE], char out[VT_CHAL_TEXT_SIZE]) +{ + int i; + + out[0] = '<'; + for (i = 0; i < VT_CHAL_SIZE; i++) { + out[1 + i * 2] = chal_chars[chal[i] >> 4]; + out[2 + i * 2] = chal_chars[chal[i] & 0x0f]; + } + out[1 + VT_CHAL_SIZE * 2] = '>'; + out[2 + VT_CHAL_SIZE * 2] = '\0'; +} + +int vt_chal_decode(const char *msg, uint8_t chal[VT_CHAL_SIZE]) +{ + const char *p = strchr(msg, '<'); + int i; + + if (!p) + return -1; + p++; + for (i = 0; i < VT_CHAL_SIZE * 2; i++) + if (p[i] < 'a' || p[i] > 'p') + return -1; + if (p[VT_CHAL_SIZE * 2] != '>') + return -1; + for (i = 0; i < VT_CHAL_SIZE; i++) + chal[i] = (uint8_t)(((p[i * 2] - 'a') << 4) | (p[i * 2 + 1] - 'a')); + return 0; +} + +void vt_chal_xor(uint8_t chal[VT_CHAL_SIZE], const char *pwd) +{ + size_t len = strlen(pwd); + int i; + + for (i = 0; i < VT_CHAL_SIZE; i++) + chal[i] ^= (uint8_t)pwd[i % len]; +} + +/* Decimal number at *pp (optional when 'optional'); advances *pp. */ +static int parse_num(const char **pp, int *out, int optional) +{ + const char *p = *pp; + long v = 0; + int digits = 0; + + while (*p >= '0' && *p <= '9') { + if (v < 100000000) + v = v * 10 + (*p - '0'); + p++; + digits++; + } + if (!digits && !optional) + return -1; + *out = (int)v; + *pp = p; + return 0; +} + +int vt_flags_parse(const char *msg, vt_flags_t *f) +{ + const char *p = strchr(msg, '<'); + + memset(f, 0, sizeof(*f)); + if (!p) + return -1; + p++; + while (*p) { + char c = *p++; + int n; + + switch (c) { + case 't': case 'p': case 'e': case 'u': + f->type = c; + break; + case 'T': case 'U': + f->transport = c; + break; + case 'K': + f->keepalive = 1; + break; + case 'C': + if (parse_num(&p, &n, 0)) + return -1; + f->zlib = 1; + f->zlevel = n; + break; + case 'L': + if (parse_num(&p, &n, 0)) + return -1; + f->lzo = 1; + f->zlevel = n; + break; + case 'E': + parse_num(&p, &n, 1); + f->encrypt = 1; + f->cipher = n; + break; + case 'S': + if (parse_num(&p, &n, 0)) + return -1; + f->shaper = n; + break; + case 'F': /* reserved */ + break; + case '>': + return 0; + default: + return -1; + } + } + return -1; /* no closing '>' */ +} + +int vt_flags_check(const vt_flags_t *f) +{ + if (f->zlib || f->lzo) + return TUNNEL_ERR_COMPRESSION; + if (f->encrypt) + return TUNNEL_ERR_ENCRYPTION; + if (f->transport == 'U') + return TUNNEL_ERR_UNSUPPORTED_TRANSPORT; + if (f->type != 'u' && f->type != 'e') + return TUNNEL_ERR_UNSUPPORTED_TYPE; + return TUNNEL_OK; +} + +void vt_flags_format(const vt_flags_t *f, char *out, size_t size) +{ + char tmp[64]; + size_t n = 0; + + tmp[n++] = '<'; + if (f->transport) + tmp[n++] = f->transport; + if (f->type) + tmp[n++] = f->type; + if (f->shaper) + n += (size_t)sprintf(tmp + n, "S%d", f->shaper); + if (f->zlib) + n += (size_t)sprintf(tmp + n, "C%d", f->zlevel); + if (f->lzo) + n += (size_t)sprintf(tmp + n, "L%d", f->zlevel); + if (f->keepalive) + tmp[n++] = 'K'; + if (f->encrypt) + n += (size_t)sprintf(tmp + n, "E%d", f->cipher); + tmp[n++] = '>'; + tmp[n] = '\0'; + snprintf(out, size, "%s", tmp); +}