Add Transparent Proxy and PROXY v2 support and help page

This commit is contained in:
mueller_minki
2026-10-04 19:21:50 +02:00
parent aebb7206f4
commit b7481c2109
24 changed files with 864 additions and 70 deletions

View File

@@ -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;
}

View File

@@ -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")) {

View File

@@ -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)

View File

@@ -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;
}

View File

@@ -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");

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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 */

View File

@@ -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)