mirror of
https://github.com/darkk/redsocks.git
synced 2025-08-24 18:58:36 +00:00
670 lines
16 KiB
C
670 lines
16 KiB
C
/* redsocks - transparent TCP-to-proxy redirector
|
|
* Copyright (C) 2007-2018 Leonid Evdokimov <leon@darkk.net.ru>
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
|
* use this file except in compliance with the License. You may obtain a copy
|
|
* of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
* License for the specific language governing permissions and limitations
|
|
* under the License.
|
|
*/
|
|
|
|
#include <stdlib.h>
|
|
#include <stdio.h>
|
|
#include <ctype.h>
|
|
#include <string.h>
|
|
#include <assert.h>
|
|
#include <stdbool.h>
|
|
#include <sys/types.h>
|
|
#include <sys/socket.h>
|
|
#include <netinet/in.h>
|
|
#include <arpa/inet.h>
|
|
#include <netdb.h>
|
|
#include <errno.h>
|
|
#include "utils.h"
|
|
#include "parser.h"
|
|
#include "log.h"
|
|
|
|
#define FREE(ptr) do { free(ptr); ptr = NULL; } while (0)
|
|
|
|
typedef int (*value_parser)(parser_context *context, void *addr, const char *token);
|
|
struct parser_context_t {
|
|
FILE *fd;
|
|
parser_section *sections;
|
|
int line;
|
|
int error;
|
|
struct {
|
|
size_t size;
|
|
size_t filled;
|
|
char *data;
|
|
} buffer;
|
|
};
|
|
|
|
|
|
void parser_error(parser_context *context, const char *fmt, ...)
|
|
{
|
|
va_list ap;
|
|
struct evbuffer *buff = evbuffer_new();
|
|
const char *msg;
|
|
|
|
va_start(ap, fmt);
|
|
if (buff) {
|
|
evbuffer_add_vprintf(buff, fmt, ap);
|
|
msg = (const char*)evbuffer_pullup(buff, -1);
|
|
}
|
|
else
|
|
msg = error_lowmem;
|
|
va_end(ap);
|
|
|
|
context->error = 1;
|
|
fprintf(stderr, "file parsing error at line %u: %s\n", context->line, msg);
|
|
if (buff)
|
|
evbuffer_free(buff);
|
|
}
|
|
|
|
parser_context* parser_start(FILE *fd)
|
|
{
|
|
parser_context *ret = calloc(1, sizeof(parser_context));
|
|
if (!ret)
|
|
return NULL;
|
|
ret->fd = fd;
|
|
ret->buffer.size = 128; // should be big enough to fetch whole ``line``
|
|
ret->buffer.data = malloc(ret->buffer.size);
|
|
if (!ret->buffer.data) {
|
|
free(ret);
|
|
return NULL;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
void parser_add_section(parser_context *context, parser_section *section)
|
|
{
|
|
section->next = context->sections;
|
|
context->sections = section;
|
|
section->context = context;
|
|
}
|
|
|
|
void parser_stop(parser_context *context)
|
|
{
|
|
free(context->buffer.data);
|
|
free(context);
|
|
}
|
|
|
|
static char unescape(int c)
|
|
{
|
|
switch (c) {
|
|
case 'n': return '\n';
|
|
case 't': return '\t';
|
|
case 'r': return '\r';
|
|
|
|
case '\\': return '\\';
|
|
case '\'': return '\'';
|
|
case '\"': return '\"';
|
|
|
|
default: return 0;
|
|
}
|
|
}
|
|
|
|
/** returns NULL on invalid OR incomplete token
|
|
*/
|
|
static char *gettoken(parser_context *context, char **iter)
|
|
{
|
|
char *ret = NULL;
|
|
size_t len = 0;
|
|
enum {
|
|
gt_cstr,
|
|
gt_plainstr
|
|
} copytype;
|
|
|
|
// skip spaces
|
|
while (**iter && isspace(**iter))
|
|
(*iter)++;
|
|
if ( !**iter )
|
|
return NULL;
|
|
|
|
// count strlen() of output buffer
|
|
if ( **iter == '\"' ) { // string with escapes and spaces
|
|
char *p = *iter + 1;
|
|
|
|
copytype = gt_cstr;
|
|
while ( 1 ) {
|
|
if (*p == '\0')
|
|
return NULL;
|
|
if (*p == '\"')
|
|
break;
|
|
if (*p == '\\') {
|
|
if ( p[1] != '\0') {
|
|
if ( unescape(p[1]) )
|
|
p++;
|
|
else {
|
|
parser_error(context, "unknown escaped char after \\");
|
|
return NULL;
|
|
}
|
|
}
|
|
else {
|
|
return NULL;
|
|
}
|
|
}
|
|
len++;
|
|
p++;
|
|
}
|
|
}
|
|
else if ( isdigit(**iter) ) { // integer OR IP/NETMASK
|
|
char *p = *iter;
|
|
copytype = gt_plainstr;
|
|
while ( 1 ) {
|
|
if ( *p == '\0' )
|
|
return NULL;
|
|
else if ( isdigit(*p) || *p == '.' )
|
|
p++;
|
|
else if ( *p == '/' ) {
|
|
if (isdigit(p[1]))
|
|
p++;
|
|
else if (p[1] == '/' || p[1] == '*') // comment token is coming!
|
|
break;
|
|
else
|
|
return NULL;
|
|
}
|
|
else
|
|
break;
|
|
}
|
|
len = p - *iter;
|
|
}
|
|
else if ( isalpha(**iter) ) { // simple-string
|
|
char *p = *iter;
|
|
copytype = gt_plainstr;
|
|
while ( 1 ) {
|
|
if ( *p == '\0' )
|
|
return NULL;
|
|
else if (isalnum(*p) || *p == '_' || *p == '.' || *p == '-') // for domain-names
|
|
p++;
|
|
else
|
|
break;
|
|
}
|
|
len = p - *iter;
|
|
}
|
|
else if ( **iter == '{' || **iter == '}' || **iter == '=' || **iter == ';' ) { // config punctuation
|
|
copytype = gt_plainstr;
|
|
len = 1;
|
|
}
|
|
else if ( **iter == '/' && ( (*iter)[1] == '/' || (*iter)[1] == '*' ) ) { // comment-start
|
|
copytype = gt_plainstr;
|
|
len = 2;
|
|
}
|
|
else {
|
|
parser_error(context, "unexpected char");
|
|
return NULL;
|
|
}
|
|
|
|
ret = malloc(len + 1);
|
|
if (!ret) {
|
|
parser_error(context, "malloc failed");
|
|
return NULL;
|
|
}
|
|
|
|
if (copytype == gt_cstr) {
|
|
char *p = ret;
|
|
(*iter)++;
|
|
while ( 1 ) {
|
|
if (**iter == '\"') {
|
|
(*iter)++;
|
|
break;
|
|
}
|
|
if (**iter == '\\') {
|
|
*p = unescape(*(*iter + 1));
|
|
(*iter)++;
|
|
}
|
|
else {
|
|
*p = **iter;
|
|
}
|
|
(*iter)++;
|
|
p++;
|
|
}
|
|
*p = 0;
|
|
}
|
|
else if (copytype == gt_plainstr) {
|
|
memcpy(ret, *iter, len);
|
|
*iter += len;
|
|
ret[len] = 0;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
static void context_filled_add(parser_context *context, int shift)
|
|
{
|
|
context->buffer.filled += shift;
|
|
context->buffer.data[context->buffer.filled] = 0;
|
|
}
|
|
|
|
static void context_filled_set(parser_context *context, size_t val)
|
|
{
|
|
context->buffer.filled = val;
|
|
context->buffer.data[context->buffer.filled] = 0;
|
|
}
|
|
|
|
static int vp_pbool(parser_context *context, void *addr, const char *token)
|
|
{
|
|
char *strtrue[] = { "ok", "on", "yes", "true" };
|
|
char *strfalse[] = { "off", "no", "false" };
|
|
char **tpl;
|
|
|
|
FOREACH(tpl, strtrue)
|
|
if (strcmp(token, *tpl) == 0) {
|
|
*(bool*)addr = true;
|
|
return 0;
|
|
}
|
|
|
|
FOREACH(tpl, strfalse)
|
|
if (strcmp(token, *tpl) == 0) {
|
|
*(bool*)addr = false;
|
|
return 0;
|
|
}
|
|
|
|
parser_error(context, "boolean is not parsed");
|
|
return -1;
|
|
}
|
|
|
|
static int vp_disclose_src(parser_context *context, void *addr, const char *token)
|
|
{
|
|
enum disclose_src_e *dst = addr;
|
|
struct { char *name; enum disclose_src_e value; } opt[] = {
|
|
{ "off", DISCLOSE_NONE },
|
|
{ "no", DISCLOSE_NONE },
|
|
{ "false", DISCLOSE_NONE },
|
|
{ "X-Forwarded-For", DISCLOSE_X_FORWARDED_FOR },
|
|
{ "Forwarded_ip", DISCLOSE_FORWARDED_IP },
|
|
{ "Forwarded_ipport", DISCLOSE_FORWARDED_IPPORT },
|
|
};
|
|
for (int i = 0; i < SIZEOF_ARRAY(opt); ++i) {
|
|
if (strcmp(token, opt[i].name) == 0) {
|
|
*dst = opt[i].value;
|
|
return 0;
|
|
}
|
|
}
|
|
parser_error(context, "disclose_src <%s> is not parsed", token);
|
|
return -1;
|
|
}
|
|
|
|
static int vp_on_proxy_fail(parser_context *context, void *addr, const char *token)
|
|
{
|
|
enum on_proxy_fail_e *dst = addr;
|
|
struct { char *name; enum on_proxy_fail_e value; } opt[] = {
|
|
{ "close", ONFAIL_CLOSE },
|
|
{ "forward_http_err", ONFAIL_FORWARD_HTTP_ERR },
|
|
};
|
|
for (int i = 0; i < SIZEOF_ARRAY(opt); ++i) {
|
|
if (strcmp(token, opt[i].name) == 0) {
|
|
*dst = opt[i].value;
|
|
return 0;
|
|
}
|
|
}
|
|
parser_error(context, "on_proxy_fail <%s> is not parsed", token);
|
|
return -1;
|
|
}
|
|
|
|
static int vp_pchar(parser_context *context, void *addr, const char *token)
|
|
{
|
|
char *p = strdup(token);
|
|
if (!p) {
|
|
parser_error(context, "strdup failed");
|
|
return -1;
|
|
}
|
|
*(char**)addr = p;
|
|
return 0;
|
|
}
|
|
|
|
static int vp_uint16(parser_context *context, void *addr, const char *token)
|
|
{
|
|
char *end;
|
|
unsigned long int uli = strtoul(token, &end, 0);
|
|
if (uli > 0xFFFF) {
|
|
parser_error(context, "integer out of 16bit range");
|
|
return -1;
|
|
}
|
|
if (*end != '\0') {
|
|
parser_error(context, "integer is not parsed");
|
|
return -1;
|
|
}
|
|
*(uint16_t*)addr = (uint16_t)uli;
|
|
return 0;
|
|
}
|
|
|
|
static int vp_uint32(parser_context *context, void *addr, const char *token)
|
|
{
|
|
char *end;
|
|
uint32_t uli = strtoul(token, &end, 0);
|
|
if (*end != '\0') {
|
|
parser_error(context, "integer is not parsed");
|
|
return -1;
|
|
}
|
|
*(uint32_t*)addr = uli;
|
|
return 0;
|
|
}
|
|
|
|
static int vp_in_addr(parser_context *context, void *addr, const char *token)
|
|
{
|
|
struct in_addr ia;
|
|
|
|
if (inet_aton(token, &ia)) {
|
|
memcpy(addr, &ia, sizeof(ia));
|
|
}
|
|
else {
|
|
struct addrinfo *ainfo, hints;
|
|
int err;
|
|
memset(&hints, 0, sizeof(hints));
|
|
hints.ai_family = AF_INET; /* IPv4-only */
|
|
hints.ai_socktype = SOCK_STREAM; /* I want to have one address once and ONLY once, that's why I specify socktype and protocol */
|
|
hints.ai_protocol = IPPROTO_TCP;
|
|
hints.ai_flags = AI_ADDRCONFIG; /* I don't need IPv4 addrs without IPv4 connectivity */
|
|
err = getaddrinfo(token, NULL, &hints, &ainfo);
|
|
if (err == 0) {
|
|
int count, taken;
|
|
struct addrinfo *iter;
|
|
struct sockaddr_in *resolved_addr;
|
|
for (iter = ainfo, count = 0; iter; iter = iter->ai_next, ++count)
|
|
;
|
|
taken = red_randui32() % count;
|
|
for (iter = ainfo; taken > 0; iter = iter->ai_next, --taken)
|
|
;
|
|
resolved_addr = (struct sockaddr_in*)iter->ai_addr;
|
|
assert(resolved_addr->sin_family == iter->ai_family && iter->ai_family == AF_INET);
|
|
if (count != 1)
|
|
log_error(LOG_WARNING, "%s resolves to %d addresses, using %s",
|
|
token, count, inet_ntoa(resolved_addr->sin_addr));
|
|
memcpy(addr, &resolved_addr->sin_addr, sizeof(ia));
|
|
freeaddrinfo(ainfo);
|
|
}
|
|
else {
|
|
if (err == EAI_SYSTEM)
|
|
parser_error(context, "unable to resolve %s, error %d (%s)", token, errno, strerror(errno));
|
|
else
|
|
parser_error(context, "unable to resolve %s, getaddrinfo error %d (%s)", token, err, gai_strerror(err));
|
|
return -1;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int vp_in_addr2(parser_context *context, void *addr, const char *token)
|
|
{
|
|
char *host = NULL, *mask = NULL;
|
|
struct in_addr ia;
|
|
int retval = 0;
|
|
|
|
host = strdup(token);
|
|
if (!host) {
|
|
parser_error(context, "strdup failed");
|
|
return -1;
|
|
}
|
|
mask = strchr(host, '/');
|
|
if (mask) {
|
|
*mask = '\0';
|
|
mask++;
|
|
}
|
|
|
|
if (inet_aton(host, &ia)) {
|
|
memcpy(addr, &ia, sizeof(ia));
|
|
}
|
|
else {
|
|
parser_error(context, "invalid IP address");
|
|
retval = -1;
|
|
}
|
|
|
|
if (mask) {
|
|
struct in_addr *pinmask = ((struct in_addr*)addr) + 1;
|
|
char *end;
|
|
unsigned long int uli = strtoul(mask, &end, 0);;
|
|
if (*end == '.') {
|
|
if (inet_aton(mask, &ia)) {
|
|
memcpy(pinmask , &ia, sizeof(ia));
|
|
}
|
|
else {
|
|
parser_error(context, "invalid IP address");
|
|
retval = -1;
|
|
}
|
|
}
|
|
else if (0 < uli && uli < 32) {
|
|
pinmask->s_addr = htonl((INADDR_BROADCAST << (32 - uli)));
|
|
}
|
|
else {
|
|
parser_error(context, "number of netmask bits out of range");
|
|
retval = -1;
|
|
}
|
|
}
|
|
|
|
free(host);
|
|
return retval;
|
|
}
|
|
|
|
static int vp_obsolete(parser_context *context, void *addr, const char *token)
|
|
{
|
|
parser_error(context, "obsolete key, delete it");
|
|
return -1;
|
|
}
|
|
|
|
static int vp_redsocks_max_accept_backoff(parser_context *context, void *addr, const char *token)
|
|
{
|
|
parser_error(context, "max_accept_backoff is not per-port setting anymore, move it from `redsocks` to `base`");
|
|
return -1;
|
|
}
|
|
|
|
static value_parser value_parser_by_type[] =
|
|
{
|
|
[pt_bool] = vp_pbool,
|
|
[pt_pchar] = vp_pchar,
|
|
[pt_uint16] = vp_uint16,
|
|
[pt_uint32] = vp_uint32,
|
|
[pt_in_addr] = vp_in_addr,
|
|
[pt_in_addr2] = vp_in_addr2,
|
|
[pt_disclose_src] = vp_disclose_src,
|
|
[pt_on_proxy_fail] = vp_on_proxy_fail,
|
|
[pt_obsolete] = vp_obsolete,
|
|
[pt_redsocks_max_accept_backoff] = vp_redsocks_max_accept_backoff,
|
|
};
|
|
|
|
int parser_run(parser_context *context)
|
|
{
|
|
char *section_token = NULL, *key_token = NULL, *value_token = NULL;
|
|
parser_section *section = NULL;
|
|
bool in_comment = false;
|
|
bool need_more_space = false;
|
|
bool need_more_data = true;
|
|
while ( !context->error && !feof(context->fd) ) {
|
|
assert(context->buffer.filled < context->buffer.size); // ``<`` and not ``<=``
|
|
|
|
if (need_more_space) {
|
|
char *new = realloc(context->buffer.data, context->buffer.size * 2);
|
|
if (!new) {
|
|
parser_error(context, "realloc failure");
|
|
return -1;
|
|
}
|
|
context->buffer.data = new;
|
|
context->buffer.size *= 2;
|
|
need_more_space = false;
|
|
}
|
|
|
|
if (need_more_data) { // read one line per call
|
|
char *sbegin = context->buffer.data + context->buffer.filled;
|
|
int len;
|
|
if (fgets(sbegin, context->buffer.size - context->buffer.filled, context->fd) == NULL) {
|
|
if (ferror(context->fd)) {
|
|
parser_error(context, "file read failure");
|
|
return -1;
|
|
}
|
|
else
|
|
continue;
|
|
}
|
|
len = strlen(sbegin);
|
|
context_filled_add(context, +len);
|
|
if (len > 0 && sbegin[len - 1] == '\n') {
|
|
context->line++;
|
|
need_more_data = false;
|
|
}
|
|
else {
|
|
need_more_space = true;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if ( in_comment ) {
|
|
char *endc = strstr(context->buffer.data, "*/");
|
|
if (endc) {
|
|
endc += 2;
|
|
int comment_len = endc - context->buffer.data;
|
|
memmove(context->buffer.data, endc, context->buffer.filled - comment_len);
|
|
context_filled_add(context, -comment_len);
|
|
in_comment = false;
|
|
}
|
|
else {
|
|
context_filled_set(context, 0);
|
|
need_more_data = true;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
char *token = NULL, *iter = context->buffer.data;
|
|
while ( !context->error && !in_comment && (token = gettoken(context, &iter)) ) {
|
|
if (strcmp(token, "//") == 0) {
|
|
char *endc = strchr(iter, '\n');
|
|
iter -= 2;
|
|
endc += 1;
|
|
// |*data |*iter |*endc -->|<--.filled
|
|
int moved_len = context->buffer.filled - (endc - context->buffer.data);
|
|
memmove(iter, endc, moved_len);
|
|
context_filled_add(context, -(endc - iter));
|
|
}
|
|
else if (strcmp(token, "/*") == 0) {
|
|
int moved_len = iter - context->buffer.data;
|
|
memmove(context->buffer.data, iter, context->buffer.filled - moved_len);
|
|
context_filled_add(context, -moved_len);
|
|
iter = context->buffer.data;
|
|
in_comment = true;
|
|
}
|
|
else if (strcmp(token, "{") == 0) { // } - I love folding
|
|
if (section) {
|
|
parser_error(context, "section-in-section is invalid");
|
|
}
|
|
else if (!section_token) {
|
|
parser_error(context, "expected token before ``{''"); // } - I love folding
|
|
}
|
|
else {
|
|
for (parser_section *p = context->sections; p; p = p->next) {
|
|
if (strcmp(p->name, section_token) == 0) {
|
|
section = p;
|
|
break;
|
|
}
|
|
}
|
|
if (section) {
|
|
if (section->onenter)
|
|
if ( section->onenter(section) == -1 )
|
|
parser_error(context, "section->onenter failed");
|
|
}
|
|
else {
|
|
parser_error(context, "unknown section <%s>", section_token);
|
|
}
|
|
FREE(section_token);
|
|
}
|
|
}
|
|
else if (strcmp(token, "}") == 0) { // { - I love folding
|
|
if (section) {
|
|
if (section->onexit)
|
|
if ( section->onexit(section) == -1 )
|
|
parser_error(context, "section->onexit failed");
|
|
section = NULL;
|
|
}
|
|
else {
|
|
parser_error(context, "can't close non-opened section");
|
|
}
|
|
}
|
|
else if (strcmp(token, "=") == 0) {
|
|
if (!section) {
|
|
parser_error(context, "assignment used outside of any section");
|
|
}
|
|
else if (!key_token) {
|
|
parser_error(context, "assignment used without key");
|
|
}
|
|
else {
|
|
; // What can I do? :)
|
|
}
|
|
}
|
|
else if (strcmp(token, ";") == 0) {
|
|
if (!section) {
|
|
parser_error(context, "assignment termination outside of any section");
|
|
}
|
|
else if (key_token && !value_token) {
|
|
parser_error(context, "assignment has only key <%s> but no value", key_token);
|
|
}
|
|
else if (key_token && value_token) {
|
|
parser_entry *e;
|
|
for (e = section->entries; e->key; e++)
|
|
if (strcmp(e->key, key_token) == 0)
|
|
break;
|
|
if (e->key) {
|
|
if ( (value_parser_by_type[e->type])(context, e->addr, value_token) == -1 )
|
|
parser_error(context, "value <%s> can't be parsed for key <%s>", value_token, key_token);
|
|
}
|
|
else {
|
|
parser_error(context, "assignment with unknown key <%s>", key_token);
|
|
}
|
|
}
|
|
else {
|
|
assert(false);
|
|
}
|
|
FREE(key_token);
|
|
FREE(value_token);
|
|
}
|
|
else {
|
|
if (!section && !section_token) {
|
|
section_token = token;
|
|
token = 0;
|
|
}
|
|
else if (section && !key_token) {
|
|
key_token = token;
|
|
token = 0;
|
|
}
|
|
else if (section && key_token && !value_token) {
|
|
value_token = token;
|
|
token = 0;
|
|
}
|
|
else {
|
|
parser_error(context, "invalid token order: key_token=%s, value_token=%s, next token=%s",
|
|
key_token, value_token, token);
|
|
}
|
|
}
|
|
free(token);
|
|
}
|
|
int moved_len = iter - context->buffer.data;
|
|
memmove(context->buffer.data, iter, context->buffer.filled - moved_len);
|
|
context_filled_add(context, -moved_len);
|
|
|
|
if (!token)
|
|
need_more_data = true;
|
|
}
|
|
// file-error goes is thrown here
|
|
if (section)
|
|
parser_error(context, "unclosed section");
|
|
if (section_token) {
|
|
parser_error(context, "stale section_token");
|
|
free(section_token);
|
|
}
|
|
if (key_token) {
|
|
parser_error(context, "stale key_token");
|
|
free(key_token);
|
|
}
|
|
if (value_token) {
|
|
parser_error(context, "stale value_token");
|
|
free(value_token);
|
|
}
|
|
return context->error ? -1 : 0;
|
|
}
|
|
|
|
|
|
/* vim:set tabstop=4 softtabstop=4 shiftwidth=4: */
|
|
/* vim:set foldmethod=marker foldlevel=32 foldmarker={,}: */
|