Ground-Zerro / Phobos Public
Code Issues Pull requests Actions Releases View on GitHub ↗
22.0 KB c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <ctype.h>
#include <limits.h>
#include "config.h"
#include "wg-obfuscator.h"
#include "mini_argp.h"
#include "masking.h"
#include "socks5_mask.h"

// Executable name
static const char *arg0;

/* The options we understand. */
static const mini_argp_opt options[] = {
    { "help", '?', 0 },
    { "config", 'c', 1 },
    { "mode", 'M', 1 },
    { "role", 'R', 1 },
    { "source-if", 'i', 1 },
    { "source-lport", 'p', 1 },
    { "target", 't', 1 },
    { "key", 'k', 1 },
    { "masking", 'a', 1 },
    { "static-bindings", 'b', 1 },
    { "socks5-users", 'U', 1 },
    { "socks5-stats", 's', 1 },
    { "max-clients" , 'm', 1 },
    { "idle-timeout", 'l', 1 },
    { "max-dummy", 'd', 1 },
    { "obfuscate-bytes", 'O', 1 },
    { "media-pt", 'P', 1 },
    { "media-ssrc", 'S', 1 },
    { "media-clock", 'C', 1 },
    { "fwmark", 'f', 1 },
    { "verbose", 'v', 1 },
    { "threads", 'T', 1 },
    { 0 }
};

static void show_usage(void)
{
    printf("Usage: %s [options]\n%s", arg0,
        "  -?, --help                 Give this help list\n"
        "\n"
        "Main settings:\n"
        "  -c, --config=<config_file> Read configuration from file\n"
        "                             (can be used instead of the rest arguments)\n"
        "  -M, --mode=<mode>          Obfuscation mode (optional, default - wireguard)\n"
        "                             Supported values: wireguard (wg), socks5\n"
        "  -R, --role=<role>          SOCKS5 role (optional, default - relay)\n"
        "                             relay  - transport-only relay to a SOCKS5 server\n"
        "                             client - local SOCKS5 server, tunnels to server role\n"
        "                             server - exit, terminates SOCKS5, dials targets\n"
        "  -i, --source-if=<ip>       Source interface to listen on\n"
        "                             (optional, default - 0.0.0.0, e.g. all)\n"
        "  -p, --source-lport=<port>  Source port to listen\n"
        "  -t, --target=<ip>:<port>   Target IP and port\n"
        "  -k, --key=<key>            Obfuscation key \n"
        "                             (required, must be 1-255 characters long)\n"
        "  -a, --masking=<type>       Masking type (optional, default - AUTO)\n"
        "                             Supported values: STUN, MEDIA, TLS, AUTO, NONE\n"
        "                             TLS is socks5 mode only. AUTO in socks5 mode\n"
        "                             behaves like NONE (no per-connection autodetect)\n"
        "  -b, --static-bindings=<ip>:<port>:<port>,...\n"
        "                             WireGuard mode: comma-separated static bindings for\n"
        "                             two-way mode as <client_ip>:<client_port>:<forward_port>\n"
        "                             SOCKS5 relay: extra listen->target TCP routes as\n"
        "                             <listen_port>:<target_host>:<target_port>,...\n"
        "  -U, --socks5-users=<login>:<password>,...\n"
        "                             SOCKS5 role=server only: accepted RFC1929\n"
        "                             username/password pairs. If set, the server\n"
        "                             requires real SOCKS5 auth from the client\n"
        "                             application; role=client never needs this\n"
        "                             (it transparently relays the real handshake).\n"
        "  -s, --socks5-stats=<path>  SOCKS5 role=server only: periodically dump\n"
        "                             per-login traffic counters to <path>\n"
        "                             (atomic rewrite, one line per login:\n"
        "                             login up_bytes down_bytes conns idle_ms)\n"
        "  -f, --fwmark=<mark>        Firewall mark to set on all packets\n"
        "                             (optional, default - 0, e.g. disabled)\n"
        "  -T, --threads=<number>     Worker threads (0 = auto-detect, default: 0)\n"
        "  -v, --verbose=<level>      Verbosity level (optional, default - INFO)\n"
        "                             ERRORS (critical errors only)\n"
        "                             WARNINGS (important messages)\n"
        "                             2 - INFO (informational messages: status messages,\n"
        "                                      connection established, etc.)\n"
        "                             3 - DEBUG (detailed debug messages)\n"
        "                             4 - TRACE (very detailed debug messages, including\n"
        "                                       packet dumps)\n"
        "\n"
        "Additional options:\n"
        "  -m, --max-clients=<number> Maximum number of clients (default: 1024)\n"
        "  -l, --idle-timeout=<sec>   Idle timeout in seconds (default: 300)\n"
        "  -d, --max-dummy=<bytes>    Maximum length of dummy bytes for data packets\n"
        "                             (default: 4)\n"
        "  -O, --obfuscate-bytes=<n>  Obfuscate only the first <n> bytes of payload\n"
        "                             (0 = whole payload, default: 0; MEDIA default: 16)\n"
        "  -P, --media-pt=<type>      RTP payload type for MEDIA (0 = random preset,\n"
        "                             default: 0; static value must match on both sides)\n"
        "  -S, --media-ssrc=<id>      RTP SSRC for MEDIA (0 = random per connection,\n"
        "                             default: 0; static value must match on both sides)\n"
        "  -C, --media-clock=<fps>    RTP timestamp clock for MEDIA (0 = random preset,\n"
        "                             default: 0)\n");
}

