// This work is based on the original netsed 0.01c by Michal Zalewski. // Please contact Julien VdG <julien@silicone.homelinux.org> if you encounter // any problems with this version. // The changes compared to version 0.01c are related in the NEWS file. ///@mainpage /// /// This documentation is targeting netsed devloppers, if you are a user /// either launch netsed without parameters or read the README file /// (@link README @endlink). /// ///@par /// - Currently netsed is implemented in a single file: netsed.c /// - some TODOs are gathered on the @link todo @endlink page, /// some others are in the TODO file. /// . ///@file netsed.c ///@par Architecture /// Netsed is implemented as a select socket dispatcher. /// First a main socket server is created (#lsock), each connection to this /// socket create a context traked in the tracker_s structure. /// Each connection has /// - a connected socket (tracker_s::csock) returned by the accept() function /// for tcp, or /// - a connection socket address (tracker_s::csa) filled by recvfrom for udp. /// - a dedicated forwarding socket (tracker_s::fsock) connected to the server. /// . /// All sockets are added to the select() call and managed by the dispatcher /// as follows: /// - When packets are received from the client, the rules are aplied by /// sed_the_buffer() and the packet is send to the server. /// This is the role of client2server_sed() function. It is only used for tcp. /// - When packets are received from the server, the rules are aplied by /// sed_the_buffer() and the packet is send to the corresponding client. /// This is the role of server2client_sed() function. /// - For udp only, connection from client to netsed are not established /// so netsed need to lookup existing #connections to find the corresponding /// established link, if any. The lookup is done by comparing tracker_s::csa. /// Once found or created, the rules are aplied by sed_the_buffer() and the /// packet is send to the server. /// This is the role of b2server_sed() function. /// /// @note For tcp tracker_s::csa is NULL and for udp the tracker_s::csock is /// filled with #lsock. This is done in order to share code and avoid /// discriminating between tcp or udp everywhere, sendto are done on /// tracker_s::csock with tracker_s::csa only and the actual value of those /// will reflect the needs. ///@page README User documentation ///@verbinclude README #include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <sys/select.h> #include <sys/wait.h> #include <netinet/in.h> #include <arpa/inet.h> #include <netdb.h> #include <fcntl.h> #include <string.h> #include <errno.h> #include <ctype.h> #include <stdlib.h> #include <signal.h> #include <netdb.h> #include <time.h> /// Current version (recovered by Makefile for several release checks) #define VERSION "0.02c" /// max size for buffers #define MAX_BUF 100000 /// printf to stderr #define ERR(x...) fprintf(stderr,x) // uncomment to add a lot of debug information //#define DEBUG #ifdef DEBUG #define DBG(x...) printf(x) #else #define DBG(x...) #endif /// Timeout for udp 'connections' in seconds #define UDP_TIMEOUT 30 /// Rule item. struct rule_s { /// binary buffer to match. char *from; /// binary buffer replacement. char *to; /// match from the command line. const char *forig; /// replacement from the command line. const char *torig; /// length of #from buffer. int fs; /// length of #to buffer. int ts; }; /// Connection state enum state_e { /// udp datagram received by netsed and send to server, no response yet. UNREPLIED, /// tcp accepted connection or udp 'connection' with a response from server. ESTABLISHED, /// tcp or udp disconnected (detected by an error on read or send). /// @note all values after and including #DISCONNECTED are considered as /// error and the connection will be discarded. DISCONNECTED, /// udp timeout expired. TIMEOUT }; /// This structure is used to track information about open connections. struct tracker_s { /// recvfrom information: 'connect' address for udp struct sockaddr* csa; /// size of #csa socklen_t csl; /// Connection socket to client int csock; /// Socket to forward to server int fsock; /// Last event time, for udp timeout time_t time; /// Connection state enum state_e state; /// By connection TTL int* live; /// chain it ! struct tracker_s * n; }; /// Store current time (just after select returned). time_t now; /// Listening socket. int lsock; /// Number of rules. int rules; /// Array of all rules. struct rule_s *rule; /// TTL part of the rule as a flat array to be able to copy it /// in tracker_s::live for each connections. int *rule_live; /// List of connections. struct tracker_s * connections = NULL; /// True when SIGINT signal was received. volatile int stop=0; /// Display an error message followed by usage information. /// @param why the error message. void usage_hints(const char* why) { ERR("Error: %s\n\n",why); ERR("Usage: netsed proto lport rhost rport rule1 [ rule2 ... ]\n\n"); ERR(" proto - protocol specification (tcp or udp)\n"); ERR(" lport - local port to listen on (see README for transparent\n"); ERR(" traffic intercepting on some systems)\n"); ERR(" rhost - where connection should be forwarded (0 = use destination\n"); ERR(" address of incoming connection, see README)\n"); ERR(" rport - destination port (0 = dst port of incoming connection)\n"); ERR(" ruleN - replacement rules (see below)\n\n"); ERR("General syntax of replacement rules: s/pat1/pat2[/expire]\n\n"); ERR("This will replace all occurrences of pat1 with pat2 in any matching packet.\n"); ERR("An additional parameter (count) can be used to expire a rule after 'count'\n"); ERR("successful substitutions. Eight-bit characters, including NULL and '/',\n"); ERR("can be passed using HTTP-like hex escape sequences (e.g. CRLF as %%0a%%0d).\n"); ERR("A match on '%%' can be achieved by specifying '%%%%'. Examples:\n\n"); ERR(" 's/andrew/mike/1' - replace 'andrew' with 'mike' (only first time)\n"); ERR(" 's/andrew/mike' - replace all occurrences of 'andrew' with 'mike'\n"); ERR(" 's/andrew/mike%%00%%00' - replace 'andrew' with 'mike\\x00\\x00'\n"); ERR(" (manually padding to keep original size)\n"); ERR(" 's/%%%%/%%2f/20' - replace the 20 first occurence of '%%' with '/'\n\n"); ERR("Rules are not active across packet boundaries, and they are evaluated\n"); ERR("from first to last, not yet expired rule, as stated on the command line.\n"); exit(1); } /// Helper function to free a tracker_s item. /// csa will be freed if needed, sockets will be closed /// @param conn pointer to free. void freetracker (struct tracker_s * conn) { if(conn->csa != NULL) { // udp free(conn->csa); } else { // tcp close(conn->csock); } close(conn->fsock); free(conn); } /// Close all sockets /// to use before exit. void clean_socks(void) { close(lsock); // close all tracker while(connections != NULL) { struct tracker_s * conn = connections; connections = conn->n; freetracker(conn); } } #ifdef __GNUC__ // avoid gcc from inlining those two function when optimizing, as otherwise // the function whould break strict-aliasing rules by dereferencing pointers... in_port_t get_port(struct sockaddr *sa) __attribute__ ((noinline)); void set_port(struct sockaddr *sa, in_port_t port) __attribute__ ((noinline)); #endif /// Extract the port information from a sockaddr for both IPv4 and IPv6. /// @param sa sockaddr to get port from in_port_t get_port(struct sockaddr *sa) { switch (sa->sa_family) { case AF_INET: return ntohs(((struct sockaddr_in *) sa)->sin_port); case AF_INET6: return ntohs(((struct sockaddr_in6 *) sa)->sin6_port); default: return 0; } } /* get_port(struct sockaddr *) */ /// Set the port information in a sockaddr for both IPv4 and IPv6. /// @param sa sockaddr to update /// @param port port value void set_port(struct sockaddr *sa, in_port_t port) { switch (sa->sa_family) { case AF_INET: ((struct sockaddr_in *) sa)->sin_port = htons(port); break; case AF_INET6: ((struct sockaddr_in6 *) sa)->sin6_port = htons(port); default: break; } } /* set_port(struct sockaddr *, in_port_t) */ /// Display an error message and exit. void error(const char* reason) { ERR("[-] Error: %s\n",reason); ERR("netsed: exiting.\n"); clean_socks(); exit(2); } /// Hex digit to parsing the % notation in rules char hex[]="0123456789ABCDEF"; /// Convert the % notation in rules to plain binary data /// @param r rule to update void shrink_to_binary(struct rule_s* r) { int i; r->from=malloc(strlen(r->forig)); r->to=malloc(strlen(r->torig)); if ((!r->from) || (!r->to)) error("shrink_to_binary: unable to malloc() buffers"); for (i=0;i<strlen(r->forig);i++) { if (r->forig[i]=='%') { // Have to shrink. i++; if (r->forig[i]=='%') { // '%%' -> '%' r->from[r->fs]='%'; r->fs++; } else { int hexval; char* x; if (!r->forig[i]) error("shrink_to_binary: src pattern: unexpected end."); if (!r->forig[i+1]) error("shrink_to_binary: src pattern: unexpected end."); x=strchr(hex,toupper(r->forig[i])); if (!x) error("shrink_to_binary: src pattern: non-hex sequence."); hexval=(x-hex)*16; x=strchr(hex,toupper(r->forig[i+1])); if (!x) error("shrink_to_binary: src pattern: non-hex sequence."); hexval+=(x-hex); r->from[r->fs]=hexval; r->fs++; i++; } } else { // Plaintext case. r->from[r->fs]=r->forig[i]; r->fs++; } } for (i=0;i<strlen(r->torig);i++) { if (r->torig[i]=='%') { // Have to shrink. i++; if (r->torig[i]=='%') { // '%%' -> '%' r->to[r->ts]='%'; r->ts++; } else { int hexval; char* x; if (!r->torig[i]) error("shrink_to_binary: dst pattern: unexpected end."); if (!r->torig[i+1]) error("shrink_to_binary: dst pattern: unexpected end."); x=strchr(hex,toupper(r->torig[i])); if (!x) error("shrink_to_binary: dst pattern: non-hex sequence."); hexval=(x-hex)*16; x=strchr(hex,toupper(r->torig[i+1])); if (!x) error("shrink_to_binary: dst pattern: non-hex sequence."); hexval+=(x-hex); r->to[r->ts]=hexval; r->ts++; i++; } } else { // Plaintext case. r->to[r->ts]=r->torig[i]; r->ts++; } } } /// Bind and optionally listen to a socket for netsed server port. /// @param af address family. /// @param tcp 1 tcp, 0 udp. /// @param portstr string representing the port to bind /// (will be resolved using getaddrinfo()). void bind_and_listen(int af, int tcp, const char *portstr) { int ret; struct addrinfo hints, *res, *reslist; memset(&hints, '\0', sizeof(hints)); hints.ai_family = af; hints.ai_flags = AI_PASSIVE; hints.ai_socktype = tcp ? SOCK_STREAM : SOCK_DGRAM; if ((ret = getaddrinfo(NULL, portstr, &hints, &reslist))) { ERR("getaddrinfo(): %s\n", gai_strerror(ret)); error("Impossible to resolve listening port."); } /* We have useful addresses. */ for (res = reslist; res; res = res->ai_next) { int one = 1; if ( (lsock = socket(res->ai_family, res->ai_socktype, res->ai_protocol)) < 0) continue; setsockopt(lsock, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)); //fcntl(lsock,F_SETFL,O_NONBLOCK); if (bind(lsock, res->ai_addr, res->ai_addrlen) < 0) { ERR("bind(): %s", strerror(errno)); close(lsock); continue; } if (tcp) { if (listen(lsock, 16) < 0) { close(lsock); continue; } } else { // udp int one=1; setsockopt(lsock,SOL_SOCKET,SO_OOBINLINE,&one,sizeof(int)); } /* Successfully bound and now also listening. */ break; } freeaddrinfo(reslist); if (res == NULL) error("Listening socket failed."); } /// Buffer for receiving a single packet or datagram char buf[MAX_BUF]; /// Buffer containing modified packet or datagram char b2[MAX_BUF]; /// Applies the rules to global buffer buf. /// @param siz useful size of the data in buf. /// @param live TTL state of current connection. int sed_the_buffer(int siz, int* live) { int i=0,j=0; int newsize=0; int changes=0; int gotchange=0; for (i=0;i<siz;) { gotchange=0; for (j=0;j<rules;j++) { if ((!memcmp(&buf[i],rule[j].from,rule[j].fs)) && (live[j]!=0)) { changes++; gotchange=1; printf(" Applying rule s/%s/%s...\n",rule[j].forig,rule[j].torig); live[j]--; if (live[j]==0) printf(" (rule just expired)\n"); memcpy(&b2[newsize],rule[j].to,rule[j].ts); newsize+=rule[j].ts; i+=rule[j].fs; break; } } if (!gotchange) { b2[newsize]=buf[i]; newsize++; i++; } } if (!changes) printf("[*] Forwarding untouched packet of size %d.\n",siz); else printf("[*] Done %d replacements, forwarding packet of size %d (orig %d).\n", changes,newsize,siz); return newsize; } // Prototype this function so that the content is in the same order as in // previous read_write_sed function. (ease patch and diff) void b2server_sed(struct tracker_s * conn, ssize_t rd); /// Receive a packet or datagram from the server, 'sed' it, send it to the /// client. /// @param conn connection giving the sockets to use. void server2client_sed(struct tracker_s * conn) { ssize_t rd; rd=read(conn->fsock,buf,sizeof(buf)); if ((rd<0) && (errno!=EAGAIN)) { DBG("[!] server disconnected. (rd err) %s\n",strerror(errno)); conn->state = DISCONNECTED; } if (rd == 0) { // nothing read but select said ok, so EOF DBG("[!] server disconnected. (rd)\n"); conn->state = DISCONNECTED; } if (rd>0) { printf("[+] Caught server -> client packet.\n"); rd=sed_the_buffer(rd, conn->live); conn->time = now; conn->state = ESTABLISHED; if (sendto(conn->csock,b2,rd,0,conn->csa, conn->csl)<=0) { DBG("[!] client disconnected. (wr)\n"); conn->state = DISCONNECTED; } } } /// Receive a packet from the client, 'sed' it, send it to the server. /// @param conn connection giving the sockets to use. void client2server_sed(struct tracker_s * conn) { ssize_t rd; rd=read(conn->csock,buf,sizeof(buf)); if ((rd<0) && (errno!=EAGAIN)) { DBG("[!] client disconnected. (rd err)\n"); conn->state = DISCONNECTED; } if (rd == 0) { // nothing read but select said ok, so EOF DBG("[!] client disconnected. (rd)\n"); conn->state = DISCONNECTED; } b2server_sed(conn, rd); } /// Send the content of global buffer b2 to the server as packet or datagram. /// @param conn connection giving the sockets to use. /// @param rd size of b2 content. void b2server_sed(struct tracker_s * conn, ssize_t rd) { if (rd>0) { printf("[+] Caught client -> server packet.\n"); rd=sed_the_buffer(rd, conn->live); conn->time = now; if (write(conn->fsock,b2,rd)<=0) { DBG("[!] server disconnected. (wr)\n"); conn->state = DISCONNECTED; } } } /// Handle SIGCHILD signal /// @todo is this still needed as we fork() no more ??? void sig_chld(int signo) { pid_t pid; int stat; while ( (pid = waitpid(-1, &stat, WNOHANG)) > 0) printf("[!] child %d terminated\n", pid); return; } /// Handle SIGINT signal for clean exit. void sig_int(int signo) { DBG("[!] user interrupt request (%d)\n",getpid()); stop = 1; } /// This is main... int main(int argc,char* argv[]) { int i, ret; in_port_t fixedport = 0; struct sockaddr_storage fixedhost; struct addrinfo hints, *res, *reslist; int tcp; struct tracker_s * conn; memset(&fixedhost, '\0', sizeof(fixedhost)); printf("netsed " VERSION " by Julien VdG <julien@silicone.homelinux.org>\n" " based on 0.01c from Michal Zalewski <lcamtuf@ids.pl>\n"); setbuffer(stdout,NULL,0); if (argc<6) usage_hints("not enough parameters"); if (strcasecmp(argv[1],"tcp")*strcasecmp(argv[1],"udp")) usage_hints("incorrect procotol"); tcp = strncasecmp(argv[1], "udp", 3); // allocate rule arrays, rule number is number of param after 5 rule=malloc((argc-5)*sizeof(struct rule_s)); rule_live=malloc((argc-5)*sizeof(int)); // parse rules for (i=5;i<argc;i++) { char *fs=0, *ts=0, *cs=0; printf("[*] Parsing rule %s...\n",argv[i]); fs=strchr(argv[i],'/'); if (!fs) error("missing first '/' in rule"); fs++; ts=strchr(fs,'/'); if (!ts) error("missing second '/' in rule"); *ts=0; ts++; cs=strchr(ts,'/'); if (cs) { *cs=0; cs++; } rule[rules].forig=fs; rule[rules].torig=ts; if (cs) rule_live[rules]=atoi(cs); else rule_live[rules]=-1; shrink_to_binary(&rule[rules]); // printf("DEBUG: (%s) (%s)\n",rule[rules].from,rule[rules].to); rules++; } printf("[+] Loaded %d rule%s...\n", rules, (rules > 1) ? "s" : ""); memset(&hints, '\0', sizeof(hints)); hints.ai_family = AF_UNSPEC; hints.ai_flags = AI_CANONNAME; hints.ai_socktype = tcp ? SOCK_STREAM : SOCK_DGRAM; if ((ret = getaddrinfo(argv[3], argv[4], &hints, &reslist))) { ERR("getaddrinfo(): %s\n", gai_strerror(ret)); error("Impossible to resolve remote address or port."); } /* We have candidates for remote host. */ for (res = reslist; res; res = res->ai_next) { int sd = -1; if ( (sd = socket(res->ai_family, res->ai_socktype, res->ai_protocol)) < 0) continue; /* Has successfully built a socket for this address family. */ /* Record the address structure and the port. */ fixedport = get_port(res->ai_addr); memcpy(&fixedhost, res->ai_addr, res->ai_addrlen); close(sd); break; } freeaddrinfo(reslist); if (res == NULL) error("Failed in resolving remote host."); if (fixedhost.ss_family && fixedport) printf("[+] Using fixed forwarding to %s,%s.\n",argv[3],argv[4]); else printf("[+] Using dynamic (transparent proxy) forwarding.\n"); bind_and_listen(fixedhost.ss_family, tcp, argv[2]); printf("[+] Listening on port %s/%s.\n", argv[2], argv[1]); signal(SIGPIPE,SIG_IGN); /// @todo use sigaction signal(SIGCHLD,sig_chld); signal(SIGINT,sig_int); while (!stop) { struct sockaddr_storage s; socklen_t l = sizeof(s); struct sockaddr_storage conho; in_port_t conpo; char ipstr[INET6_ADDRSTRLEN], portstr[12]; int sel; fd_set rd_set; struct timeval timeout; int nfds = lsock; FD_ZERO(&rd_set); FD_SET(lsock,&rd_set); timeout.tv_sec = 1; timeout.tv_usec = 0; { conn = connections; /// @todo process time to adjust timeout while(conn != NULL) { if(tcp) { FD_SET(conn->csock, &rd_set); if (nfds < conn->csock) nfds = conn->csock; } FD_SET(conn->fsock, &rd_set); if (nfds < conn->fsock) nfds = conn->fsock; // point on next conn = conn->n; } } sel=select(nfds+1, &rd_set, (fd_set*)0, (fd_set*)0, &timeout); time(&now); if (stop) { break; } if (sel < 0) { if (errno == EINTR) continue; // we will get some SIGCHLD DBG("[!] select fail! %s\n", strerror(errno)); break; } if (sel == 0) { // DBG("[*] select timeout\n"); // continue; // select timeout } if (FD_ISSET(lsock, &rd_set)) { int csock=-1; ssize_t rd=-1; if (tcp) { csock = accept(lsock,(struct sockaddr*)&s,&l); } else { // udp does not handle accept, so track connections manually // also set csock if a new connection need to be registered // to share the code with tcp ;) rd = recvfrom(lsock,buf,sizeof(buf),0,(struct sockaddr*)&s,&l); if(rd >= 0) { conn = connections; while(conn != NULL) { // look for existing connections if ((conn->csl == l) && (0 == memcmp(&s, conn->csa, l))) { // found break; } // point on next conn = conn->n; } // not found if(conn == NULL) { // udp 'connection' socket is the listening one csock = lsock; } else { DBG("[+] Got incoming datagram from existing connection.\n"); } } else { ERR("recvfrom(): %s", strerror(errno)); } } // new connection (tcp accept, or udp conn not found) if ((csock)>=0) { int one=1; getnameinfo((struct sockaddr *) &s, l, ipstr, sizeof(ipstr), portstr, sizeof(portstr), NI_NUMERICHOST | NI_NUMERICSERV); printf("[+] Got incoming connection from %s,%s", ipstr, portstr); conn = malloc(sizeof(struct tracker_s)); if(NULL == conn) error("netsed: unable to malloc() connection tracker struct"); // protocol specific init if (tcp) { setsockopt(csock,SOL_SOCKET,SO_OOBINLINE,&one,sizeof(int)); conn->csa = NULL; conn->csl = 0; conn->state = ESTABLISHED; } else { conn->csa = malloc(l); if(NULL == conn->csa) error("netsed: unable to malloc() connection tracker sockaddr struct"); memcpy(conn->csa, &s, l); conn->csl = l; conn->state = UNREPLIED; } conn->csock = csock; conn->time = now; conn->live = malloc(rules*sizeof(int)); if(NULL == conn->live) error("netsed: unable to malloc() connection tracker sockaddr struct"); memcpy(conn->live, rule_live, rules*sizeof(int)); l = sizeof(s); getsockname(csock,(struct sockaddr*)&s,&l); getnameinfo((struct sockaddr *) &s, l, ipstr, sizeof(ipstr), portstr, sizeof(portstr), NI_NUMERICHOST | NI_NUMERICSERV); printf(" to %s,%s\n", ipstr, portstr); conpo = get_port((struct sockaddr *) &s); memcpy(&conho, &s, sizeof(conho)); if (fixedport) conpo=fixedport; if (fixedhost.ss_family) memcpy(&conho, &fixedhost, sizeof(conho)); memcpy(&s, &conho, sizeof(s)); getnameinfo((struct sockaddr *) &s, l, ipstr, sizeof(ipstr), portstr, sizeof(portstr), NI_NUMERICHOST | NI_NUMERICSERV); printf("[*] Forwarding connection to %s,%s\n", ipstr, portstr); // forward to addr memcpy(&s, &conho, sizeof(s)); set_port((struct sockaddr *) &s, conpo); l=sizeof(s); // connect will bind with some dynamic addr/port conn->fsock = socket(s.ss_family, tcp ? SOCK_STREAM : SOCK_DGRAM, 0); if (connect(conn->fsock,(struct sockaddr*)&s,l)) { printf("[!] Cannot connect to remote server, dropping connection.\n"); freetracker(conn); conn = NULL; } else { setsockopt(conn->fsock,SOL_SOCKET,SO_OOBINLINE,&one,sizeof(int)); conn->n = connections; connections = conn; } } // udp has data process forwarding if((rd >= 0) && (conn != NULL)) { b2server_sed(conn, rd); } } // lsock is set // all other sockets conn = connections; struct tracker_s ** pconn = &connections; while(conn != NULL) { // incoming data ? if(tcp && FD_ISSET(conn->csock, &rd_set)) { client2server_sed(conn); } if(FD_ISSET(conn->fsock, &rd_set)) { server2client_sed(conn); } // timeout ? udp only //DBG("[!] connection last time: %d, now: %d\n", conn->time, now); if(!tcp && ((now - conn->time) > UDP_TIMEOUT)) { DBG("[!] connection timeout.\n"); conn->state = TIMEOUT; } if(conn->state >= DISCONNECTED) { // remove it (*pconn)=conn->n; freetracker(conn); conn=(*pconn); } else { // point on next pconn = &(conn->n); conn = conn->n; } } } clean_socks(); exit(0); } // vim:sw=2:sta:et: