Added key generation support with --key-gen
Added --key-gen to allow KEY_BASE64 and HMAC_KEY_BASE64 keys to be created from reading random data from /dev/random. These keys can be placed within server access.conf files and corresponding client .fwknoprc files for SPA communications. The HMAC key is not used yet with this commit, but that is coming.
This commit is contained in:
+5
-1
@@ -43,6 +43,7 @@ enum {
|
||||
TIME_OFFSET_PLUS,
|
||||
NO_SAVE_ARGS,
|
||||
SHOW_LAST_ARGS,
|
||||
RC_FILE_PATH,
|
||||
RESOLVE_URL,
|
||||
/* Put GPG-related items below the following line */
|
||||
GPG_ENCRYPTION = 0x200,
|
||||
@@ -56,7 +57,7 @@ enum {
|
||||
|
||||
/* Our getopt_long options string.
|
||||
*/
|
||||
#define GETOPTS_OPTION_STRING "a:A:bB:C:D:f:gG:hH:lm:M:n:N:p:P:Q:rRsS:Tu:U:vV"
|
||||
#define GETOPTS_OPTION_STRING "a:A:bB:C:D:f:gG:hH:kK:lm:M:n:N:p:P:Q:rRsS:Tu:U:vV"
|
||||
|
||||
/* Our program command-line options...
|
||||
*/
|
||||
@@ -80,6 +81,8 @@ static struct option cmd_opts[] =
|
||||
{"get-key", 1, NULL, 'G'},
|
||||
{"help", 0, NULL, 'h'},
|
||||
{"http-proxy", 1, NULL, 'H'},
|
||||
{"key-gen", 0, NULL, 'k'},
|
||||
{"key-gen-file", 1, NULL, 'K'},
|
||||
{"last-cmd", 0, NULL, 'l'},
|
||||
{"nat-access", 1, NULL, 'N'},
|
||||
{"named-config", 1, NULL, 'n'},
|
||||
@@ -89,6 +92,7 @@ static struct option cmd_opts[] =
|
||||
{"server-port", 1, NULL, 'p'},
|
||||
{"server-proto", 1, NULL, 'P'},
|
||||
{"spoof-src", 1, NULL, 'Q'},
|
||||
{"rc-file", 1, NULL, RC_FILE_PATH},
|
||||
{"rand-port", 0, NULL, 'r'},
|
||||
{"resolve-ip-http", 0, NULL, 'R'},
|
||||
{"resolve-url", 1, NULL, RESOLVE_URL},
|
||||
|
||||
+86
-29
@@ -367,6 +367,39 @@ parse_rc_param(fko_cli_options_t *options, const char *var, char * val)
|
||||
if(val[0] == 'y' || val[0] == 'Y')
|
||||
options->rand_port = 1;
|
||||
}
|
||||
/* Rijndael key */
|
||||
else if(CONF_VAR_IS(var, "KEY"))
|
||||
{
|
||||
strlcpy(options->key, val, MAX_KEY_LEN);
|
||||
options->have_key = 1;
|
||||
}
|
||||
/* Rijndael key (base-64 encoded) */
|
||||
else if(CONF_VAR_IS(var, "KEY_BASE64"))
|
||||
{
|
||||
if (! is_base64((unsigned char *) val, strlen(val)))
|
||||
{
|
||||
fprintf(stderr,
|
||||
"KEY_BASE64 argument '%s' doesn't look like base64-encoded data.\n",
|
||||
val);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
strlcpy(options->key_base64, val, MAX_KEY_LEN);
|
||||
options->have_base64_key = 1;
|
||||
}
|
||||
/* HMAC key */
|
||||
else if(CONF_VAR_IS(var, "HMAC_KEY_BASE64"))
|
||||
{
|
||||
if (! is_base64((unsigned char *) val, strlen(val)))
|
||||
{
|
||||
fprintf(stderr,
|
||||
"HMAC_KEY_BASE64 argument '%s' doesn't look like base64-encoded data.\n",
|
||||
val);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
strlcpy(options->hmac_key_base64, val, MAX_KEY_LEN);
|
||||
options->have_hmac_base64_key = 1;
|
||||
}
|
||||
|
||||
/* Key file */
|
||||
else if(CONF_VAR_IS(var, "KEY_FILE"))
|
||||
{
|
||||
@@ -437,38 +470,45 @@ process_rc(fko_cli_options_t *options)
|
||||
|
||||
char *ndx, *emark, *homedir;
|
||||
|
||||
#ifdef WIN32
|
||||
homedir = getenv("USERPROFILE");
|
||||
#else
|
||||
homedir = getenv("HOME");
|
||||
#endif
|
||||
|
||||
if(homedir == NULL)
|
||||
{
|
||||
fprintf(stderr, "Warning: Unable to determine HOME directory.\n"
|
||||
" No .fwknoprc file processed.\n");
|
||||
return;
|
||||
}
|
||||
|
||||
memset(rcfile, 0x0, MAX_PATH_LEN);
|
||||
|
||||
strlcpy(rcfile, homedir, MAX_PATH_LEN);
|
||||
|
||||
rcf_offset = strlen(rcfile);
|
||||
|
||||
/* Sanity check the path to .fwknoprc.
|
||||
* The preceeding path plus the path separator and '.fwknoprc' = 11
|
||||
* cannot exceed MAX_PATH_LEN.
|
||||
*/
|
||||
if(rcf_offset > (MAX_PATH_LEN - 11))
|
||||
if(options->rc_file[0] == 0x0)
|
||||
{
|
||||
fprintf(stderr, "Warning: Path to .fwknoprc file is too long.\n"
|
||||
" No .fwknoprc file processed.\n");
|
||||
return;
|
||||
}
|
||||
#ifdef WIN32
|
||||
homedir = getenv("USERPROFILE");
|
||||
#else
|
||||
homedir = getenv("HOME");
|
||||
#endif
|
||||
|
||||
rcfile[rcf_offset] = PATH_SEP;
|
||||
strlcat(rcfile, ".fwknoprc", MAX_PATH_LEN);
|
||||
if(homedir == NULL)
|
||||
{
|
||||
fprintf(stderr, "Warning: Unable to determine HOME directory.\n"
|
||||
" No .fwknoprc file processed.\n");
|
||||
return;
|
||||
}
|
||||
|
||||
strlcpy(rcfile, homedir, MAX_PATH_LEN);
|
||||
|
||||
rcf_offset = strlen(rcfile);
|
||||
|
||||
/* Sanity check the path to .fwknoprc.
|
||||
* The preceeding path plus the path separator and '.fwknoprc' = 11
|
||||
* cannot exceed MAX_PATH_LEN.
|
||||
*/
|
||||
if(rcf_offset > (MAX_PATH_LEN - 11))
|
||||
{
|
||||
fprintf(stderr, "Warning: Path to .fwknoprc file is too long.\n"
|
||||
" No .fwknoprc file processed.\n");
|
||||
return;
|
||||
}
|
||||
|
||||
rcfile[rcf_offset] = PATH_SEP;
|
||||
strlcat(rcfile, ".fwknoprc", MAX_PATH_LEN);
|
||||
}
|
||||
else
|
||||
{
|
||||
strlcpy(rcfile, options->rc_file, MAX_PATH_LEN);
|
||||
}
|
||||
|
||||
/* Open the rc file for reading, if it does not exist, then create
|
||||
* an initial .fwknoprc file with defaults and go on.
|
||||
@@ -545,6 +585,13 @@ process_rc(fko_cli_options_t *options)
|
||||
if((ndx = strrchr(var, ':')) != NULL)
|
||||
*ndx = '\0';
|
||||
|
||||
/* Even though sscanf should automatically add a terminating
|
||||
* NULL byte, an assumption is made that the input arrays are
|
||||
* big enough, so we'll force a terminating NULL byte regardless
|
||||
*/
|
||||
var[MAX_LINE_LEN-1] = 0x0;
|
||||
val[MAX_LINE_LEN-1] = 0x0;
|
||||
|
||||
if(options->verbose > 3)
|
||||
fprintf(stderr,
|
||||
"RC FILE: %s, LINE: %s\tVar: %s, Val: '%s'\n",
|
||||
@@ -590,6 +637,7 @@ validate_options(fko_cli_options_t *options)
|
||||
* the version, and must use one of [-s|-R|-a].
|
||||
*/
|
||||
if(!options->test
|
||||
&& !options->key_gen
|
||||
&& !options->version
|
||||
&& !options->show_last_command
|
||||
&& !options->run_last_command)
|
||||
@@ -677,7 +725,7 @@ config_init(fko_cli_options_t *options, int argc, char **argv)
|
||||
*/
|
||||
set_defaults(options);
|
||||
|
||||
/* First pass over cmd_line args to see if a named-stanza in the
|
||||
/* First pass over cmd_line args to see if a named-stanza in the
|
||||
* rc file is used.
|
||||
*/
|
||||
while ((cmd_arg = getopt_long(argc, argv,
|
||||
@@ -690,6 +738,9 @@ config_init(fko_cli_options_t *options, int argc, char **argv)
|
||||
options->no_save_args = 1;
|
||||
strlcpy(options->use_rc_stanza, optarg, MAX_LINE_LEN);
|
||||
break;
|
||||
case RC_FILE_PATH:
|
||||
strlcpy(options->rc_file, optarg, MAX_PATH_LEN);
|
||||
break;
|
||||
case 'v':
|
||||
options->verbose++;
|
||||
break;
|
||||
@@ -747,6 +798,9 @@ config_init(fko_cli_options_t *options, int argc, char **argv)
|
||||
options->spa_proto = FKO_PROTO_HTTP;
|
||||
strlcpy(options->http_proxy, optarg, MAX_PATH_LEN);
|
||||
break;
|
||||
case 'k':
|
||||
options->key_gen = 1;
|
||||
break;
|
||||
case 'l':
|
||||
options->run_last_command = 1;
|
||||
break;
|
||||
@@ -798,6 +852,9 @@ config_init(fko_cli_options_t *options, int argc, char **argv)
|
||||
case 'Q':
|
||||
strlcpy(options->spoof_ip_src_str, optarg, MAX_IPV4_STR_LEN);
|
||||
break;
|
||||
case RC_FILE_PATH:
|
||||
strlcpy(options->rc_file, optarg, MAX_PATH_LEN);
|
||||
break;
|
||||
case 'r':
|
||||
options->rand_port = 1;
|
||||
break;
|
||||
|
||||
+22
-2
@@ -36,7 +36,7 @@
|
||||
|
||||
/* prototypes
|
||||
*/
|
||||
static char * get_user_pw(fko_cli_options_t *options, const int crypt_op);
|
||||
static char *get_user_pw(fko_cli_options_t *options, const int crypt_op);
|
||||
static void display_ctx(fko_ctx_t ctx);
|
||||
static void errmsg(const char *msg, const int err);
|
||||
static void show_last_command(void);
|
||||
@@ -79,9 +79,20 @@ main(int argc, char **argv)
|
||||
return(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
/* Generate Rijndael + HMAC keys from /dev/random (base64
|
||||
* encoded) and exit.
|
||||
*/
|
||||
if(options.key_gen)
|
||||
{
|
||||
fko_key_gen(options.key_base64, options.hmac_key_base64);
|
||||
printf("KEY_BASE64: %s\nHMAC_KEY_BASE64: %s\n", options.key_base64, options.hmac_key_base64);
|
||||
return(EXIT_SUCCESS);
|
||||
}
|
||||
|
||||
/* Display version info and exit.
|
||||
*/
|
||||
if (options.version) {
|
||||
if(options.version)
|
||||
{
|
||||
fko_get_version(ctx, &version);
|
||||
|
||||
fprintf(stdout, "fwknop client %s, FKO protocol version %s\n",
|
||||
@@ -738,6 +749,15 @@ get_user_pw(fko_cli_options_t *options, const int crypt_op)
|
||||
{
|
||||
pw_ptr = getpasswd_file(options->get_key_file, options->spa_server_str);
|
||||
}
|
||||
else if (options->have_key)
|
||||
{
|
||||
pw_ptr = options->key;
|
||||
}
|
||||
else if (options->have_base64_key)
|
||||
{
|
||||
fko_base64_decode(options->key_base64, (unsigned char *) options->key);
|
||||
pw_ptr = options->key;
|
||||
}
|
||||
else if (options->use_gpg)
|
||||
{
|
||||
if(crypt_op == CRYPT_OP_DECRYPT)
|
||||
|
||||
+14
-1
@@ -55,7 +55,7 @@
|
||||
#define TIME_OFFSET_DAYS 86400
|
||||
|
||||
/* For resolving the allow IP via HTTP and sending SPA packets over
|
||||
* HTTP - http://www.whatismyip.com/automation/n09230945.asp
|
||||
* HTTP - http://www.whatismyip.com/automation/n09230945.asp
|
||||
#define HTTP_RESOLVE_HOST "www.whatismyip.com"
|
||||
#define HTTP_RESOLVE_URL "/automation/n09230945.asp"
|
||||
* --DSS Note: The whatismyip.com site has some usage restrictions.
|
||||
@@ -70,6 +70,7 @@
|
||||
#define MAX_HOSTNAME_LEN 70
|
||||
#define MAX_URL_HOST_LEN 256
|
||||
#define MAX_URL_PATH_LEN 1024
|
||||
#define MAX_KEY_LEN 128
|
||||
|
||||
/* fwknop client configuration parameters and values
|
||||
*/
|
||||
@@ -77,6 +78,8 @@ typedef struct fko_cli_options
|
||||
{
|
||||
char config_file[MAX_PATH_LEN];
|
||||
char access_str[MAX_PATH_LEN];
|
||||
char rc_file[MAX_PATH_LEN];
|
||||
char key_gen_file[MAX_PATH_LEN];
|
||||
char server_command[MAX_LINE_LEN];
|
||||
char get_key_file[MAX_PATH_LEN];
|
||||
char save_packet_file[MAX_PATH_LEN];
|
||||
@@ -93,6 +96,15 @@ typedef struct fko_cli_options
|
||||
char gpg_signer_key[MAX_GPG_KEY_ID];
|
||||
char gpg_home_dir[MAX_PATH_LEN];
|
||||
|
||||
/* Encryption keys read from a .fwknoprc stanza
|
||||
*/
|
||||
char key[MAX_KEY_LEN+1];
|
||||
char key_base64[MAX_KEY_LEN+1];
|
||||
char hmac_key_base64[MAX_KEY_LEN+1];
|
||||
int have_key;
|
||||
int have_base64_key;
|
||||
int have_hmac_base64_key;
|
||||
|
||||
/* NAT access
|
||||
*/
|
||||
char nat_access_str[MAX_PATH_LEN];
|
||||
@@ -128,6 +140,7 @@ typedef struct fko_cli_options
|
||||
int time_offset_plus;
|
||||
int time_offset_minus;
|
||||
int fw_timeout;
|
||||
int key_gen;
|
||||
|
||||
char use_rc_stanza[MAX_LINE_LEN];
|
||||
unsigned char got_named_stanza;
|
||||
|
||||
+3
-5
@@ -40,14 +40,12 @@
|
||||
#include "fwknop_common.h"
|
||||
#include "getpasswd.h"
|
||||
|
||||
#define MAX_PASS_LEN 128
|
||||
|
||||
/* Function for accepting password input from users
|
||||
*/
|
||||
char*
|
||||
getpasswd(const char *prompt)
|
||||
{
|
||||
static char pwbuf[MAX_PASS_LEN + 1] = {0};
|
||||
static char pwbuf[MAX_KEY_LEN + 1] = {0};
|
||||
char *ptr;
|
||||
int c;
|
||||
|
||||
@@ -109,7 +107,7 @@ getpasswd(const char *prompt)
|
||||
while((c = getc(fp)) != EOF && c != '\n')
|
||||
{
|
||||
#endif
|
||||
if(ptr < &pwbuf[MAX_PASS_LEN])
|
||||
if(ptr < &pwbuf[MAX_KEY_LEN])
|
||||
*ptr++ = c;
|
||||
}
|
||||
|
||||
@@ -146,7 +144,7 @@ getpasswd_file(const char *pw_file, const char *server_str)
|
||||
FILE *pwfile_ptr;
|
||||
unsigned int numLines = 0, i = 0, found_dst;
|
||||
|
||||
static char pwbuf[MAX_PASS_LEN + 1] = {0};
|
||||
static char pwbuf[MAX_KEY_LEN + 1] = {0};
|
||||
char conf_line_buf[MAX_LINE_LEN] = {0};
|
||||
char tmp_char_buf[MAX_LINE_LEN] = {0};
|
||||
char *lptr;
|
||||
|
||||
+21
-2
@@ -28,8 +28,7 @@
|
||||
*
|
||||
*****************************************************************************
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "fwknop_common.h"
|
||||
#include "utils.h"
|
||||
|
||||
/* Generic hex dump function.
|
||||
@@ -69,5 +68,25 @@ hex_dump(const unsigned char *data, const int size)
|
||||
}
|
||||
}
|
||||
|
||||
/* Determine if a buffer contains only characters from the base64
|
||||
* encoding set
|
||||
*/
|
||||
int
|
||||
is_base64(const unsigned char *buf, const unsigned short int len)
|
||||
{
|
||||
unsigned short int i;
|
||||
int rv = 1;
|
||||
|
||||
for(i=0; i<len; i++)
|
||||
{
|
||||
if(!(isalnum(buf[i]) || buf[i] == '/' || buf[i] == '+' || buf[i] == '='))
|
||||
{
|
||||
rv = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return rv;
|
||||
}
|
||||
|
||||
/***EOF***/
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
/* Prototypes
|
||||
*/
|
||||
void hex_dump(const unsigned char *data, const int size);
|
||||
int is_base64(const unsigned char *buf, const unsigned short int len);
|
||||
size_t strlcat(char *dst, const char *src, size_t siz);
|
||||
size_t strlcpy(char *dst, const char *src, size_t siz);
|
||||
|
||||
|
||||
+1
-1
@@ -107,7 +107,7 @@ b64_encode(unsigned char *in, char *out, int in_len)
|
||||
return(dst - out);
|
||||
}
|
||||
|
||||
/* Strip trailing equals ("=") charcters from a base64-encode
|
||||
/* Strip trailing equals ("=") charcters from a base64-encoded
|
||||
* message digest.
|
||||
*/
|
||||
void
|
||||
|
||||
+1
-1
@@ -50,7 +50,7 @@
|
||||
|
||||
/* Get random data.
|
||||
*/
|
||||
static void
|
||||
void
|
||||
get_random_data(unsigned char *data, const size_t len)
|
||||
{
|
||||
uint32_t i;
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
*/
|
||||
#define PREDICT_ENCSIZE(x) (1+(x>>4)+(x&0xf?1:0))<<4
|
||||
|
||||
void get_random_data(unsigned char *data, const size_t len);
|
||||
size_t rij_encrypt(unsigned char *in, size_t len,
|
||||
const char *key, unsigned char *out, int encryption_mode);
|
||||
size_t rij_decrypt(unsigned char *in, size_t len,
|
||||
|
||||
@@ -232,6 +232,9 @@ DLL_API int fko_set_spa_data(fko_ctx_t ctx, const char *enc_msg);
|
||||
*/
|
||||
DLL_API const char* fko_errstr(const int err_code);
|
||||
DLL_API int fko_encryption_type(const char *enc_data);
|
||||
DLL_API int fko_key_gen(char *key_base64, char *hmac_key_base64);
|
||||
DLL_API int fko_base64_encode(unsigned char *in, char *out, int in_len);
|
||||
DLL_API int fko_base64_decode(const char *in, unsigned char *out);
|
||||
|
||||
DLL_API int fko_encode_spa_data(fko_ctx_t ctx);
|
||||
DLL_API int fko_decode_spa_data(fko_ctx_t ctx);
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
#include "fko_common.h"
|
||||
#include "fko.h"
|
||||
#include "cipher_funcs.h"
|
||||
#include "base64.h"
|
||||
|
||||
/* Initialize an fko context.
|
||||
*/
|
||||
@@ -313,6 +314,40 @@ fko_destroy(fko_ctx_t ctx)
|
||||
free(ctx);
|
||||
}
|
||||
|
||||
/* Generate Rijndael and HMAC keys from /dev/random and base-64
|
||||
* encode them
|
||||
*/
|
||||
int
|
||||
fko_key_gen(char *key_base64, char *hmac_key_base64)
|
||||
{
|
||||
unsigned char key[RIJNDAEL_MAX_KEYSIZE];
|
||||
unsigned char hmac_key[RIJNDAEL_MAX_KEYSIZE];
|
||||
|
||||
get_random_data(key, RIJNDAEL_MAX_KEYSIZE);
|
||||
get_random_data(hmac_key, RIJNDAEL_MAX_KEYSIZE);
|
||||
|
||||
b64_encode(key, key_base64, RIJNDAEL_MAX_KEYSIZE);
|
||||
b64_encode(hmac_key, hmac_key_base64, RIJNDAEL_MAX_KEYSIZE);
|
||||
|
||||
return(FKO_SUCCESS);
|
||||
}
|
||||
|
||||
/* Provide an FKO wrapper around base64 encode/decode functions
|
||||
*/
|
||||
int
|
||||
fko_base64_encode(unsigned char *in, char *out, int in_len)
|
||||
{
|
||||
b64_encode(in, out, in_len);
|
||||
return(FKO_SUCCESS);
|
||||
}
|
||||
|
||||
int
|
||||
fko_base64_decode(const char *in, unsigned char *out)
|
||||
{
|
||||
b64_decode(in, out);
|
||||
return(FKO_SUCCESS);
|
||||
}
|
||||
|
||||
/* Return the fko version
|
||||
*/
|
||||
int
|
||||
|
||||
@@ -578,6 +578,12 @@ free_acc_stanza_data(acc_stanza_t *acc)
|
||||
if(acc->key != NULL)
|
||||
free(acc->key);
|
||||
|
||||
if(acc->key_base64 != NULL)
|
||||
free(acc->key_base64);
|
||||
|
||||
if(acc->hmac_key_base64 != NULL)
|
||||
free(acc->hmac_key_base64);
|
||||
|
||||
if(acc->cmd_exec_user != NULL)
|
||||
free(acc->cmd_exec_user);
|
||||
|
||||
@@ -744,6 +750,7 @@ static int
|
||||
acc_data_is_valid(const acc_stanza_t *acc)
|
||||
{
|
||||
if((acc->key == NULL || !strlen(acc->key))
|
||||
&& (acc->key_base64 == NULL || !strlen(acc->key_base64))
|
||||
&& (acc->gpg_decrypt_pw == NULL || !strlen(acc->gpg_decrypt_pw)))
|
||||
{
|
||||
fprintf(stderr,
|
||||
@@ -826,6 +833,13 @@ parse_access_file(fko_srv_options_t *opts)
|
||||
if((ndx = strrchr(var, ':')) != NULL)
|
||||
*ndx = '\0';
|
||||
|
||||
/* Even though sscanf should automatically add a terminating
|
||||
* NULL byte, an assumption is made that the input arrays are
|
||||
* big enough, so we'll force a terminating NULL byte regardless
|
||||
*/
|
||||
var[MAX_LINE_LEN-1] = 0x0;
|
||||
val[MAX_LINE_LEN-1] = 0x0;
|
||||
|
||||
/*
|
||||
*/
|
||||
if(opts->verbose > 3)
|
||||
@@ -888,6 +902,42 @@ parse_access_file(fko_srv_options_t *opts)
|
||||
}
|
||||
add_acc_string(&(curr_acc->key), val);
|
||||
}
|
||||
else if(CONF_VAR_IS(var, "KEY_BASE64"))
|
||||
{
|
||||
if(strcasecmp(val, "__CHANGEME__") == 0)
|
||||
{
|
||||
fprintf(stderr,
|
||||
"[*] KEY_BASE64 value is not properly set in stanza source '%s' in access file: '%s'\n",
|
||||
curr_acc->source, opts->config[CONF_ACCESS_FILE]);
|
||||
clean_exit(opts, NO_FW_CLEANUP, EXIT_FAILURE);
|
||||
}
|
||||
if (! is_base64((unsigned char *) val, strlen(val)))
|
||||
{
|
||||
fprintf(stderr,
|
||||
"KEY_BASE64 argument '%s' doesn't look like base64-encoded data.\n",
|
||||
val);
|
||||
clean_exit(opts, NO_FW_CLEANUP, EXIT_FAILURE);
|
||||
}
|
||||
add_acc_string(&(curr_acc->key_base64), val);
|
||||
}
|
||||
else if(CONF_VAR_IS(var, "HMAC_KEY_BASE64"))
|
||||
{
|
||||
if(strcasecmp(val, "__CHANGEME__") == 0)
|
||||
{
|
||||
fprintf(stderr,
|
||||
"[*] HMAC_KEY_BASE64 value is not properly set in stanza source '%s' in access file: '%s'\n",
|
||||
curr_acc->source, opts->config[CONF_ACCESS_FILE]);
|
||||
clean_exit(opts, NO_FW_CLEANUP, EXIT_FAILURE);
|
||||
}
|
||||
if (! is_base64((unsigned char *) val, strlen(val)))
|
||||
{
|
||||
fprintf(stderr,
|
||||
"HMAC_KEY_BASE64 argument '%s' doesn't look like base64-encoded data.\n",
|
||||
val);
|
||||
clean_exit(opts, NO_FW_CLEANUP, EXIT_FAILURE);
|
||||
}
|
||||
add_acc_string(&(curr_acc->hmac_key_base64), val);
|
||||
}
|
||||
else if(CONF_VAR_IS(var, "FW_ACCESS_TIMEOUT"))
|
||||
{
|
||||
add_acc_int(&(curr_acc->fw_access_timeout), val);
|
||||
|
||||
@@ -267,6 +267,8 @@ typedef struct acc_stanza
|
||||
char *restrict_ports;
|
||||
acc_port_list_t *rport_list;
|
||||
char *key;
|
||||
char *key_base64;
|
||||
char *hmac_key_base64;
|
||||
int fw_access_timeout;
|
||||
unsigned char enable_cmd_exec;
|
||||
char *cmd_exec_user;
|
||||
|
||||
@@ -242,8 +242,25 @@ incoming_spa(fko_srv_options_t *opts)
|
||||
if(enc_type == FKO_ENCRYPTION_RIJNDAEL)
|
||||
{
|
||||
if(acc->key != NULL)
|
||||
{
|
||||
res = fko_new_with_data(&ctx,
|
||||
(char *)spa_pkt->packet_data, acc->key, acc->encryption_mode);
|
||||
}
|
||||
else if (acc->key_base64 != NULL)
|
||||
{
|
||||
if ((acc->key = strdup(acc->key_base64)) == NULL)
|
||||
{
|
||||
log_msg(LOG_ERR,
|
||||
"Fatal memory allocation error copying key_base64 -> key: %s",
|
||||
acc->key_base64
|
||||
);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
memset(acc->key, 0x0, strlen(acc->key_base64));
|
||||
fko_base64_decode(acc->key_base64, (unsigned char *) acc->key);
|
||||
res = fko_new_with_data(&ctx,
|
||||
(char *)spa_pkt->packet_data, acc->key, acc->encryption_mode);
|
||||
}
|
||||
else
|
||||
{
|
||||
log_msg(LOG_ERR,
|
||||
|
||||
+1
-1
@@ -164,7 +164,7 @@ is_valid_dir(const char *path)
|
||||
* encoding set
|
||||
*/
|
||||
int
|
||||
is_base64(const unsigned char *buf, unsigned short int len)
|
||||
is_base64(const unsigned char *buf, const unsigned short int len)
|
||||
{
|
||||
unsigned short int i;
|
||||
int rv = 1;
|
||||
|
||||
+1
-1
@@ -60,7 +60,7 @@
|
||||
*/
|
||||
void hex_dump(const unsigned char *data, const int size);
|
||||
char* dump_ctx(fko_ctx_t ctx);
|
||||
int is_base64(const unsigned char *buf, unsigned short int len);
|
||||
int is_base64(const unsigned char *buf, const unsigned short int len);
|
||||
int is_valid_dir(const char *path);
|
||||
|
||||
size_t strlcat(char *dst, const char *src, size_t siz);
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
SOURCE: ANY;
|
||||
KEY_BASE64: w8KMBQlKf4UCARNQmi25DPxw4xAUaRs21nf8nbRNzUo=;
|
||||
FW_ACCESS_TIMEOUT: 3;
|
||||
@@ -0,0 +1,71 @@
|
||||
# .fwknoprc
|
||||
##############################################################################
|
||||
#
|
||||
# Firewall Knock Operator (fwknop) client rc file.
|
||||
#
|
||||
# This file contains user-specific fwknop client configuration default
|
||||
# and named parameter sets for specific invocations of the fwknop client.
|
||||
#
|
||||
# Each section (or stanza) is identified and started by a line in this
|
||||
# file that contains a single identifier surrounded by square brackets.
|
||||
#
|
||||
# The parameters within the stanza typicaly match corresponding client
|
||||
# command-line parameters.
|
||||
#
|
||||
# The first one should always be `[default]' as it defines the global
|
||||
# default settings for the user. These override the program defaults
|
||||
# for these parameter. If a named stanza is used, its entries will
|
||||
# override any of the default. Command-line options will trump them
|
||||
# all.
|
||||
#
|
||||
# Subsequent stanzas will have only the overriding and destination
|
||||
# specific parameters.
|
||||
#
|
||||
# Lines starting with `#' and empty lines are ignored.
|
||||
#
|
||||
# See the fwknop.8 man page for a complete list of valid parameters
|
||||
# and their values.
|
||||
#
|
||||
##############################################################################
|
||||
#
|
||||
# We start with the 'default' stanza. Uncomment and edit for your
|
||||
# preferences. The client will use its build-in default for those items
|
||||
# that are commented out.
|
||||
#
|
||||
[default]
|
||||
|
||||
#DIGEST_TYPE sha256
|
||||
#FW_TIMEOUT 30
|
||||
#SPA_SERVER_PORT 62201
|
||||
#SPA_SERVER_PROTO udp
|
||||
#ALLOW_IP <ip addr>
|
||||
#SPOOF_USER <username>
|
||||
#SPOOF_SOURCE_IP <IPaddr>
|
||||
#TIME_OFFSET 0
|
||||
#USE_GPG N
|
||||
#GPG_HOMEDIR /path/to/.gnupg
|
||||
#GPG_SIGNER <signer ID>
|
||||
#GPG_RECIPIENT <recipient ID>
|
||||
KEY_BASE64 w8KMBQlKf4UCARNQmi25DPxw4xAUaRs21nf8nbRNzUo=
|
||||
|
||||
# User-provided named stanzas:
|
||||
|
||||
# Example for a destination server of 192.168.1.20 to open access to
|
||||
# SSH for an IP that is resoved exteranlly, and one with a NAT request
|
||||
# for a specific source IP that maps port 8088 on the server
|
||||
# to port 88 on 192.168.1.55 with timeout.
|
||||
#
|
||||
#[myssh]
|
||||
#SPA_SERVER 192.168.1.20
|
||||
#ACCESS tcp/22
|
||||
#ALLOW_IP resolve
|
||||
#
|
||||
#[mynatreq]
|
||||
#SPA_SERVER 192.168.1.20
|
||||
#ACCESS tcp/8088
|
||||
#ALLOW_IP 10.21.2.6
|
||||
#NAT_ACCESS 192.168.1.55,88
|
||||
#CLIENT_TIMEOUT 60
|
||||
#
|
||||
|
||||
###EOF###
|
||||
@@ -0,0 +1,71 @@
|
||||
# .fwknoprc
|
||||
##############################################################################
|
||||
#
|
||||
# Firewall Knock Operator (fwknop) client rc file.
|
||||
#
|
||||
# This file contains user-specific fwknop client configuration default
|
||||
# and named parameter sets for specific invocations of the fwknop client.
|
||||
#
|
||||
# Each section (or stanza) is identified and started by a line in this
|
||||
# file that contains a single identifier surrounded by square brackets.
|
||||
#
|
||||
# The parameters within the stanza typicaly match corresponding client
|
||||
# command-line parameters.
|
||||
#
|
||||
# The first one should always be `[default]' as it defines the global
|
||||
# default settings for the user. These override the program defaults
|
||||
# for these parameter. If a named stanza is used, its entries will
|
||||
# override any of the default. Command-line options will trump them
|
||||
# all.
|
||||
#
|
||||
# Subsequent stanzas will have only the overriding and destination
|
||||
# specific parameters.
|
||||
#
|
||||
# Lines starting with `#' and empty lines are ignored.
|
||||
#
|
||||
# See the fwknop.8 man page for a complete list of valid parameters
|
||||
# and their values.
|
||||
#
|
||||
##############################################################################
|
||||
#
|
||||
# We start with the 'default' stanza. Uncomment and edit for your
|
||||
# preferences. The client will use its build-in default for those items
|
||||
# that are commented out.
|
||||
#
|
||||
[default]
|
||||
|
||||
#DIGEST_TYPE sha256
|
||||
#FW_TIMEOUT 30
|
||||
#SPA_SERVER_PORT 62201
|
||||
#SPA_SERVER_PROTO udp
|
||||
#ALLOW_IP <ip addr>
|
||||
#SPOOF_USER <username>
|
||||
#SPOOF_SOURCE_IP <IPaddr>
|
||||
#TIME_OFFSET 0
|
||||
#USE_GPG N
|
||||
#GPG_HOMEDIR /path/to/.gnupg
|
||||
#GPG_SIGNER <signer ID>
|
||||
#GPG_RECIPIENT <recipient ID>
|
||||
KEY fwknoptest
|
||||
|
||||
# User-provided named stanzas:
|
||||
|
||||
# Example for a destination server of 192.168.1.20 to open access to
|
||||
# SSH for an IP that is resoved exteranlly, and one with a NAT request
|
||||
# for a specific source IP that maps port 8088 on the server
|
||||
# to port 88 on 192.168.1.55 with timeout.
|
||||
#
|
||||
#[myssh]
|
||||
#SPA_SERVER 192.168.1.20
|
||||
#ACCESS tcp/22
|
||||
#ALLOW_IP resolve
|
||||
#
|
||||
#[mynatreq]
|
||||
#SPA_SERVER 192.168.1.20
|
||||
#ACCESS tcp/8088
|
||||
#ALLOW_IP 10.21.2.6
|
||||
#NAT_ACCESS 192.168.1.55,88
|
||||
#CLIENT_TIMEOUT 60
|
||||
#
|
||||
|
||||
###EOF###
|
||||
@@ -0,0 +1,73 @@
|
||||
# .fwknoprc
|
||||
##############################################################################
|
||||
#
|
||||
# Firewall Knock Operator (fwknop) client rc file.
|
||||
#
|
||||
# This file contains user-specific fwknop client configuration default
|
||||
# and named parameter sets for specific invocations of the fwknop client.
|
||||
#
|
||||
# Each section (or stanza) is identified and started by a line in this
|
||||
# file that contains a single identifier surrounded by square brackets.
|
||||
#
|
||||
# The parameters within the stanza typicaly match corresponding client
|
||||
# command-line parameters.
|
||||
#
|
||||
# The first one should always be `[default]' as it defines the global
|
||||
# default settings for the user. These override the program defaults
|
||||
# for these parameter. If a named stanza is used, its entries will
|
||||
# override any of the default. Command-line options will trump them
|
||||
# all.
|
||||
#
|
||||
# Subsequent stanzas will have only the overriding and destination
|
||||
# specific parameters.
|
||||
#
|
||||
# Lines starting with `#' and empty lines are ignored.
|
||||
#
|
||||
# See the fwknop.8 man page for a complete list of valid parameters
|
||||
# and their values.
|
||||
#
|
||||
##############################################################################
|
||||
#
|
||||
# We start with the 'default' stanza. Uncomment and edit for your
|
||||
# preferences. The client will use its build-in default for those items
|
||||
# that are commented out.
|
||||
#
|
||||
[default]
|
||||
|
||||
#DIGEST_TYPE sha256
|
||||
#FW_TIMEOUT 30
|
||||
#SPA_SERVER_PORT 62201
|
||||
#SPA_SERVER_PROTO udp
|
||||
#ALLOW_IP <ip addr>
|
||||
#SPOOF_USER <username>
|
||||
#SPOOF_SOURCE_IP <IPaddr>
|
||||
#TIME_OFFSET 0
|
||||
#USE_GPG N
|
||||
#GPG_HOMEDIR /path/to/.gnupg
|
||||
#GPG_SIGNER <signer ID>
|
||||
#GPG_RECIPIENT <recipient ID>
|
||||
|
||||
# User-provided named stanzas:
|
||||
|
||||
# Example for a destination server of 192.168.1.20 to open access to
|
||||
# SSH for an IP that is resoved exteranlly, and one with a NAT request
|
||||
# for a specific source IP that maps port 8088 on the server
|
||||
# to port 88 on 192.168.1.55 with timeout.
|
||||
#
|
||||
#[myssh]
|
||||
#SPA_SERVER 192.168.1.20
|
||||
#ACCESS tcp/22
|
||||
#ALLOW_IP resolve
|
||||
#
|
||||
#[mynatreq]
|
||||
#SPA_SERVER 192.168.1.20
|
||||
#ACCESS tcp/8088
|
||||
#ALLOW_IP 10.21.2.6
|
||||
#NAT_ACCESS 192.168.1.55,88
|
||||
#CLIENT_TIMEOUT 60
|
||||
#
|
||||
|
||||
[testssh]
|
||||
KEY fwknoptest;
|
||||
|
||||
###EOF###
|
||||
@@ -23,6 +23,7 @@ my $gpg_client_home_dir = "$conf_dir/client-gpg";
|
||||
my $nat_conf = "$conf_dir/nat_fwknopd.conf";
|
||||
my $default_conf = "$conf_dir/default_fwknopd.conf";
|
||||
my $default_access_conf = "$conf_dir/default_access.conf";
|
||||
my $base64_key_access_conf = "$conf_dir/base64_key_access.conf";
|
||||
my $ecb_mode_access_conf = "$conf_dir/ecb_mode_access.conf";
|
||||
my $ctr_mode_access_conf = "$conf_dir/ctr_mode_access.conf";
|
||||
my $cfb_mode_access_conf = "$conf_dir/cfb_mode_access.conf";
|
||||
@@ -36,6 +37,10 @@ my $force_nat_access_conf = "$conf_dir/force_nat_access.conf";
|
||||
my $gpg_access_conf = "$conf_dir/gpg_access.conf";
|
||||
my $default_digest_file = "$run_dir/digest.cache";
|
||||
my $default_pid_file = "$run_dir/fwknopd.pid";
|
||||
my $tmp_rc_file = "$run_dir/fwknoprc";
|
||||
my $rc_file_default_key = "$conf_dir/fwknoprc_with_default_key";
|
||||
my $rc_file_default_base64_key = "$conf_dir/fwknoprc_with_default_base64_key";
|
||||
my $rc_file_named_key = "$conf_dir/fwknoprc_with_named_key";
|
||||
my $open_ports_access_conf = "$conf_dir/open_ports_access.conf";
|
||||
my $multi_gpg_access_conf = "$conf_dir/multi_gpg_access.conf";
|
||||
my $multi_stanzas_access_conf = "$conf_dir/multi_stanzas_access.conf";
|
||||
@@ -145,11 +150,20 @@ my $default_client_args = "LD_LIBRARY_PATH=$lib_dir $valgrind_str " .
|
||||
"$fwknopCmd -A tcp/22 -a $fake_ip -D $loopback_ip --get-key " .
|
||||
"$local_key_file --no-save-args --verbose --verbose";
|
||||
|
||||
my $default_client_args_no_get_key = "LD_LIBRARY_PATH=$lib_dir " .
|
||||
"$valgrind_str $fwknopCmd -A tcp/22 -a $fake_ip -D $loopback_ip " .
|
||||
"--no-save-args --verbose --verbose";
|
||||
|
||||
my $default_client_gpg_args = "$default_client_args " .
|
||||
"--gpg-recipient-key $gpg_server_key " .
|
||||
"--gpg-signer-key $gpg_client_key " .
|
||||
"--gpg-home-dir $gpg_client_home_dir";
|
||||
|
||||
my $default_client_gpg_args_no_get_key = "$default_client_args_no_get_key " .
|
||||
"--gpg-recipient-key $gpg_server_key " .
|
||||
"--gpg-signer-key $gpg_client_key " .
|
||||
"--gpg-home-dir $gpg_client_home_dir";
|
||||
|
||||
my $default_server_conf_args = "-c $default_conf -a $default_access_conf " .
|
||||
"-d $default_digest_file -p $default_pid_file";
|
||||
|
||||
@@ -612,6 +626,82 @@ my @tests = (
|
||||
'fw_rule_removed' => $NEW_RULE_REMOVED,
|
||||
'fatal' => $NO
|
||||
},
|
||||
{
|
||||
'category' => 'Rijndael SPA',
|
||||
'subcategory' => 'client+server',
|
||||
'detail' => 'create rc file (tcp/22 ssh)',
|
||||
'err_msg' => 'could not complete SPA cycle',
|
||||
'function' => \&spa_cycle,
|
||||
'cmdline' => "$default_client_args --rc-file $tmp_rc_file",
|
||||
'fwknopd_cmdline' => "LD_LIBRARY_PATH=$lib_dir $valgrind_str " .
|
||||
"$fwknopdCmd $default_server_conf_args $intf_str",
|
||||
'fw_rule_created' => $NEW_RULE_REQUIRED,
|
||||
'fw_rule_removed' => $NEW_RULE_REMOVED,
|
||||
'fatal' => $NO
|
||||
},
|
||||
{
|
||||
'category' => 'basic operations',
|
||||
'subcategory' => 'client',
|
||||
'detail' => "rc file created",
|
||||
'err_msg' => "rc file $tmp_rc_file does not exist",
|
||||
'function' => \&rc_file_exists,
|
||||
'fatal' => $NO
|
||||
},
|
||||
{
|
||||
'category' => 'Rijndael SPA',
|
||||
'subcategory' => 'client+server',
|
||||
'detail' => 'rc file default key (tcp/22 ssh)',
|
||||
'err_msg' => 'could not complete SPA cycle',
|
||||
'function' => \&spa_cycle,
|
||||
'cmdline' => "$default_client_args_no_get_key " .
|
||||
"--rc-file $rc_file_default_key",
|
||||
'fwknopd_cmdline' => "LD_LIBRARY_PATH=$lib_dir $valgrind_str " .
|
||||
"$fwknopdCmd $default_server_conf_args $intf_str",
|
||||
'fw_rule_created' => $NEW_RULE_REQUIRED,
|
||||
'fw_rule_removed' => $NEW_RULE_REMOVED,
|
||||
'fatal' => $NO
|
||||
},
|
||||
{
|
||||
'category' => 'Rijndael SPA',
|
||||
'subcategory' => 'client+server',
|
||||
'detail' => 'rc file base64 key (tcp/22 ssh)',
|
||||
'err_msg' => 'could not complete SPA cycle',
|
||||
'function' => \&spa_cycle,
|
||||
'cmdline' => "$default_client_args_no_get_key " .
|
||||
"--rc-file $rc_file_default_base64_key",
|
||||
'fwknopd_cmdline' => "LD_LIBRARY_PATH=$lib_dir $valgrind_str " .
|
||||
"$fwknopdCmd -c $default_conf -a $base64_key_access_conf " .
|
||||
"-d $default_digest_file -p $default_pid_file $intf_str",
|
||||
'fw_rule_created' => $NEW_RULE_REQUIRED,
|
||||
'fw_rule_removed' => $NEW_RULE_REMOVED,
|
||||
'fatal' => $NO
|
||||
},
|
||||
{
|
||||
'category' => 'Rijndael SPA',
|
||||
'subcategory' => 'client+server',
|
||||
'detail' => 'rc file named key (tcp/22 ssh)',
|
||||
'err_msg' => 'could not complete SPA cycle',
|
||||
'function' => \&spa_cycle,
|
||||
'cmdline' => "$default_client_args_no_get_key " .
|
||||
"--rc-file $rc_file_named_key -n testssh",
|
||||
'fwknopd_cmdline' => "LD_LIBRARY_PATH=$lib_dir $valgrind_str " .
|
||||
"$fwknopdCmd $default_server_conf_args $intf_str",
|
||||
'fw_rule_created' => $NEW_RULE_REQUIRED,
|
||||
'fw_rule_removed' => $NEW_RULE_REMOVED,
|
||||
'fatal' => $NO
|
||||
},
|
||||
{
|
||||
'category' => 'Rijndael SPA',
|
||||
'subcategory' => 'client+server',
|
||||
'detail' => 'rc file invalid stanza (tcp/22 ssh)',
|
||||
'err_msg' => 'SPA packet generated/accepted',
|
||||
'function' => \&generic_exec,
|
||||
'cmdline' => "$default_client_args_no_get_key " .
|
||||
"--rc-file $rc_file_named_key -n invalidstanza",
|
||||
'positive_output_matches' => [qr/Named\sconfiguration.*not\sfound/],
|
||||
'fatal' => $NO
|
||||
},
|
||||
|
||||
{
|
||||
'category' => 'Rijndael SPA',
|
||||
'subcategory' => 'client+server',
|
||||
@@ -1201,6 +1291,32 @@ my @tests = (
|
||||
'fw_rule_removed' => $NEW_RULE_REMOVED,
|
||||
'fatal' => $NO
|
||||
},
|
||||
{
|
||||
'category' => 'GnuPG (GPG) SPA',
|
||||
'subcategory' => 'client+server',
|
||||
'detail' => 'rc file default key (tcp/22 ssh)',
|
||||
'err_msg' => 'could not complete SPA cycle',
|
||||
'function' => \&spa_cycle,
|
||||
'cmdline' => "$default_client_gpg_args_no_get_key " .
|
||||
"--rc-file $rc_file_default_key",
|
||||
'fwknopd_cmdline' => $default_server_gpg_args,
|
||||
'fw_rule_created' => $NEW_RULE_REQUIRED,
|
||||
'fw_rule_removed' => $NEW_RULE_REMOVED,
|
||||
'fatal' => $NO
|
||||
},
|
||||
{
|
||||
'category' => 'GnuPG (GPG) SPA',
|
||||
'subcategory' => 'client+server',
|
||||
'detail' => 'rc file named key (tcp/22 ssh)',
|
||||
'err_msg' => 'could not complete SPA cycle',
|
||||
'function' => \&spa_cycle,
|
||||
'cmdline' => "$default_client_gpg_args_no_get_key " .
|
||||
"--rc-file $rc_file_named_key -n testssh",
|
||||
'fwknopd_cmdline' => $default_server_gpg_args,
|
||||
'fw_rule_created' => $NEW_RULE_REQUIRED,
|
||||
'fw_rule_removed' => $NEW_RULE_REMOVED,
|
||||
'fatal' => $NO
|
||||
},
|
||||
{
|
||||
'category' => 'GnuPG (GPG) SPA',
|
||||
'subcategory' => 'client+server',
|
||||
@@ -1683,6 +1799,18 @@ sub spa_cycle() {
|
||||
$rv = 0 if $fw_rule_removed;
|
||||
}
|
||||
|
||||
if ($test_hr->{'client_positive_output_matches'}) {
|
||||
$rv = 0 unless &file_find_regex(
|
||||
$test_hr->{'client_positive_output_matches'},
|
||||
$current_test_file);
|
||||
}
|
||||
|
||||
if ($test_hr->{'client_negative_output_matches'}) {
|
||||
$rv = 0 if &file_find_regex(
|
||||
$test_hr->{'client_negative_output_matches'},
|
||||
$current_test_file);
|
||||
}
|
||||
|
||||
if ($test_hr->{'server_positive_output_matches'}) {
|
||||
$rv = 0 unless &file_find_regex(
|
||||
$test_hr->{'server_positive_output_matches'},
|
||||
@@ -2256,6 +2384,24 @@ sub send_packets() {
|
||||
return;
|
||||
}
|
||||
|
||||
sub rc_file_exists() {
|
||||
my $test_hr = shift;
|
||||
|
||||
my $rv = 1;
|
||||
|
||||
if (-e $tmp_rc_file) {
|
||||
$rv = 0 unless &file_find_regex([qr/This\sfile\scontains/],
|
||||
$tmp_rc_file);
|
||||
} else {
|
||||
&write_test_file("[-] $tmp_rc_file does not exist.\n",
|
||||
$current_test_file);
|
||||
$rv = 0;
|
||||
}
|
||||
|
||||
return $rv;
|
||||
}
|
||||
|
||||
|
||||
sub generic_exec() {
|
||||
my $test_hr = shift;
|
||||
|
||||
@@ -2623,6 +2769,9 @@ sub init() {
|
||||
if (-e "$output_dir/init") {
|
||||
unlink "$output_dir/init" or die $!;
|
||||
}
|
||||
if (-e $tmp_rc_file) {
|
||||
unlink $tmp_rc_file or die $!;
|
||||
}
|
||||
|
||||
if (-e $logfile) {
|
||||
unlink $logfile or die $!;
|
||||
|
||||
Reference in New Issue
Block a user