static int parse_opt(const char *lname, char sname, const char *val, void *ctx);

/**
 * @brief Resets the configuration structure to its default values.
 *
 * This function clears the configuration structure by setting all fields to zero
 * and resetting the verbosity level to the default value.
 *
 * @param config Pointer to the obfuscator_config structure to be reset.
 */
static void reset_config(obfuscator_config_t *config)
{
    memset(config, 0, sizeof(*config));
    config->max_clients = MAX_CLIENTS_DEFAULT;
    config->idle_timeout = IDLE_TIMEOUT_DEFAULT;
    config->max_dummy_length_data = MAX_DUMMY_LENGTH_DATA_DEFAULT;
    verbose = LL_DEFAULT;
}

/**
 * Checks if the given string represents a valid integer.
 *
 * @param str Pointer to the null-terminated string to check.
 * @return Non-zero value if the string is a valid integer, 0 otherwise.
 */
static uint8_t is_integer(const char *str)
{
    if (!str || !*str) {
        return 0; // Empty string is not an integer
    }
    while (*str) {
        if (!isdigit((unsigned char)*str)) {
            return 0; // Non-digit character found
        }
        str++;
    }
    return 1; // All characters are digits
}

static long parse_int_range(const char *val, long min, long max, const char *name)
{
    if (!is_integer(val)) {
        log(LL_ERROR, "Invalid %s: %s (must be an integer)", name, val);
        exit(EXIT_FAILURE);
    }
    long v = atol(val);
    if (v < min || v > max) {
        log(LL_ERROR, "Invalid %s: %s (must be between %ld and %ld)", name, val, min, max);
        exit(EXIT_FAILURE);
    }
    return v;
}

/**
 * @brief Reads and processes the configuration file.
 *
 * This function opens the specified configuration file and parses its contents
 * to initialize or update the application's configuration settings.
 *
 * @param filename The path to the configuration file to be read.
 * @param config Pointer to the obfuscator_config structure where the parsed settings will be stored.
 */
