Add Transparent Proxy and PROXY v2 support and help page
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <libssh2.h>
|
||||
#include <net/if.h>
|
||||
#include <netdb.h>
|
||||
@@ -10,6 +11,8 @@
|
||||
#include <string.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/uio.h>
|
||||
#include <sys/wait.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
@@ -31,6 +34,7 @@ struct csink {
|
||||
char bind[64];
|
||||
char iface[IFNAMSIZ]; // tunnel sink: pinned to this interface
|
||||
unsigned ifindex;
|
||||
int origin; // sink passes the peer address on: report it in PB1
|
||||
int fd; // -1 while binding fails; retried by sinks_retry()
|
||||
int keep;
|
||||
char state[96]; // last SINKSTATE sent, to report only changes
|
||||
@@ -50,6 +54,8 @@ struct cc {
|
||||
struct csink *sink; // K_SINK_UDP: listener to answer through
|
||||
struct sockaddr_storage peer;
|
||||
socklen_t peerlen;
|
||||
unsigned char pp[PROXY_V2_MAX]; // K_SRC_UDP: PROXY header for every datagram
|
||||
size_t pplen;
|
||||
long last_act;
|
||||
};
|
||||
|
||||
@@ -77,6 +83,7 @@ static char token[PB_TOKEN_LEN + 1];
|
||||
static int hub_port;
|
||||
static int hello_done;
|
||||
static long last_svc, last_retry;
|
||||
static int spoof_routing_done; // 1 = rules added by us, -1 = attempted and failed
|
||||
static long ctl_last_rx; // last control data; the hub PINGs every 30 s
|
||||
|
||||
static volatile sig_atomic_t sig_stop;
|
||||
@@ -416,8 +423,11 @@ static void udp_local_io(struct cc *c)
|
||||
int len;
|
||||
while ((len = udp_frame_peek(&c->from_ch, &payload)) != 0) {
|
||||
size_t plen = len == -2 ? 0 : (size_t)len;
|
||||
if (c->kind == K_SRC_UDP && c->fd >= 0)
|
||||
send(c->fd, payload, plen, MSG_DONTWAIT);
|
||||
if (c->kind == K_SRC_UDP && c->fd >= 0) {
|
||||
struct iovec iov[2] = { { c->pp, c->pplen }, { (void *)payload, plen } };
|
||||
struct msghdr mh = { .msg_iov = iov + !c->pplen, .msg_iovlen = c->pplen ? 2 : 1 };
|
||||
sendmsg(c->fd, &mh, MSG_DONTWAIT);
|
||||
}
|
||||
else if (c->kind == K_SINK_UDP && c->sink && c->sink->fd >= 0)
|
||||
sendto(c->sink->fd, payload, plen, MSG_DONTWAIT, (struct sockaddr *)&c->peer, c->peerlen);
|
||||
buf_consume(&c->from_ch, plen + 2);
|
||||
@@ -490,7 +500,7 @@ static void sink_bind(struct csink *s)
|
||||
sink_report(s, "ok -");
|
||||
}
|
||||
|
||||
static void sinks_add(int id, int proto, const char *bind, int port, const char *iface)
|
||||
static void sinks_add(int id, int proto, const char *bind, int port, const char *iface, int origin)
|
||||
{
|
||||
if (!strcmp(iface, "-") || !iface_valid(iface))
|
||||
iface = "";
|
||||
@@ -498,6 +508,7 @@ static void sinks_add(int id, int proto, const char *bind, int port, const char
|
||||
if (s->sink_id == id && s->proto == proto && s->port == port && !strcmp(s->bind, bind) &&
|
||||
!strcmp(s->iface, iface)) {
|
||||
s->keep = 1;
|
||||
s->origin = origin;
|
||||
// The hub forgets states on reload; repeat ours.
|
||||
s->state[0] = '\0';
|
||||
sink_report(s, s->fd >= 0 ? "ok -" : "err bind_failed");
|
||||
@@ -520,6 +531,7 @@ static void sinks_add(int id, int proto, const char *bind, int port, const char
|
||||
s->sink_id = id;
|
||||
s->proto = proto;
|
||||
s->port = port;
|
||||
s->origin = origin;
|
||||
s->fd = -1;
|
||||
snprintf(s->bind, sizeof(s->bind), "%s", bind);
|
||||
snprintf(s->iface, sizeof(s->iface), "%s", iface);
|
||||
@@ -561,11 +573,30 @@ static void sinks_end(void)
|
||||
}
|
||||
}
|
||||
|
||||
// Data channel header; sinks with an origin add the addresses they saw.
|
||||
static void sink_header(struct buf *b, struct csink *s, const struct sockaddr_storage *peer, int local_fd)
|
||||
{
|
||||
struct sockaddr_storage p = *peer, l;
|
||||
socklen_t ll = sizeof(l);
|
||||
if (!s->origin || getsockname(local_fd, (struct sockaddr *)&l, &ll) < 0) {
|
||||
buf_printf(b, "PB1 %s SINK %d\n", token, s->sink_id);
|
||||
return;
|
||||
}
|
||||
sockaddr_unmap(&p);
|
||||
sockaddr_unmap(&l);
|
||||
char ps[64], ls[64];
|
||||
sockaddr_str((struct sockaddr *)&p, ps, sizeof(ps));
|
||||
sockaddr_str((struct sockaddr *)&l, ls, sizeof(ls));
|
||||
buf_printf(b, "PB1 %s SINK %d %s %s\n", token, s->sink_id, ps, ls);
|
||||
}
|
||||
|
||||
static void sink_accept(struct csink *s)
|
||||
{
|
||||
if (s->proto == PROTO_TCP) {
|
||||
for (;;) {
|
||||
int fd = accept4(s->fd, NULL, NULL, SOCK_NONBLOCK | SOCK_CLOEXEC);
|
||||
struct sockaddr_storage peer;
|
||||
socklen_t plen = sizeof(peer);
|
||||
int fd = accept4(s->fd, (struct sockaddr *)&peer, &plen, SOCK_NONBLOCK | SOCK_CLOEXEC);
|
||||
if (fd < 0)
|
||||
return;
|
||||
tune_stream(fd);
|
||||
@@ -574,7 +605,7 @@ static void sink_accept(struct csink *s)
|
||||
close(fd);
|
||||
continue;
|
||||
}
|
||||
buf_printf(&c->to_ch, "PB1 %s SINK %d\n", token, s->sink_id);
|
||||
sink_header(&c->to_ch, s, &peer, fd);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -597,7 +628,8 @@ static void sink_accept(struct csink *s)
|
||||
c->sink = s;
|
||||
c->peer = peer;
|
||||
c->peerlen = plen;
|
||||
buf_printf(&c->to_ch, "PB1 %s SINK %d\n", token, s->sink_id);
|
||||
// Local address of the listener; 0.0.0.0 when bound to any.
|
||||
sink_header(&c->to_ch, s, &peer, s->fd);
|
||||
}
|
||||
if (c->to_ch.len < BUF_LIMIT)
|
||||
udp_frame_append(&c->to_ch, tmp, (size_t)n);
|
||||
@@ -605,6 +637,117 @@ static void sink_accept(struct csink *s)
|
||||
}
|
||||
}
|
||||
|
||||
/* Transparent source spoofing */
|
||||
|
||||
// Runs ip(8) without a shell. Returns its exit status, -1 if it did not run.
|
||||
static int run_ip(const char *const argv[])
|
||||
{
|
||||
pid_t pid = fork();
|
||||
if (pid < 0)
|
||||
return -1;
|
||||
if (pid == 0) {
|
||||
int nul = open("/dev/null", O_RDWR);
|
||||
if (nul >= 0) {
|
||||
dup2(nul, STDOUT_FILENO);
|
||||
dup2(nul, STDERR_FILENO);
|
||||
}
|
||||
execvp("ip", (char *const *)argv);
|
||||
_exit(127);
|
||||
}
|
||||
int st;
|
||||
while (waitpid(pid, &st, 0) < 0)
|
||||
if (errno != EINTR)
|
||||
return -1;
|
||||
return WIFEXITED(st) ? WEXITSTATUS(st) : -1;
|
||||
}
|
||||
|
||||
// A loopback service answers a spoofed peer address; without this the reply
|
||||
// would be routed out of the host instead of back to our transparent socket
|
||||
// (same setup as go-mmproxy). Only packets from loopback addresses that are
|
||||
// not addressed to a local address are affected.
|
||||
static void spoof_routing(int add)
|
||||
{
|
||||
char tab[16];
|
||||
snprintf(tab, sizeof(tab), "%d", cfg->transparent_table);
|
||||
static const char *const fams[2][3] = { { "-4", "127.0.0.0/8", "0.0.0.0/0" }, { "-6", "::1/128", "::/0" } };
|
||||
int ok4 = 1;
|
||||
for (int i = 0; i < 2; i++) {
|
||||
const char *fam = fams[i][0];
|
||||
// Remove copies left by an earlier run, then add exactly one.
|
||||
for (int k = 0; k < 16; k++) {
|
||||
const char *del[] = { "ip", fam, "rule", "del", "pref", tab, NULL };
|
||||
if (run_ip(del) != 0)
|
||||
break;
|
||||
}
|
||||
const char *flush[] = { "ip", fam, "route", "flush", "table", tab, NULL };
|
||||
run_ip(flush);
|
||||
if (!add)
|
||||
continue;
|
||||
const char *rule[] = { "ip", fam, "rule", "add", "pref", tab, "from", fams[i][1], "iif", "lo", "lookup", tab, NULL };
|
||||
const char *route[] = { "ip", fam, "route", "replace", "local", fams[i][2], "dev", "lo", "table", tab, NULL };
|
||||
int ok = run_ip(rule) == 0 && run_ip(route) == 0;
|
||||
if (i == 0)
|
||||
ok4 = ok;
|
||||
else if (!ok)
|
||||
log_debug("transparent spoofing: IPv6 policy routing not set up");
|
||||
}
|
||||
if (!add)
|
||||
return;
|
||||
if (ok4)
|
||||
log_info("transparent spoofing: policy routing set up (table %s)", tab);
|
||||
else
|
||||
log_warn("transparent spoofing: setting up policy routing failed (is iproute2 installed?)");
|
||||
spoof_routing_done = ok4 ? 1 : -1;
|
||||
}
|
||||
|
||||
/* Source connections */
|
||||
|
||||
// OPEN <conn_id> <proto> <host> <port> <iface|-> [<origin> <peer> <local>]
|
||||
static void open_source(char **f, int n)
|
||||
{
|
||||
int proto = proto_parse(f[2]);
|
||||
if (proto < 0)
|
||||
return;
|
||||
const char *iface = n > 5 && strcmp(f[5], "-") && iface_valid(f[5]) ? f[5] : NULL;
|
||||
struct sockaddr_storage peer, local;
|
||||
int proxy = 0, spoof = 0;
|
||||
if (n >= 9 && sockaddr_parse(f[7], &peer) == 0 && sockaddr_parse(f[8], &local) == 0) {
|
||||
proxy = !strcmp(f[6], "proxy");
|
||||
spoof = !strcmp(f[6], "spoof");
|
||||
}
|
||||
if (spoof && !spoof_routing_done && cfg->transparent_table > 0)
|
||||
spoof_routing(1);
|
||||
|
||||
int inprog = 0;
|
||||
int fd = net_connect(f[3], atoi(f[4]), proto, iface, spoof ? (struct sockaddr *)&peer : NULL, &inprog);
|
||||
if (fd < 0 && spoof) {
|
||||
// E.g. an IPv6 peer for an IPv4-only service, or no CAP_NET_ADMIN.
|
||||
log_warn("OPEN %s: cannot connect to %s:%s from %s (%s), connecting without spoofing", f[1], f[3], f[4],
|
||||
f[7], strerror(errno));
|
||||
fd = net_connect(f[3], atoi(f[4]), proto, iface, NULL, &inprog);
|
||||
}
|
||||
if (fd < 0) {
|
||||
log_debug("OPEN %s: cannot connect to %s:%s: %s", f[1], f[3], f[4], strerror(errno));
|
||||
return; // the hub times out the waiting connection
|
||||
}
|
||||
struct cc *c = cc_new(proto == PROTO_UDP ? K_SRC_UDP : K_SRC_TCP, fd);
|
||||
if (!c) {
|
||||
close(fd);
|
||||
return;
|
||||
}
|
||||
c->connecting = inprog;
|
||||
if (proxy) {
|
||||
unsigned char hdr[PROXY_V2_MAX];
|
||||
size_t len = proxy_v2_header(hdr, proto, (struct sockaddr *)&peer, (struct sockaddr *)&local);
|
||||
// TCP: first bytes towards the service. UDP: in front of every datagram.
|
||||
if (proto == PROTO_TCP)
|
||||
buf_append(&c->from_ch, hdr, len);
|
||||
else
|
||||
memcpy(c->pp, hdr, c->pplen = len);
|
||||
}
|
||||
buf_printf(&c->to_ch, "PB1 %s OPEN %s\n", token, f[1]);
|
||||
}
|
||||
|
||||
/* Control messages */
|
||||
|
||||
static void send_services(void)
|
||||
@@ -659,25 +802,11 @@ static void ctl_line(char *line)
|
||||
} else if (!strcmp(f[0], "SINK") && n >= 5) {
|
||||
int proto = proto_parse(f[2]);
|
||||
if (proto >= 0)
|
||||
sinks_add(atoi(f[1]), proto, f[3], atoi(f[4]), n > 5 ? f[5] : "-");
|
||||
sinks_add(atoi(f[1]), proto, f[3], atoi(f[4]), n > 5 ? f[5] : "-", n > 6 && strcmp(f[6], "-"));
|
||||
} else if (!strcmp(f[0], "SINKS-END")) {
|
||||
sinks_end();
|
||||
} else if (!strcmp(f[0], "OPEN") && n >= 5) {
|
||||
int proto = proto_parse(f[2]);
|
||||
int inprog = 0;
|
||||
const char *iface = n > 5 && strcmp(f[5], "-") && iface_valid(f[5]) ? f[5] : NULL;
|
||||
int fd = proto < 0 ? -1 : net_connect(f[3], atoi(f[4]), proto, iface, &inprog);
|
||||
if (fd < 0) {
|
||||
log_debug("OPEN %s: cannot connect to %s:%s: %s", f[1], f[3], f[4], strerror(errno));
|
||||
return; // the hub times out the waiting connection
|
||||
}
|
||||
struct cc *c = cc_new(proto == PROTO_UDP ? K_SRC_UDP : K_SRC_TCP, fd);
|
||||
if (!c) {
|
||||
close(fd);
|
||||
return;
|
||||
}
|
||||
c->connecting = inprog;
|
||||
buf_printf(&c->to_ch, "PB1 %s OPEN %s\n", token, f[1]);
|
||||
open_source(f, n);
|
||||
} else if (!strcmp(f[0], "SVC-REQ")) {
|
||||
send_services();
|
||||
} else if (!strcmp(f[0], "PING")) {
|
||||
@@ -1099,6 +1228,8 @@ int client_run(const struct pb_config *conf)
|
||||
usleep(100000);
|
||||
backoff = backoff < 30 ? backoff * 2 : 30;
|
||||
}
|
||||
if (spoof_routing_done > 0)
|
||||
spoof_routing(0);
|
||||
libssh2_exit();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ void config_defaults(struct pb_config *c)
|
||||
snprintf(c->known_hosts, sizeof(c->known_hosts), "/etc/patchbay/known_hosts");
|
||||
snprintf(c->ssh_user, sizeof(c->ssh_user), "patchbay");
|
||||
c->services_interval = 30;
|
||||
c->transparent_table = 470;
|
||||
c->ssh_port = 0; // 0 = use target_port
|
||||
c->hub_port = 7701;
|
||||
snprintf(c->sshd_host_key, sizeof(c->sshd_host_key), "/etc/patchbay/ssh_host_ed25519_key");
|
||||
@@ -93,6 +94,8 @@ int config_parse_line(struct pb_config *c, char *line)
|
||||
STR(ssh_user);
|
||||
} else if (!strcasecmp(key, "ServicesInterval")) {
|
||||
c->services_interval = atoi(val);
|
||||
} else if (!strcasecmp(key, "TransparentTable")) {
|
||||
c->transparent_table = atoi(val);
|
||||
} else if (!strcasecmp(key, "HubPort")) {
|
||||
c->hub_port = atoi(val);
|
||||
} else if (!strcasecmp(key, "SSHHostKey")) {
|
||||
|
||||
@@ -21,6 +21,7 @@ struct pb_config {
|
||||
char known_hosts[256];
|
||||
char ssh_user[64];
|
||||
int services_interval; // seconds between Services reports
|
||||
int transparent_table; // policy routing table for spoofing, 0 = set up by hand
|
||||
|
||||
// target role
|
||||
int ssh_port; // dedicated sshd port (same value clients use as TargetPort)
|
||||
|
||||
@@ -32,8 +32,9 @@ sqlite3 *db_open(const char *path)
|
||||
sqlite3_close(db);
|
||||
return NULL;
|
||||
}
|
||||
// Migration for databases from before tunnel nodes; fails harmlessly if present.
|
||||
// Migrations for databases from before tunnel nodes / origin; fail harmlessly if present.
|
||||
sqlite3_exec(db, "ALTER TABLE nodes ADD COLUMN iface TEXT NOT NULL DEFAULT ''", NULL, NULL, NULL);
|
||||
sqlite3_exec(db, "ALTER TABLE nodes ADD COLUMN origin TEXT NOT NULL DEFAULT ''", NULL, NULL, NULL);
|
||||
return db;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <sys/epoll.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/uio.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
@@ -40,11 +41,15 @@ enum node_type {
|
||||
NODE_TUNNEL_SOURCE, NODE_TUNNEL_SINK,
|
||||
};
|
||||
|
||||
// How the connecting peer's address reaches the service (nodes.origin).
|
||||
enum { ORIGIN_NONE, ORIGIN_PROXY, ORIGIN_SPOOF };
|
||||
|
||||
// A sink resolved back to its source. Host id 0 means the target itself.
|
||||
struct route {
|
||||
int sink_id;
|
||||
int sink_type;
|
||||
int sink_client;
|
||||
int origin;
|
||||
char bind[256];
|
||||
char sink_iface[IFNAMSIZ];
|
||||
int port;
|
||||
@@ -138,6 +143,11 @@ struct conn {
|
||||
struct sockaddr_storage peer;
|
||||
socklen_t peerlen;
|
||||
struct buf ain, aout, bin, bout;
|
||||
int origin;
|
||||
int has_addr; // o_peer/o_local known (sinks with an origin)
|
||||
struct sockaddr_storage o_peer, o_local;
|
||||
unsigned char pp[PROXY_V2_MAX]; // PROXY header for a source on the target
|
||||
size_t pplen;
|
||||
int dead;
|
||||
};
|
||||
|
||||
@@ -178,6 +188,20 @@ static const char *iface_field(const char *iface)
|
||||
return iface[0] ? iface : "-";
|
||||
}
|
||||
|
||||
static const char *origin_name(int origin)
|
||||
{
|
||||
return origin == ORIGIN_PROXY ? "proxy" : origin == ORIGIN_SPOOF ? "spoof" : "-";
|
||||
}
|
||||
|
||||
static int origin_parse(const char *s)
|
||||
{
|
||||
if (!strcmp(s, "proxy_v2"))
|
||||
return ORIGIN_PROXY;
|
||||
if (!strcmp(s, "transparent"))
|
||||
return ORIGIN_SPOOF;
|
||||
return ORIGIN_NONE;
|
||||
}
|
||||
|
||||
/* Epoll helpers */
|
||||
|
||||
static void ev_set(struct ev *e, uint32_t mask)
|
||||
@@ -291,8 +315,8 @@ static void ctl_push_sinks(struct ctl *c)
|
||||
// Invalid routes are not pushed so the client does not bind a dead port.
|
||||
if (!route_on_client(r, c->client_id) || r->err[0])
|
||||
continue;
|
||||
ctl_send(c, "SINK %d %s %s %d %s", r->sink_id, proto_name(r->proto), r->bind, r->port,
|
||||
iface_field(r->sink_iface));
|
||||
ctl_send(c, "SINK %d %s %s %d %s %s", r->sink_id, proto_name(r->proto), r->bind, r->port,
|
||||
iface_field(r->sink_iface), origin_name(r->origin));
|
||||
}
|
||||
ctl_send(c, "SINKS-END");
|
||||
}
|
||||
@@ -529,6 +553,7 @@ static struct conn *conn_new(struct route *r, int a_kind)
|
||||
c->sink_id = r->sink_id;
|
||||
c->src_client = r->src_client;
|
||||
c->proto = r->proto;
|
||||
c->origin = r->origin;
|
||||
c->state = CONN_WAIT;
|
||||
c->a_kind = a_kind;
|
||||
c->b_kind = B_STREAM;
|
||||
@@ -602,8 +627,12 @@ static void dg_to_b(struct conn *c, const char *p, size_t n)
|
||||
if (c->st)
|
||||
stats_add(c->st, n, 0);
|
||||
if (c->b_kind == B_DGRAM) {
|
||||
if (c->state == CONN_RELAY)
|
||||
send(c->b.fd, p, n, MSG_DONTWAIT);
|
||||
if (c->state == CONN_RELAY) {
|
||||
// A PROXY header goes in front of every datagram.
|
||||
struct iovec iov[2] = { { c->pp, c->pplen }, { (void *)p, n } };
|
||||
struct msghdr mh = { .msg_iov = iov + !c->pplen, .msg_iovlen = c->pplen ? 2 : 1 };
|
||||
sendmsg(c->b.fd, &mh, MSG_DONTWAIT);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Like a real UDP path, drop when the tunnel cannot keep up.
|
||||
@@ -760,6 +789,11 @@ static void conn_start(struct conn *c)
|
||||
conn_kill(c);
|
||||
return;
|
||||
}
|
||||
// Fresh socket with an empty send buffer: the header goes out in one write.
|
||||
if (c->pplen && write(c->b.fd, c->pp, c->pplen) != (ssize_t)c->pplen) {
|
||||
conn_kill(c);
|
||||
return;
|
||||
}
|
||||
conn_relay(c);
|
||||
}
|
||||
|
||||
@@ -781,18 +815,30 @@ static void conn_attach_channel(struct conn *c, int fd)
|
||||
// the source is a tunnel source of the target.
|
||||
static int conn_request_source(struct conn *c, struct route *r)
|
||||
{
|
||||
int origin = c->has_addr ? c->origin : ORIGIN_NONE;
|
||||
if (r->src_client) {
|
||||
struct ctl *src = ctl_by_client(r->src_client);
|
||||
if (!src)
|
||||
return -1;
|
||||
conn_set_mode(c);
|
||||
ctl_send(src, "OPEN %d %s %s %d %s", c->id, proto_name(r->proto), r->src_host, r->src_port,
|
||||
iface_field(r->src_iface));
|
||||
if (origin == ORIGIN_NONE) {
|
||||
ctl_send(src, "OPEN %d %s %s %d %s", c->id, proto_name(r->proto), r->src_host, r->src_port,
|
||||
iface_field(r->src_iface));
|
||||
} else {
|
||||
char peer[64], local[64];
|
||||
sockaddr_str((struct sockaddr *)&c->o_peer, peer, sizeof(peer));
|
||||
sockaddr_str((struct sockaddr *)&c->o_local, local, sizeof(local));
|
||||
ctl_send(src, "OPEN %d %s %s %d %s %s %s %s", c->id, proto_name(r->proto), r->src_host, r->src_port,
|
||||
iface_field(r->src_iface), origin_name(origin), peer, local);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Spoofing is client-only (route error), so only PROXY applies here.
|
||||
if (origin == ORIGIN_PROXY)
|
||||
c->pplen = proxy_v2_header(c->pp, c->proto, (struct sockaddr *)&c->o_peer, (struct sockaddr *)&c->o_local);
|
||||
int inprog = 0;
|
||||
int fd = net_connect(r->src_host, r->src_port, r->proto, r->src_iface, &inprog);
|
||||
int fd = net_connect(r->src_host, r->src_port, r->proto, r->src_iface, NULL, &inprog);
|
||||
if (fd < 0) {
|
||||
log_debug("sink %d: connect %s:%d failed: %s", r->sink_id, r->src_host, r->src_port, strerror(errno));
|
||||
return -1;
|
||||
@@ -824,10 +870,26 @@ static void conn_connected(struct conn *c)
|
||||
|
||||
/* Target sinks */
|
||||
|
||||
// Records the addresses a sink saw for its origin mode.
|
||||
static void conn_set_addr(struct conn *c, const struct sockaddr_storage *peer, int local_fd)
|
||||
{
|
||||
if (c->origin == ORIGIN_NONE)
|
||||
return;
|
||||
socklen_t ll = sizeof(c->o_local);
|
||||
if (getsockname(local_fd, (struct sockaddr *)&c->o_local, &ll) < 0)
|
||||
return;
|
||||
c->o_peer = *peer;
|
||||
sockaddr_unmap(&c->o_peer);
|
||||
sockaddr_unmap(&c->o_local);
|
||||
c->has_addr = 1;
|
||||
}
|
||||
|
||||
static void tsink_accept_tcp(struct tsink *ts)
|
||||
{
|
||||
for (;;) {
|
||||
int fd = accept4(ts->ev.fd, NULL, NULL, SOCK_NONBLOCK | SOCK_CLOEXEC);
|
||||
struct sockaddr_storage peer;
|
||||
socklen_t plen = sizeof(peer);
|
||||
int fd = accept4(ts->ev.fd, (struct sockaddr *)&peer, &plen, SOCK_NONBLOCK | SOCK_CLOEXEC);
|
||||
if (fd < 0)
|
||||
return;
|
||||
struct route *r = route_find(ts->sink_id);
|
||||
@@ -842,6 +904,7 @@ static void tsink_accept_tcp(struct tsink *ts)
|
||||
continue;
|
||||
}
|
||||
ev_init(&c->a, EV_CONN_A, fd, c);
|
||||
conn_set_addr(c, &peer, fd);
|
||||
if (conn_request_source(c, r) < 0) {
|
||||
log_debug("sink %d: source unavailable", ts->sink_id);
|
||||
conn_kill(c);
|
||||
@@ -875,6 +938,8 @@ static void tsink_recv_udp(struct tsink *ts)
|
||||
c->ts = ts;
|
||||
c->peer = peer;
|
||||
c->peerlen = plen;
|
||||
// Local address of the listener; 0.0.0.0 when bound to any.
|
||||
conn_set_addr(c, &peer, ts->ev.fd);
|
||||
if (conn_request_source(c, r) < 0) {
|
||||
conn_kill(c);
|
||||
continue;
|
||||
@@ -890,7 +955,7 @@ static void tsink_recv_udp(struct tsink *ts)
|
||||
|
||||
static void pend_read(struct pend *p)
|
||||
{
|
||||
char hdr[160];
|
||||
char hdr[256];
|
||||
ssize_t n = recv(p->ev.fd, hdr, sizeof(hdr) - 1, MSG_PEEK);
|
||||
if (n == 0 || (n < 0 && errno != EAGAIN)) {
|
||||
p->dead = 1;
|
||||
@@ -915,7 +980,7 @@ static void pend_read(struct pend *p)
|
||||
|
||||
char *f[PB_MAX_FIELDS];
|
||||
int nf = line_split(hdr, f, PB_MAX_FIELDS);
|
||||
struct ctl *owner = nf == 4 && !strcmp(f[0], "PB1") ? ctl_by_token(f[1]) : NULL;
|
||||
struct ctl *owner = (nf == 4 || nf == 6) && !strcmp(f[0], "PB1") ? ctl_by_token(f[1]) : NULL;
|
||||
p->dead = 1; // the fd is handed over or closed below
|
||||
if (!owner) {
|
||||
log_warn("data channel with invalid header rejected");
|
||||
@@ -949,6 +1014,10 @@ static void pend_read(struct pend *p)
|
||||
return;
|
||||
}
|
||||
ev_init(&c->a, EV_CONN_A, fd, c);
|
||||
// Addresses the client sink saw; without them the origin is dropped.
|
||||
if (nf == 6 && c->origin != ORIGIN_NONE && sockaddr_parse(f[4], &c->o_peer) == 0 &&
|
||||
sockaddr_parse(f[5], &c->o_local) == 0)
|
||||
c->has_addr = 1;
|
||||
if (conn_request_source(c, r) < 0) {
|
||||
conn_kill(c);
|
||||
return;
|
||||
@@ -962,7 +1031,7 @@ static void pend_read(struct pend *p)
|
||||
/* Patch graph */
|
||||
|
||||
struct gnode {
|
||||
int id, type, client_id, port, proto, input;
|
||||
int id, type, client_id, port, proto, input, origin;
|
||||
char host[256];
|
||||
char iface[IFNAMSIZ];
|
||||
};
|
||||
@@ -1016,6 +1085,8 @@ static void route_resolve(struct route *r, struct gnode *sink, struct gnode *g,
|
||||
snprintf(r->err, sizeof(r->err), "tunnel source has no interface");
|
||||
else if (tunnel && host_empty(cur->host))
|
||||
snprintf(r->err, sizeof(r->err), "tunnel source has no peer address");
|
||||
else if (r->origin == ORIGIN_SPOOF && !cur->client_id)
|
||||
snprintf(r->err, sizeof(r->err), "transparent spoofing needs a source on a client");
|
||||
else {
|
||||
r->src_client = cur->client_id;
|
||||
snprintf(r->src_host, sizeof(r->src_host), "%s", host_empty(cur->host) ? "127.0.0.1" : cur->host);
|
||||
@@ -1038,7 +1109,7 @@ static int load_graph(void)
|
||||
int n = 0, cap = 0;
|
||||
sqlite3_stmt *st;
|
||||
const char *sql = "SELECT n.id, n.type, IFNULL(n.client_id, 0), n.host, n.port, n.proto, n.iface, "
|
||||
"IFNULL((SELECT from_node FROM links WHERE to_node = n.id), 0) FROM nodes n";
|
||||
"IFNULL((SELECT from_node FROM links WHERE to_node = n.id), 0), n.origin FROM nodes n";
|
||||
if (sqlite3_prepare_v2(db, sql, -1, &st, NULL) != SQLITE_OK) {
|
||||
log_err("graph: %s", sqlite3_errmsg(db));
|
||||
return -1;
|
||||
@@ -1058,6 +1129,7 @@ static int load_graph(void)
|
||||
if (x->iface[0] && !iface_valid(x->iface))
|
||||
x->iface[0] = '\0';
|
||||
x->input = sqlite3_column_int(st, 7);
|
||||
x->origin = origin_parse((const char *)sqlite3_column_text(st, 8));
|
||||
}
|
||||
sqlite3_finalize(st);
|
||||
|
||||
@@ -1073,6 +1145,7 @@ static int load_graph(void)
|
||||
r->sink_client = t == NODE_PUBLIC_SINK ? 0 : g[i].client_id;
|
||||
r->proto = g[i].proto;
|
||||
r->port = g[i].port;
|
||||
r->origin = g[i].origin;
|
||||
if (t == NODE_TUNNEL_SINK) {
|
||||
// Bound to the interface, so any local address of it is accepted.
|
||||
snprintf(r->bind, sizeof(r->bind), "0.0.0.0");
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
#include <netinet/in.h>
|
||||
#include <net/if.h>
|
||||
#include <netinet/tcp.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
#include <sys/stat.h>
|
||||
@@ -14,6 +16,13 @@
|
||||
|
||||
#include "net.h"
|
||||
|
||||
#ifndef IP_TRANSPARENT
|
||||
#define IP_TRANSPARENT 19
|
||||
#endif
|
||||
#ifndef IPV6_TRANSPARENT
|
||||
#define IPV6_TRANSPARENT 75
|
||||
#endif
|
||||
|
||||
int proto_parse(const char *s)
|
||||
{
|
||||
if (!strcasecmp(s, "tcp"))
|
||||
@@ -109,21 +118,52 @@ int net_listen(const char *addr, int port, int proto, const char *iface)
|
||||
return fd;
|
||||
}
|
||||
|
||||
int net_connect(const char *host, int port, int proto, const char *iface, int *in_progress)
|
||||
static socklen_t sa_len(const struct sockaddr *sa)
|
||||
{
|
||||
return sa->sa_family == AF_INET6 ? sizeof(struct sockaddr_in6) : sizeof(struct sockaddr_in);
|
||||
}
|
||||
|
||||
// Binds fd to a foreign address. The peer's own port is preferred so the
|
||||
// service logs it; it is taken when the same peer arrives through two sinks.
|
||||
static int bind_transparent(int fd, const struct sockaddr *src)
|
||||
{
|
||||
int one = 1;
|
||||
int v6 = src->sa_family == AF_INET6;
|
||||
if (setsockopt(fd, v6 ? IPPROTO_IPV6 : IPPROTO_IP, v6 ? IPV6_TRANSPARENT : IP_TRANSPARENT, &one,
|
||||
sizeof(one)) < 0)
|
||||
return -1;
|
||||
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
|
||||
if (bind(fd, src, sa_len(src)) == 0)
|
||||
return 0;
|
||||
if (errno != EADDRINUSE)
|
||||
return -1;
|
||||
struct sockaddr_storage any;
|
||||
memcpy(&any, src, sa_len(src));
|
||||
if (v6)
|
||||
((struct sockaddr_in6 *)&any)->sin6_port = 0;
|
||||
else
|
||||
((struct sockaddr_in *)&any)->sin_port = 0;
|
||||
return bind(fd, (struct sockaddr *)&any, sa_len(src));
|
||||
}
|
||||
|
||||
int net_connect(const char *host, int port, int proto, const char *iface, const struct sockaddr *spoof,
|
||||
int *in_progress)
|
||||
{
|
||||
struct addrinfo *res;
|
||||
*in_progress = 0;
|
||||
if (resolve(host, port, proto, 0, &res) < 0)
|
||||
return -1;
|
||||
|
||||
int fd = -1, err = 0;
|
||||
int fd = -1, err = spoof ? EAFNOSUPPORT : 0;
|
||||
for (struct addrinfo *ai = res; ai; ai = ai->ai_next) {
|
||||
if (spoof && ai->ai_family != spoof->sa_family)
|
||||
continue;
|
||||
fd = socket(ai->ai_family, ai->ai_socktype | SOCK_CLOEXEC | SOCK_NONBLOCK, 0);
|
||||
if (fd < 0) {
|
||||
err = errno;
|
||||
continue;
|
||||
}
|
||||
if (bind_device(fd, iface) < 0) {
|
||||
if (bind_device(fd, iface) < 0 || (spoof && bind_transparent(fd, spoof) < 0)) {
|
||||
err = errno;
|
||||
close(fd);
|
||||
fd = -1;
|
||||
@@ -214,3 +254,106 @@ void sockaddr_str(const struct sockaddr *sa, char *out, size_t outsz)
|
||||
snprintf(out, outsz, "?");
|
||||
}
|
||||
}
|
||||
|
||||
int sockaddr_parse(const char *s, struct sockaddr_storage *ss)
|
||||
{
|
||||
char host[INET6_ADDRSTRLEN];
|
||||
const char *colon;
|
||||
size_t hl;
|
||||
memset(ss, 0, sizeof(*ss));
|
||||
if (*s == '[') {
|
||||
const char *end = strchr(s, ']');
|
||||
if (!end || end[1] != ':')
|
||||
return -1;
|
||||
s++;
|
||||
hl = (size_t)(end - s);
|
||||
colon = end + 1;
|
||||
} else {
|
||||
colon = strrchr(s, ':');
|
||||
if (!colon)
|
||||
return -1;
|
||||
hl = (size_t)(colon - s);
|
||||
}
|
||||
if (hl == 0 || hl >= sizeof(host))
|
||||
return -1;
|
||||
memcpy(host, s, hl);
|
||||
host[hl] = '\0';
|
||||
char *e;
|
||||
long port = strtol(colon + 1, &e, 10);
|
||||
if (*e || e == colon + 1 || port < 0 || port > 65535)
|
||||
return -1;
|
||||
|
||||
struct sockaddr_in *s4 = (struct sockaddr_in *)ss;
|
||||
struct sockaddr_in6 *s6 = (struct sockaddr_in6 *)ss;
|
||||
if (inet_pton(AF_INET, host, &s4->sin_addr) == 1) {
|
||||
s4->sin_family = AF_INET;
|
||||
s4->sin_port = htons((uint16_t)port);
|
||||
} else if (inet_pton(AF_INET6, host, &s6->sin6_addr) == 1) {
|
||||
s6->sin6_family = AF_INET6;
|
||||
s6->sin6_port = htons((uint16_t)port);
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void sockaddr_unmap(struct sockaddr_storage *ss)
|
||||
{
|
||||
struct sockaddr_in6 *s6 = (struct sockaddr_in6 *)ss;
|
||||
if (ss->ss_family != AF_INET6 || !IN6_IS_ADDR_V4MAPPED(&s6->sin6_addr))
|
||||
return;
|
||||
struct sockaddr_in s4;
|
||||
memset(&s4, 0, sizeof(s4));
|
||||
s4.sin_family = AF_INET;
|
||||
s4.sin_port = s6->sin6_port;
|
||||
memcpy(&s4.sin_addr, &s6->sin6_addr.s6_addr[12], 4);
|
||||
memset(ss, 0, sizeof(*ss));
|
||||
memcpy(ss, &s4, sizeof(s4));
|
||||
}
|
||||
|
||||
/* PROXY protocol v2 */
|
||||
|
||||
static void put_v6(unsigned char *out, const struct sockaddr *sa)
|
||||
{
|
||||
if (sa->sa_family == AF_INET6) {
|
||||
memcpy(out, &((const struct sockaddr_in6 *)sa)->sin6_addr, 16);
|
||||
return;
|
||||
}
|
||||
memset(out, 0, 10);
|
||||
out[10] = out[11] = 0xff;
|
||||
memcpy(out + 12, &((const struct sockaddr_in *)sa)->sin_addr, 4);
|
||||
}
|
||||
|
||||
static uint16_t sa_port(const struct sockaddr *sa)
|
||||
{
|
||||
// Already network byte order.
|
||||
return sa->sa_family == AF_INET6 ? ((const struct sockaddr_in6 *)sa)->sin6_port
|
||||
: ((const struct sockaddr_in *)sa)->sin_port;
|
||||
}
|
||||
|
||||
size_t proxy_v2_header(unsigned char *out, int proto, const struct sockaddr *src, const struct sockaddr *dst)
|
||||
{
|
||||
static const unsigned char sig[12] = { 0x0d, 0x0a, 0x0d, 0x0a, 0x00, 0x0d, 0x0a, 0x51, 0x55, 0x49, 0x54, 0x0a };
|
||||
int v4 = src->sa_family == AF_INET && dst->sa_family == AF_INET;
|
||||
size_t alen = v4 ? 12 : 36;
|
||||
unsigned char *a = out + 16;
|
||||
uint16_t sp = sa_port(src), dp = sa_port(dst);
|
||||
|
||||
memcpy(out, sig, sizeof(sig));
|
||||
out[12] = 0x21; // version 2, PROXY
|
||||
out[13] = (unsigned char)((v4 ? 0x10 : 0x20) | (proto == PROTO_UDP ? 0x02 : 0x01));
|
||||
out[14] = 0;
|
||||
out[15] = (unsigned char)alen;
|
||||
if (v4) {
|
||||
memcpy(a, &((const struct sockaddr_in *)src)->sin_addr, 4);
|
||||
memcpy(a + 4, &((const struct sockaddr_in *)dst)->sin_addr, 4);
|
||||
a += 8;
|
||||
} else {
|
||||
put_v6(a, src);
|
||||
put_v6(a + 16, dst);
|
||||
a += 32;
|
||||
}
|
||||
memcpy(a, &sp, 2);
|
||||
memcpy(a + 2, &dp, 2);
|
||||
return 16 + alen;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifndef PB_NET_H
|
||||
#define PB_NET_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <sys/socket.h>
|
||||
|
||||
enum pb_proto { PROTO_TCP, PROTO_UDP };
|
||||
@@ -15,8 +16,11 @@ void tune_stream(int fd);
|
||||
// socket to one interface with SO_BINDTODEVICE. Returns fd or -1 with errno.
|
||||
int net_listen(const char *addr, int port, int proto, const char *iface);
|
||||
// Starts a non-blocking connect, optionally pinned to iface. Returns fd or -1;
|
||||
// *in_progress set if pending.
|
||||
int net_connect(const char *host, int port, int proto, const char *iface, int *in_progress);
|
||||
// *in_progress set if pending. spoof (may be NULL) is a foreign source address
|
||||
// bound with IP_TRANSPARENT (needs CAP_NET_ADMIN); only service addresses of
|
||||
// the same family are tried, EAFNOSUPPORT if there are none.
|
||||
int net_connect(const char *host, int port, int proto, const char *iface, const struct sockaddr *spoof,
|
||||
int *in_progress);
|
||||
int net_listen_unix(const char *path, int mode);
|
||||
int net_connect_unix(const char *path);
|
||||
|
||||
@@ -25,5 +29,17 @@ int iface_valid(const char *iface);
|
||||
|
||||
// Formats a sockaddr as "addr:port" (IPv6 in brackets).
|
||||
void sockaddr_str(const struct sockaddr *sa, char *out, size_t outsz);
|
||||
// Parses the sockaddr_str format back. Returns 0 or -1.
|
||||
int sockaddr_parse(const char *s, struct sockaddr_storage *ss);
|
||||
// Turns a v4-mapped IPv6 address into plain IPv4 in place.
|
||||
void sockaddr_unmap(struct sockaddr_storage *ss);
|
||||
|
||||
/* PROXY protocol v2 */
|
||||
|
||||
#define PROXY_V2_MAX 52 // 16 byte header + IPv6 address block
|
||||
|
||||
// Builds a PROXY v2 header (command PROXY) for a connection from src to dst.
|
||||
// Mixed families are sent as IPv6 with a v4-mapped address. Returns length.
|
||||
size_t proxy_v2_header(unsigned char *out, int proto, const struct sockaddr *src, const struct sockaddr *dst);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
* (patchbayd --relay) bridges it to the hub socket on the target.
|
||||
*
|
||||
* hub -> client: HELLO <token> <hub_port>
|
||||
* SINKS-BEGIN / SINK <sink_id> <proto> <bind> <port> <iface|-> / SINKS-END
|
||||
* OPEN <conn_id> <proto> <host> <port> <iface|->
|
||||
* SINKS-BEGIN / SINK <sink_id> <proto> <bind> <port> <iface|-> [<origin>] / SINKS-END
|
||||
* OPEN <conn_id> <proto> <host> <port> <iface|-> [<origin> <peer> <local>]
|
||||
* SVC-REQ, PING
|
||||
* client -> hub: HELLO <version> <hostname>
|
||||
* SVC-BEGIN / SVC <proto> <addr> <port> <pid> <process> / SVC-END
|
||||
@@ -22,16 +22,22 @@
|
||||
*
|
||||
* <iface> pins a socket to an interface (tunnel nodes, SO_BINDTODEVICE).
|
||||
*
|
||||
* <origin> says how the connecting peer's address reaches the service: "proxy"
|
||||
* (PROXY v2 header in front of the stream / every UDP datagram) or "spoof"
|
||||
* (connect from the peer's address with IP_TRANSPARENT). <peer> and <local>
|
||||
* are the addresses the sink saw, "addr:port" with IPv6 in brackets.
|
||||
*
|
||||
* Data channels are direct-tcpip channels to 127.0.0.1:HubPort on the target.
|
||||
* They start with "PB1 <token> SINK <sink_id>\n" (a client sink accepted a
|
||||
* connection) or "PB1 <token> OPEN <conn_id>\n" (answer to OPEN), followed by
|
||||
* raw stream bytes. UDP flows use length-prefixed frames, see udp_frame_*.
|
||||
* They start with "PB1 <token> SINK <sink_id> [<peer> <local>]\n" (a client
|
||||
* sink accepted a connection; the addresses only for sinks with an origin) or
|
||||
* "PB1 <token> OPEN <conn_id>\n" (answer to OPEN), followed by raw stream
|
||||
* bytes. UDP flows use length-prefixed frames, see udp_frame_*.
|
||||
*/
|
||||
|
||||
#define PB_VERSION "0.1.0"
|
||||
#define PB_TOKEN_LEN 32 // hex chars
|
||||
#define PB_MAX_LINE 1024
|
||||
#define PB_MAX_FIELDS 8
|
||||
#define PB_MAX_FIELDS 12
|
||||
#define PB_UDP_MAX 65507
|
||||
|
||||
/* Byte buffer */
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* Minimal unit test runner: ./test_runner [test_name ...] */
|
||||
|
||||
#include <netinet/in.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -112,6 +113,49 @@ static void test_token(void)
|
||||
CHECK(random_token(b, sizeof(b)) == 0 && strcmp(a, b));
|
||||
}
|
||||
|
||||
static void test_sockaddr(void)
|
||||
{
|
||||
struct sockaddr_storage ss;
|
||||
char out[64];
|
||||
CHECK(sockaddr_parse("1.2.3.4:5678", &ss) == 0 && ss.ss_family == AF_INET);
|
||||
sockaddr_str((struct sockaddr *)&ss, out, sizeof(out));
|
||||
CHECK(!strcmp(out, "1.2.3.4:5678"));
|
||||
CHECK(sockaddr_parse("[2001:db8::1]:443", &ss) == 0 && ss.ss_family == AF_INET6);
|
||||
sockaddr_str((struct sockaddr *)&ss, out, sizeof(out));
|
||||
CHECK(!strcmp(out, "[2001:db8::1]:443"));
|
||||
CHECK(sockaddr_parse("[::ffff:10.0.0.1]:80", &ss) == 0);
|
||||
sockaddr_unmap(&ss);
|
||||
sockaddr_str((struct sockaddr *)&ss, out, sizeof(out));
|
||||
CHECK(ss.ss_family == AF_INET && !strcmp(out, "10.0.0.1:80"));
|
||||
CHECK(sockaddr_parse("1.2.3.4", &ss) == -1);
|
||||
CHECK(sockaddr_parse("1.2.3.4:99999", &ss) == -1);
|
||||
CHECK(sockaddr_parse("host:80", &ss) == -1);
|
||||
CHECK(sockaddr_parse("[::1:80", &ss) == -1);
|
||||
}
|
||||
|
||||
static void test_proxy_v2(void)
|
||||
{
|
||||
static const unsigned char want4[] = {
|
||||
0x0d, 0x0a, 0x0d, 0x0a, 0x00, 0x0d, 0x0a, 0x51, 0x55, 0x49, 0x54, 0x0a,
|
||||
0x21, 0x11, 0x00, 0x0c,
|
||||
1, 2, 3, 4, 10, 0, 0, 5, 0x16, 0x2e, 0x01, 0xbb,
|
||||
};
|
||||
struct sockaddr_storage src, dst;
|
||||
unsigned char out[PROXY_V2_MAX];
|
||||
sockaddr_parse("1.2.3.4:5678", &src);
|
||||
sockaddr_parse("10.0.0.5:443", &dst);
|
||||
size_t n = proxy_v2_header(out, PROTO_TCP, (struct sockaddr *)&src, (struct sockaddr *)&dst);
|
||||
CHECK(n == sizeof(want4) && !memcmp(out, want4, n));
|
||||
|
||||
// Mixed families become IPv6 with a v4-mapped address; UDP sets DGRAM.
|
||||
sockaddr_parse("[2001:db8::1]:1000", &dst);
|
||||
n = proxy_v2_header(out, PROTO_UDP, (struct sockaddr *)&src, (struct sockaddr *)&dst);
|
||||
static const unsigned char mapped[16] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 1, 2, 3, 4 };
|
||||
CHECK(n == 52 && out[13] == 0x22 && out[15] == 36);
|
||||
CHECK(!memcmp(out + 16, mapped, 16) && out[32] == 0x20 && out[33] == 0x01);
|
||||
CHECK(out[48] == 0x16 && out[49] == 0x2e && out[50] == 0x03 && out[51] == 0xe8);
|
||||
}
|
||||
|
||||
/* Runner */
|
||||
|
||||
static const struct { const char *name; void (*fn)(void); } tests[] = {
|
||||
@@ -121,6 +165,8 @@ static const struct { const char *name; void (*fn)(void); } tests[] = {
|
||||
{ "services_parse", test_services_parse },
|
||||
{ "json", test_json },
|
||||
{ "token", test_token },
|
||||
{ "sockaddr", test_sockaddr },
|
||||
{ "proxy_v2", test_proxy_v2 },
|
||||
};
|
||||
|
||||
int main(int argc, char **argv)
|
||||
|
||||
Reference in New Issue
Block a user