mirror of
https://codeberg.org/scip/udpxd.git
synced 2025-12-16 11:30:57 +01:00
441 lines
10 KiB
C
441 lines
10 KiB
C
/*
|
|
This file is part of udpxd.
|
|
|
|
Copyright (C) 2015-2016 T.v.Dein.
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
You can contact me by mail: <tom AT vondein DOT org>.
|
|
*/
|
|
|
|
#include "net.h"
|
|
#include "client.h"
|
|
#include "host.h"
|
|
#include "log.h"
|
|
|
|
|
|
|
|
/* called each time when the loop restarts to feed select() correctly */
|
|
int fill_set(fd_set *fds) {
|
|
int max = 0;
|
|
|
|
client_t *current = NULL;
|
|
client_iter(clients, current) {
|
|
if (current->socket < (int)FD_SETSIZE) {
|
|
if (current->socket > max)
|
|
max = current->socket;
|
|
FD_SET(current->socket, fds);
|
|
}
|
|
else {
|
|
fprintf(stderr, "skipped client, socket too large!\n");
|
|
}
|
|
}
|
|
|
|
return max;
|
|
}
|
|
|
|
/* return file handle ready to read */
|
|
int get_sender(fd_set *fds) {
|
|
int i = 0;
|
|
|
|
while(!FD_ISSET(i, fds))
|
|
i++;
|
|
|
|
return i;
|
|
}
|
|
|
|
|
|
/* bind to a socket, either for listen() or for outgoing src ip binding */
|
|
int bindsocket( host_t *sock_h) {
|
|
int fd;
|
|
int err = 0;
|
|
|
|
if(sock_h->is_v6) {
|
|
fd = socket( PF_INET6, SOCK_DGRAM, IPPROTO_UDP );
|
|
}
|
|
else {
|
|
fd = socket( PF_INET, SOCK_DGRAM, IPPROTO_UDP );
|
|
}
|
|
|
|
if( -1 == bind( fd, (struct sockaddr*)sock_h->sock, sock_h->size ) ) {
|
|
err = 1;
|
|
}
|
|
|
|
if(err) {
|
|
fprintf( stderr, "Cannot bind address ([%s]:%d)\n", sock_h->ip, sock_h->port );
|
|
perror(NULL);
|
|
return -1;
|
|
}
|
|
|
|
return fd;
|
|
}
|
|
|
|
/*
|
|
returns:
|
|
-1: error in any case
|
|
0: parent not forked (fork disabled)
|
|
1: parent after successful fork
|
|
2: child after successful fork
|
|
*/
|
|
int daemonize(char *pidfile) {
|
|
if(FORKED) {
|
|
// fork
|
|
pid_t pid, sid;
|
|
FILE *fd;
|
|
|
|
pid = fork();
|
|
|
|
if (pid < 0) {
|
|
perror("fork error");
|
|
return 1;
|
|
}
|
|
|
|
if (pid > 0) {
|
|
/* leave parent */
|
|
if((fd = fopen(pidfile, "w")) == NULL) {
|
|
perror("failed to write pidfile");
|
|
return -1;
|
|
}
|
|
else {
|
|
fprintf(fd, "%d\n", pid);
|
|
fclose(fd);
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
/* child */
|
|
|
|
sid = setsid();
|
|
if (sid < 0) {
|
|
perror("set sid error");
|
|
return 1;
|
|
}
|
|
|
|
umask(0);
|
|
|
|
openlog("udpxd", LOG_NOWAIT|LOG_PID, LOG_USER);
|
|
|
|
return 2;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int drop_privileges(char *user, char *chrootdir) {
|
|
struct passwd *pw = getpwnam(user);
|
|
uid_t me = getuid();
|
|
|
|
if(!FORKED)
|
|
return 0;
|
|
|
|
if ((chdir("/")) < 0) {
|
|
perror("failed to chdir to /");
|
|
return 1;
|
|
}
|
|
|
|
if(me == 0) {
|
|
/* drop privileges */
|
|
if(chroot(chrootdir) != 0) {
|
|
perror("failed to chroot");
|
|
return 1;
|
|
}
|
|
|
|
if(pw == NULL) {
|
|
perror("user not found");
|
|
return 1;
|
|
}
|
|
|
|
if(setegid(pw->pw_gid) != 0) {
|
|
perror("could not set egid");
|
|
return 1;
|
|
}
|
|
|
|
if(setgid(pw->pw_gid) != 0) {
|
|
perror("could not set gid");
|
|
return 1;
|
|
}
|
|
|
|
if(setuid(pw->pw_uid) != 0) {
|
|
perror("could not set uid");
|
|
return 1;
|
|
}
|
|
|
|
if(seteuid(pw->pw_uid) != 0) {
|
|
perror("could not set euid");
|
|
return 1;
|
|
}
|
|
|
|
if (setuid(0) != -1) {
|
|
fprintf(stderr, "error, managed to regain root privileges after dropping them!\n");
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int start_listener (char *inip, char *inpt, char *srcip, char *dstip,
|
|
char *dstpt, char *pidfile, char *chrootdir, char *user) {
|
|
host_t *listen_h, *dst_h, *bind_h;
|
|
|
|
int dm = daemonize(pidfile);
|
|
switch(dm) {
|
|
case -1:
|
|
return 1; /* parent, fork error */
|
|
break;
|
|
case 0:
|
|
break; /* parent, not forking */
|
|
case 1:
|
|
return 0; /* parent, fork ok, leave */
|
|
break;
|
|
case 2:
|
|
break; /* child, fork ok, continue */
|
|
}
|
|
|
|
listen_h = get_host(inip, atoi(inpt), NULL, NULL);
|
|
dst_h = get_host(dstip, atoi(dstpt), NULL, NULL);
|
|
bind_h = NULL;
|
|
|
|
if(srcip != NULL) {
|
|
bind_h = get_host(srcip, 0, NULL, NULL);
|
|
}
|
|
else {
|
|
if(dst_h->is_v6)
|
|
bind_h = get_host("::0", 0, NULL, NULL);
|
|
else
|
|
bind_h = get_host("0.0.0.0", 0, NULL, NULL);
|
|
}
|
|
|
|
int listen = bindsocket(listen_h);
|
|
|
|
if(listen == -1)
|
|
return 1;
|
|
|
|
if(VERBOSE) {
|
|
verbose("Listening on %s:%s, forwarding to %s:%s",
|
|
listen_h->ip, inpt, dst_h->ip, dstpt);
|
|
if(srcip != NULL)
|
|
verbose(", binding to %s\n", bind_h->ip);
|
|
else
|
|
verbose("\n");
|
|
}
|
|
|
|
if(drop_privileges(user, chrootdir) != 0) {
|
|
host_clean(bind_h);
|
|
host_clean(listen_h);
|
|
host_clean(dst_h);
|
|
return 1;
|
|
}
|
|
|
|
close(STDIN_FILENO);
|
|
close(STDOUT_FILENO);
|
|
close(STDERR_FILENO);
|
|
|
|
main_loop(listen, listen_h, bind_h, dst_h);
|
|
|
|
host_clean(bind_h);
|
|
host_clean(listen_h);
|
|
host_clean(dst_h);
|
|
|
|
closelog();
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* handle new or known incoming requests */
|
|
void handle_inside(int inside, host_t *listen_h, host_t *bind_h, host_t *dst_h) {
|
|
int len;
|
|
unsigned char buffer[MAX_BUFFER_SIZE];
|
|
void *src;
|
|
client_t *client;
|
|
host_t *src_h;
|
|
int output;
|
|
size_t size = listen_h->size;
|
|
|
|
src = malloc(size);
|
|
|
|
len = recvfrom( inside, buffer, sizeof( buffer ), 0,
|
|
(struct sockaddr*)src, (socklen_t *)&size );
|
|
|
|
if(listen_h->is_v6)
|
|
src_h = get_host(NULL, 0, NULL, (struct sockaddr_in6 *)src);
|
|
else
|
|
src_h = get_host(NULL, 0, (struct sockaddr_in *)src, NULL);
|
|
|
|
free(src);
|
|
|
|
if(len > 0) {
|
|
/* do we know it ? */
|
|
client = client_find_src(src_h);
|
|
if(client != NULL) {
|
|
/* yes, we know it, send req out via existing bind socket */
|
|
verbose("Client %s:%d is known, forwarding %d bytes to %s:%d ",
|
|
src_h->ip, src_h->port, len, dst_h->ip, dst_h->port);
|
|
verb_prbind(bind_h);
|
|
|
|
if(sendto(client->socket, buffer, len, 0, (struct sockaddr*)dst_h->sock, dst_h->size) < 0) {
|
|
fprintf(stderr, "unable to forward to %s:%d\n", dst_h->ip, dst_h->port);
|
|
perror(NULL);
|
|
}
|
|
else {
|
|
client_seen(client);
|
|
}
|
|
}
|
|
else {
|
|
/* unknown client, open new out socket */
|
|
verbose("Client %s:%d is unknown, forwarding %d bytes to %s:%d ",
|
|
src_h->ip, src_h->port, len, dst_h->ip, dst_h->port);
|
|
verb_prbind(bind_h);
|
|
|
|
output = bindsocket(bind_h);
|
|
|
|
/* send req out */
|
|
if(sendto(output, buffer, len, 0, (struct sockaddr*)dst_h->sock, dst_h->size) < 0) {
|
|
fprintf(stderr, "unable to forward to %s:%d\n", dst_h->ip, dst_h->port);
|
|
perror(NULL);
|
|
}
|
|
else {
|
|
size = listen_h->size;
|
|
host_t *ret_h;
|
|
if(listen_h->is_v6) {
|
|
struct sockaddr_in6 *ret = malloc(size);
|
|
getsockname(output, (struct sockaddr*)ret, (socklen_t *)&size);
|
|
ret_h = get_host(NULL, 0, NULL, ret);
|
|
free(ret);
|
|
client = client_new(output, src_h, ret_h);
|
|
}
|
|
else {
|
|
struct sockaddr_in *ret = malloc(size);
|
|
getsockname(output, (struct sockaddr*)ret, (socklen_t *)&size);
|
|
ret_h = get_host(NULL, 0, ret, NULL);
|
|
free(ret);
|
|
client = client_new(output, src_h, ret_h);
|
|
}
|
|
|
|
client_add(client);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* handle answer from the outside */
|
|
void handle_outside(int inside, int outside, host_t *outside_h) {
|
|
int len;
|
|
unsigned char buffer[MAX_BUFFER_SIZE];
|
|
void *src;
|
|
client_t *client;
|
|
|
|
size_t size = outside_h->size;
|
|
src = malloc(size);
|
|
|
|
len = recvfrom( outside, buffer, sizeof( buffer ), 0, (struct sockaddr*)src, (socklen_t *)&size );
|
|
free(src);
|
|
|
|
if(len > 0) {
|
|
/* do we know it? */
|
|
client = client_find_fd(outside);
|
|
if(client != NULL) {
|
|
/* yes, we know it */
|
|
/* FIXME: check src vs. client->src ? */
|
|
if(sendto(inside, buffer, len, 0,
|
|
(struct sockaddr*)client->src->sock, client->src->size) < 0) {
|
|
perror("unable to send back to client"); /* FIXME: add src+port */
|
|
client_close(client);
|
|
}
|
|
}
|
|
else {
|
|
fprintf(stderr, "weird, no matching client found!\n");
|
|
}
|
|
}
|
|
else {
|
|
fprintf(stderr, "weird, recvfrom returned 0 bytes!\n");
|
|
}
|
|
}
|
|
|
|
/* stores system specific information, used by longjmp(), see below */
|
|
jmp_buf JumpBuffer;
|
|
|
|
/* runs forever, handles incoming requests on the inside and answers on the outside */
|
|
int main_loop(int listensocket, host_t *listen_h, host_t *bind_h, host_t *dst_h) {
|
|
int max, sender;
|
|
fd_set fds;
|
|
|
|
/* we want to properly tear down running sessions when interrupted,
|
|
int_handler() will be called on INT or TERM signals */
|
|
signal(SIGINT, int_handler);
|
|
signal(SIGTERM, int_handler);
|
|
|
|
for(;;) {
|
|
/*
|
|
Normally returns 0, that is, if it's the first instruction after
|
|
entering the loop. However, it will return 1, when called from
|
|
longjmp(), which will be called by int_handler() if a SIGINT- or
|
|
TERM arrives. In that case we leave the loop, tear down
|
|
everything and exit.
|
|
*/
|
|
if (setjmp(JumpBuffer) == 1) {
|
|
break;
|
|
}
|
|
|
|
FD_ZERO(&fds);
|
|
max = fill_set(&fds);
|
|
|
|
FD_SET(listensocket, &fds);
|
|
if (listensocket > max)
|
|
max = listensocket;
|
|
|
|
select(max + 1, &fds, NULL, NULL, NULL);
|
|
|
|
if (FD_ISSET(listensocket, &fds)) {
|
|
/* incoming client on the inside, get src, bind output fd, add
|
|
to list if known, otherwise just handle it */
|
|
handle_inside(listensocket, listen_h, bind_h, dst_h);
|
|
}
|
|
else {
|
|
/* remote answer came in on an output fd, proxy back to the inside */
|
|
sender = get_sender(&fds);
|
|
handle_outside(listensocket, sender, dst_h);
|
|
}
|
|
|
|
/* close old outputs, if any */
|
|
client_clean(0);
|
|
}
|
|
|
|
/* we came here via signal handler, clean up */
|
|
close(listensocket);
|
|
client_clean(1);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
Handle SIGINT- and TERM, call longjmp(), which jumps right into the
|
|
main loop, where it causes the loop to be left.
|
|
*/
|
|
void int_handler(int sig) {
|
|
signal(sig, SIG_IGN);
|
|
longjmp(JumpBuffer, 1);
|
|
}
|
|
|
|
void verb_prbind (host_t *bind_h) {
|
|
if(VERBOSE) {
|
|
if(strcmp(bind_h->ip, "0.0.0.0") != 0 || strcmp(bind_h->ip, "[::0]") != 0) {
|
|
verbose("from %s:%d\n", bind_h->ip, bind_h->port);
|
|
}
|
|
else {
|
|
verbose("\n");
|
|
}
|
|
}
|
|
}
|