static void read_config_file(const char *filename, obfuscator_config_t *config)
{
    // Read configuration from the file
    uint8_t first_section = 1; // Flag to indicate if this is the first section being processed
    char line[10 * 1024]; // Buffer to hold each line from the config file

    FILE *config_file = fopen(filename, "r");
    if (config_file == NULL) {
        perror("Can't open config file");
        exit(EXIT_FAILURE);
    }

    while (fgets(line, sizeof(line), config_file)) {
        // Remove trailing newlines, carriage returns, spaces and tabs
        while (strlen(line) && (line[strlen(line) - 1] == '\n' || line[strlen(line) - 1] == '\r' 
            || line[strlen(line) - 1] == ' ' || line[strlen(line) - 1] == '\t')) {
            line[strlen(line) - 1] = 0;
        }
        // Remove leading spaces and tabs
        while (strlen(line) && (line[0] == ' ' || line[0] == '\t')) {
            memmove(line, line + 1, strlen(line));
        }
        // Ignore comments
        char *comment_index = strstr(line, "#");
        if (comment_index != NULL) {
            *comment_index = 0;
        }
        // Skip empty lines or with spaces only
        if (strspn(line, " \t\r\n") == strlen(line)) {
            continue;
        }

        // It can be new section
        if (line[0] == '[' && line[strlen(line) - 1] == ']') {
            if (!first_section) {
                // new config, need to fork the process
                if (fork() == 0) {
                    // Close in the child process
                    fclose(config_file);
                    // Stop config file processing for this instance
                    return;
                }
            }
            size_t len = strlen(line) - 2;
            if (len > sizeof(section_name) - 1) {
                len = sizeof(section_name) - 1;
            }
            strncpy(section_name, line + 1, len);
            section_name[len] = 0;

            // Reset all the parameters
            reset_config(config);

            first_section = 0; // We have processed the first section
            continue;
        }

        // Parse key-value pairs
        char *key = strtok(line, "=");
        key = trim(key);
        while (strlen(key) && (key[strlen(key) - 1] == ' ' || key[strlen(key) - 1] == '\t' || key[strlen(key) - 1] == '\r' || key[strlen(key) - 1] == '\n')) {
            key[strlen(key) - 1] = 0;
        }
        char *value = strtok(NULL, "=");
        if (value == NULL) {
            log(LL_ERROR, "Invalid configuration line: %s", line);
            exit(EXIT_FAILURE);
        }
        value = trim(value);
        if (!*value) {
            log(LL_ERROR, "Invalid configuration line: %s", line);
            exit(EXIT_FAILURE);
        }
        const mini_argp_opt *o = margp_find(options, key, 0);
        if (o == NULL) {
            log(LL_ERROR, "Unknown configuration key: %s", key);
            exit(EXIT_FAILURE);
        }
        if (!o->has_arg) {
            log(LL_ERROR, "Configuration key '%s' does not accept a value", key);
            exit(EXIT_FAILURE);
        }
        parse_opt(o->long_name, o->short_name, value, config);
    }
    fclose(config_file);
}

/* Parse a single option. */
static int parse_opt(const char *lname, char sname, const char *val, void *ctx)
{
    obfuscator_config_t *config = (obfuscator_config_t *)ctx;
    char val_lower[16];

    switch (sname)
    {
        case '?':
            // Show usage and exit
            show_usage();
            exit(EXIT_SUCCESS);
        case 'c':
            read_config_file(val, config);
            break;
        case 'M':
            strncpy(val_lower, val, sizeof(val_lower) - 1);
            val_lower[sizeof(val_lower) - 1] = 0;
            for (char *p = val_lower; *p; ++p) *p = tolower((unsigned char)*p);
            if (strcmp(val_lower, "wireguard") == 0 || strcmp(val_lower, "wg") == 0) {
                config->mode = MODE_WIREGUARD;
            } else if (strcmp(val_lower, "socks5") == 0) {
                config->mode = MODE_SOCKS5;
            } else {
                log(LL_ERROR, "Unknown mode: %s (must be 'wireguard', 'wg' or 'socks5')", val);
                exit(EXIT_FAILURE);
            }
            break;
        case 'R':
            strncpy(val_lower, val, sizeof(val_lower) - 1);
            val_lower[sizeof(val_lower) - 1] = 0;
            for (char *p = val_lower; *p; ++p) *p = tolower((unsigned char)*p);
            if (strcmp(val_lower, "relay") == 0) {
                config->socks5_role = S5_ROLE_RELAY;
            } else if (strcmp(val_lower, "client") == 0) {
                config->socks5_role = S5_ROLE_CLIENT;
            } else if (strcmp(val_lower, "server") == 0) {
                config->socks5_role = S5_ROLE_SERVER;
            } else {
                log(LL_ERROR, "Unknown role: %s (must be 'relay', 'client' or 'server')", val);
                exit(EXIT_FAILURE);
            }
            break;
        case 'i':
            strncpy(config->client_interface, val, sizeof(config->client_interface) - 1);
            config->client_interface[sizeof(config->client_interface) - 1] = 0; // Ensure null-termination
            config->client_interface_set = 1;
            break;
        case 'p':
            config->listen_port = (int)parse_int_range(val, 1, 65535, "source port");
            config->listen_port_set = 1;
            break;
        case 't':
            strncpy(config->forward_host_port, val, sizeof(config->forward_host_port) - 1);
            config->forward_host_port[sizeof(config->forward_host_port) - 1] = 0; // Ensure null-termination
            config->forward_host_port_set = 1;
            break;
        case 'b':
            strncpy(config->static_bindings, val, sizeof(config->static_bindings) - 1);
            config->static_bindings[sizeof(config->static_bindings) - 1] = 0; // Ensure null-termination
            config->static_bindings_set = 1;
            break;
        case 'U':
            {
                char users_buf[10 * 1024];
                strncpy(users_buf, val, sizeof(users_buf) - 1);
                users_buf[sizeof(users_buf) - 1] = 0;
                config->socks5_user_count = 0;
                char *pair = strtok(users_buf, ",");
                while (pair) {
                    pair = trim(pair);
                    char *colon = strchr(pair, ':');
                    if (!colon) {
                        log(LL_ERROR, "Invalid socks5-users entry: %s (expected login:password)", pair);
                        exit(EXIT_FAILURE);
                    }
                    *colon = 0;
                    const char *login = pair;
                    const char *password = colon + 1;
                    size_t login_len = strlen(login);
                    size_t password_len = strlen(password);
                    if (login_len == 0 || login_len >= S5_CRED_MAX || password_len >= S5_CRED_MAX) {
                        log(LL_ERROR, "Invalid socks5-users entry: login/password length out of range");
                        exit(EXIT_FAILURE);
                    }
                    if (config->socks5_user_count >= MAX_SOCKS5_USERS) {
                        log(LL_ERROR, "Too many socks5-users entries (max %d)", MAX_SOCKS5_USERS);
                        exit(EXIT_FAILURE);
                    }
                    socks5_cred_t *u = &config->socks5_users[config->socks5_user_count++];
                    memcpy(u->login, login, login_len);
                    u->login_len = (uint8_t)login_len;
                    memcpy(u->password, password, password_len);
                    u->password_len = (uint8_t)password_len;
                    pair = strtok(NULL, ",");
                }
            }
            break;
        case 's':
            strncpy(config->socks5_stats_path, val, sizeof(config->socks5_stats_path) - 1);
            config->socks5_stats_path[sizeof(config->socks5_stats_path) - 1] = 0;
            break;
        case 'k':
            strncpy(config->xor_key, val, sizeof(config->xor_key));
            config->xor_key[sizeof(config->xor_key) - 1] = 0; // Ensure null-termination
            if (strlen(config->xor_key) == 0) {
                log(LL_ERROR, "XOR key cannot be empty");
                exit(EXIT_FAILURE);
            }
            config->xor_key_set = 1;
            break;
        case 'm':
            config->max_clients = (int)parse_int_range(val, 1, INT_MAX, "maximum number of clients");
            break;
        case 'l':
            config->idle_timeout = parse_int_range(val, 1, INT_MAX / 1000, "idle timeout") * 1000;
            break;
        case 'd':
            config->max_dummy_length_data = (int)parse_int_range(val, 0, MAX_DUMMY_LENGTH_TOTAL, "maximum dummy length for data packets");
            break;
        case 'O':
            config->obfuscate_bytes = (int)parse_int_range(val, 0, INT_MAX, "obfuscate-bytes");
            break;
        case 'P':
            config->media_payload_type = (uint8_t)parse_int_range(val, 0, 127, "media-pt");
            break;
        case 'S':
            {
                char *end = NULL;
                unsigned long ssrc = strtoul(val, &end, 0);
                if (!end || *end) {
                    log(LL_ERROR, "Invalid media-ssrc: %s (must be a number)", val);
                    exit(EXIT_FAILURE);
                }
                config->media_ssrc = (uint32_t)ssrc;
            }
            break;
        case 'C':
            {
                int fps = (int)parse_int_range(val, 0, 1000, "media-clock");
                config->media_ts_step = fps ? (uint16_t)(90000 / fps) : 0;
            }
            break;
        case 'f':
            {
#ifdef __linux__
                long int v = strtol(val, NULL, 0);
                if (v <= 0 || v > UINT16_MAX) {
                    log(LL_ERROR, "Invalid firewall mark: %s", val);
                    exit(EXIT_FAILURE);
                }
                config->fwmark = (uint16_t)v;
#else
                log(LL_WARN, "Firewall mark is not supported on this platform");
#endif
            }
            break;
        case 'a':
            {
                strncpy(val_lower, val, sizeof(val_lower) - 1);
                val_lower[sizeof(val_lower) - 1] = 0;
                for (char *p = val_lower; *p; ++p) *p = tolower((unsigned char)*p);
                if (strcmp(val_lower, "none") == 0) {
                    config->masking_handler = NULL;
                    config->masking_handler_set = 1;
                    config->socks5_mask = S5_MASK_NONE;
                    break;
                }
                if (strcmp(val_lower, "auto") == 0) {
                    config->masking_handler = NULL;
                    config->masking_handler_set = 0;
                    config->socks5_mask = S5_MASK_NONE;
                    break;
                }
                if (strcmp(val_lower, "tls") == 0) {
                    config->masking_handler = NULL;
                    config->masking_handler_set = 1;
                    config->socks5_mask = S5_MASK_TLS;
                    break;
                }
                masking_handler_t *handler = get_masking_handler_by_name(val_lower);
                if (handler == NULL) {
                    log(LL_ERROR, "Unknown masking type: %s", val);
                    exit(EXIT_FAILURE);
                }
                config->masking_handler = handler;
                config->masking_handler_set = 1;
                if (strcmp(handler->name, "STUN") == 0) config->socks5_mask = S5_MASK_STUN;
                if (strcmp(handler->name, "MEDIA") == 0) {
                    config->socks5_mask = S5_MASK_MEDIA;
                    if (config->obfuscate_bytes == 0) {
                        config->obfuscate_bytes = MEDIA_OBFUSCATE_BYTES_DEFAULT;
                    }
                }
            }
            break;
        case 'T':
            config->threads = (int)parse_int_range(val, 0, INT_MAX, "threads");
            break;
        case 'v':
            strncpy(val_lower, val, sizeof(val_lower) - 1);
            val_lower[sizeof(val_lower) - 1] = 0;
            for (char *p = val_lower; *p; ++p) *p = tolower((unsigned char)*p);
            if (strcmp(val_lower, "error") == 0) {
                verbose = LL_ERROR;
            } else if (strcmp(val_lower, "warn") == 0) {
                verbose = LL_WARN;
            } else if (strcmp(val_lower, "info") == 0) {
                verbose = LL_INFO;
            } else if (strcmp(val_lower, "debug") == 0) {
                verbose = LL_DEBUG;
            } else if (strcmp(val_lower, "trace") == 0) {
                verbose = LL_TRACE;
            } else {
                // check if it's a number
                if (is_integer(val)) {
                    verbose = atoi(val);
                    if (verbose < 0 || verbose > 4) {
                        log(LL_ERROR, "Invalid verbosity level: %s (must be one of 'ERROR', 'WARN', 'INFO', 'DEBUG', 'TRACE')", val);
                        exit(EXIT_FAILURE);
                    }
                } else {
                    log(LL_ERROR, "Invalid verbosity level: %s (must be one of 'ERROR', 'WARN', 'INFO', 'DEBUG', 'TRACE')", val);
                    exit(EXIT_FAILURE);
                }            
            }
            break;
        default:
            // should never happen
            return -1;
    }
    return 0;
}

int parse_config(int argc, char **argv, obfuscator_config_t *config)
{
    /* Parse command line arguments */
    reset_config(config);
    arg0 = argv[0]; // Save the executable name
    if (argc == 1) {
        fprintf(stderr, "No arguments provided, use \"%s --help\" command for usage information\n", argv[0]);
        return -1;
    }
    if (mini_argp_parse(argc, argv, options, config, parse_opt) != 0) {
        fprintf(stderr, "Failed to parse command line arguments\n");
        return -1;
    }

    return 0;
}

/**
 * @brief Removes leading and trailing whitespace characters from the input string.
 *
 * This function modifies the input string in place by trimming any whitespace
 * characters (such as spaces, tabs, or newlines) from both the beginning and end.
 *
 * @param s Pointer to the null-terminated string to be trimmed.
 * @return Pointer to the trimmed string.
 */
char *trim(char *s) {
    char *end;
    // Trim leading spaces, tabs, carriage returns and newlines
    while (*s && (*s == ' ' || *s == '\t' || *s == '\r' || *s == '\n')) s++;
    if (!*s) return s;
    // Trim trailing spaces, tabs, carriage returns and newlines
    end = s + strlen(s) - 1;
    while (end > s && (*end == ' ' || *end == '\t' || *end == '\r' || *end == '\n')) *end-- = 0;
    return s;
}