Compare commits

...

13 Commits

Author SHA1 Message Date
Adrien Gallouët
0a797b4f5f Try value 42 for mptcp (fallback to 26) 2017-06-07 11:27:44 +02:00
Adrien Gallouët
8cb849fbc3 Update pkg.m4 2017-06-07 11:26:22 +02:00
Adrien Gallouët
13f9f4c896 Fix printf format 2016-07-20 15:32:22 +00:00
Adrien Gallouët
cfd7af9241 Add .build.sh 2016-07-11 11:13:29 +00:00
Adrien Gallouët
c81592fcc5 Don't fd_set_nonblock(-1) 2016-06-23 12:15:01 +00:00
Adrien Gallouët
b4a311cdc8 Check for clock_gettime() 2016-06-15 14:12:21 +00:00
Adrien Gallouët
08617d0017 Fix macosx build 2016-06-15 14:01:20 +00:00
Adrien Gallouët
38cd3b0371 Try to do a more accurate bench 2016-06-15 09:20:18 +00:00
Adrien Gallouët
5944e61dfe Fix bench output 2016-06-15 09:10:57 +00:00
Adrien Gallouët
585b2b08bc Be more verbose on kx errors 2016-06-06 13:37:05 +00:00
angt
a3aa6fc4fb Add option bench 2016-06-02 13:44:24 +00:00
angt
6f36424d12 Code cleanup 2016-04-26 06:18:12 +00:00
angt
fa9301da16 Show tun_name in all states and fix state_send() 2016-04-26 06:13:14 +00:00
6 changed files with 212 additions and 84 deletions

15
.build.sh Executable file
View File

@@ -0,0 +1,15 @@
#!/bin/sh
export CC="gcc -static"
git clone https://github.com/jedisct1/libsodium --depth=1 --branch stable
cd libsodium || exit 1
./autogen.sh && ./configure --enable-minimal --disable-shared --prefix=/usr && make install
cd ..
./autogen.sh && ./configure && make
[ -x glorytun ] || exit 1
mkdir -p deploy
strip -s glorytun
mv glorytun deploy/glorytun-$(cat VERSION)-$(uname -m).bin

View File

@@ -16,6 +16,8 @@ AC_PROG_CC_C99
AC_USE_SYSTEM_EXTENSIONS AC_USE_SYSTEM_EXTENSIONS
AC_SEARCH_LIBS([getaddrinfo], [resolv nsl]) AC_SEARCH_LIBS([getaddrinfo], [resolv nsl])
AC_SEARCH_LIBS([socket], [socket]) AC_SEARCH_LIBS([socket], [socket])
AC_CHECK_LIB([rt], [clock_gettime])
AC_CHECK_FUNCS([clock_gettime])
PKG_CHECK_MODULES([libsodium], [libsodium >= 1.0.4]) PKG_CHECK_MODULES([libsodium], [libsodium >= 1.0.4])
AC_CONFIG_FILES([Makefile]) AC_CONFIG_FILES([Makefile])
AC_OUTPUT AC_OUTPUT

View File

@@ -1,6 +1,6 @@
dnl pkg.m4 - Macros to locate and utilise pkg-config. -*- Autoconf -*- # pkg.m4 - Macros to locate and utilise pkg-config. -*- Autoconf -*-
dnl serial 11 (pkg-config-0.29) # serial 12 (pkg-config-0.29.2)
dnl
dnl Copyright © 2004 Scott James Remnant <scott@netsplit.com>. dnl Copyright © 2004 Scott James Remnant <scott@netsplit.com>.
dnl Copyright © 2012-2015 Dan Nicholson <dbn.lists@gmail.com> dnl Copyright © 2012-2015 Dan Nicholson <dbn.lists@gmail.com>
dnl dnl
@@ -41,7 +41,7 @@ dnl
dnl See the "Since" comment for each macro you use to see what version dnl See the "Since" comment for each macro you use to see what version
dnl of the macros you require. dnl of the macros you require.
m4_defun([PKG_PREREQ], m4_defun([PKG_PREREQ],
[m4_define([PKG_MACROS_VERSION], [0.29]) [m4_define([PKG_MACROS_VERSION], [0.29.2])
m4_if(m4_version_compare(PKG_MACROS_VERSION, [$1]), -1, m4_if(m4_version_compare(PKG_MACROS_VERSION, [$1]), -1,
[m4_fatal([pkg.m4 version $1 or higher is required but ]PKG_MACROS_VERSION[ found])]) [m4_fatal([pkg.m4 version $1 or higher is required but ]PKG_MACROS_VERSION[ found])])
])dnl PKG_PREREQ ])dnl PKG_PREREQ
@@ -142,7 +142,7 @@ AC_ARG_VAR([$1][_CFLAGS], [C compiler flags for $1, overriding pkg-config])dnl
AC_ARG_VAR([$1][_LIBS], [linker flags for $1, overriding pkg-config])dnl AC_ARG_VAR([$1][_LIBS], [linker flags for $1, overriding pkg-config])dnl
pkg_failed=no pkg_failed=no
AC_MSG_CHECKING([for $1]) AC_MSG_CHECKING([for $2])
_PKG_CONFIG([$1][_CFLAGS], [cflags], [$2]) _PKG_CONFIG([$1][_CFLAGS], [cflags], [$2])
_PKG_CONFIG([$1][_LIBS], [libs], [$2]) _PKG_CONFIG([$1][_LIBS], [libs], [$2])
@@ -152,11 +152,11 @@ and $1[]_LIBS to avoid the need to call pkg-config.
See the pkg-config man page for more details.]) See the pkg-config man page for more details.])
if test $pkg_failed = yes; then if test $pkg_failed = yes; then
AC_MSG_RESULT([no]) AC_MSG_RESULT([no])
_PKG_SHORT_ERRORS_SUPPORTED _PKG_SHORT_ERRORS_SUPPORTED
if test $_pkg_short_errors_supported = yes; then if test $_pkg_short_errors_supported = yes; then
$1[]_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "$2" 2>&1` $1[]_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "$2" 2>&1`
else else
$1[]_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "$2" 2>&1` $1[]_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "$2" 2>&1`
fi fi
# Put the nasty error message in config.log where it belongs # Put the nasty error message in config.log where it belongs
@@ -173,7 +173,7 @@ installed software in a non-standard prefix.
_PKG_TEXT])[]dnl _PKG_TEXT])[]dnl
]) ])
elif test $pkg_failed = untried; then elif test $pkg_failed = untried; then
AC_MSG_RESULT([no]) AC_MSG_RESULT([no])
m4_default([$4], [AC_MSG_FAILURE( m4_default([$4], [AC_MSG_FAILURE(
[The pkg-config script could not be found or is too old. Make sure it [The pkg-config script could not be found or is too old. Make sure it
is in your PATH or set the PKG_CONFIG environment variable to the full is in your PATH or set the PKG_CONFIG environment variable to the full

View File

@@ -14,6 +14,7 @@
#include <signal.h> #include <signal.h>
#include <poll.h> #include <poll.h>
#include <fcntl.h> #include <fcntl.h>
#include <time.h>
#include <sys/socket.h> #include <sys/socket.h>
#include <sys/time.h> #include <sys/time.h>
@@ -36,6 +37,10 @@
#include <sodium.h> #include <sodium.h>
#ifdef __APPLE__
#include <mach/mach_time.h>
#endif
#ifndef O_CLOEXEC #ifndef O_CLOEXEC
#define O_CLOEXEC 0 #define O_CLOEXEC 0
#endif #endif
@@ -48,11 +53,12 @@
#define GT_ABYTES (16) #define GT_ABYTES (16)
#define GT_KEYBYTES (32) #define GT_KEYBYTES (32)
#define MPTCP_ENABLED (26)
static struct { static struct {
volatile sig_atomic_t quit;
volatile sig_atomic_t info;
long timeout; long timeout;
int mptcp; int mptcp;
int state_fd;
} gt; } gt;
struct fdbuf { struct fdbuf {
@@ -70,9 +76,6 @@ struct crypto_ctx {
int chacha; int chacha;
}; };
volatile sig_atomic_t gt_close = 0;
volatile sig_atomic_t gt_info = 0;
_pure_ _pure_
static int64_t dt_ms (struct timeval *ta, struct timeval *tb) static int64_t dt_ms (struct timeval *ta, struct timeval *tb)
{ {
@@ -111,13 +114,16 @@ enum sk_opt {
sk_acceptfilter, sk_acceptfilter,
sk_quickack, sk_quickack,
sk_user_timeout, sk_user_timeout,
sk_mptcp, sk_mptcp_26,
sk_mptcp_42,
}; };
static void sk_set (int fd, enum sk_opt opt, const void *val, socklen_t len) static int sk_set (int fd, enum sk_opt opt, const void *val, socklen_t len)
{ {
if (!val || len<=0) if (!val || len<=0) {
return; errno = EINVAL;
return -1;
}
struct { struct {
const char *name; const char *name;
@@ -168,33 +174,44 @@ static void sk_set (int fd, enum sk_opt opt, const void *val, socklen_t len)
1, IPPROTO_TCP, TCP_USER_TIMEOUT, 1, IPPROTO_TCP, TCP_USER_TIMEOUT,
#endif #endif
}, },
[sk_mptcp] = { "MPTCP_ENABLED", [sk_mptcp_26] = { "MPTCP_ENABLED (26)", 1, IPPROTO_TCP, 26 },
#ifdef MPTCP_ENABLED [sk_mptcp_42] = { "MPTCP_ENABLED (42)", 1, IPPROTO_TCP, 42 },
1, IPPROTO_TCP, MPTCP_ENABLED,
#endif
},
}; };
if (!opts[opt].present) { if (!opts[opt].present) {
gt_na(opts[opt].name); gt_na(opts[opt].name);
return; errno = EINVAL;
return -1;
} }
if (setsockopt(fd, opts[opt].level, opts[opt].option, val, len)==-1) int ret = setsockopt(fd, opts[opt].level, opts[opt].option, val, len);
if (ret==-1) {
int err = errno;
gt_log("couldn't set socket option `%s'\n", opts[opt].name); gt_log("couldn't set socket option `%s'\n", opts[opt].name);
errno = err;
}
return ret;
} }
static void sk_set_int (int fd, enum sk_opt opt, int val) static int sk_set_int (int fd, enum sk_opt opt, int val)
{ {
return sk_set(fd, opt, &val, sizeof(val)); return sk_set(fd, opt, &val, sizeof(val));
} }
static void sk_set_mptcp (int fd)
{
if (sk_set_int(fd, sk_mptcp_42, 1)==-1)
sk_set_int(fd, sk_mptcp_26, 1);
}
static int sk_listen (int fd, struct addrinfo *ai) static int sk_listen (int fd, struct addrinfo *ai)
{ {
sk_set_int(fd, sk_reuseaddr, 1); sk_set_int(fd, sk_reuseaddr, 1);
if (gt.mptcp) if (gt.mptcp)
sk_set_int(fd, sk_mptcp, 1); sk_set_mptcp(fd);
if (bind(fd, ai->ai_addr, ai->ai_addrlen)==-1) { if (bind(fd, ai->ai_addr, ai->ai_addrlen)==-1) {
perror("bind"); perror("bind");
@@ -221,7 +238,7 @@ static int sk_connect (int fd, struct addrinfo *ai)
fd_set_nonblock(fd); fd_set_nonblock(fd);
if (gt.mptcp) if (gt.mptcp)
sk_set_int(fd, sk_mptcp, 1); sk_set_mptcp(fd);
int ret = connect(fd, ai->ai_addr, ai->ai_addrlen); int ret = connect(fd, ai->ai_addr, ai->ai_addrlen);
@@ -277,8 +294,11 @@ static int sk_accept (int fd)
int ret = accept(fd, (struct sockaddr *)&addr, &addr_size); int ret = accept(fd, (struct sockaddr *)&addr, &addr_size);
if (ret==-1 && errno!=EINTR) if (ret==-1) {
perror("accept"); if (errno!=EINTR)
perror("accept");
return -1;
}
fd_set_nonblock(ret); fd_set_nonblock(ret);
@@ -365,10 +385,10 @@ static void gt_sa_handler (int sig)
case SIGINT: case SIGINT:
case SIGQUIT: case SIGQUIT:
case SIGTERM: case SIGTERM:
gt_close = 1; gt.quit = 1;
break; break;
case SIGUSR1: case SIGUSR1:
gt_info = 1; gt.info = 1;
break; break;
} }
} }
@@ -986,6 +1006,81 @@ static int gt_track (uint8_t **db, struct ip_common *ic, uint8_t *data, int rev)
return 0; return 0;
} }
static unsigned long long gt_now (void)
{
#if defined __APPLE__
static mach_timebase_info_data_t mtid;
if (!mtid.denom) mach_timebase_info(&mtid);
return (mach_absolute_time()*mtid.numer/mtid.denom)/1000ULL;
#elif defined CLOCK_MONOTONIC
struct timespec tv;
clock_gettime(CLOCK_MONOTONIC, &tv);
return tv.tv_sec*1000000ULL+tv.tv_nsec/1000ULL;
#else
struct timeval tv;
gettimeofday(&tv, NULL);
return tv.tv_sec*1000000ULL+tv.tv_usec;
#endif
}
static void gt_bench (int chacha)
{
unsigned char npub[crypto_aead_aes256gcm_NPUBBYTES];
memset(npub, 0, sizeof(npub));
unsigned char key[crypto_aead_aes256gcm_KEYBYTES];
memset(key, 1, sizeof(key));
crypto_aead_aes256gcm_state ctx;
if (!chacha)
crypto_aead_aes256gcm_beforenm(&ctx, key);
gt_print("bench: %s\n", chacha?"chacha20poly1305":"aes256gcm");
_align_(16) unsigned char buf[32*1024+crypto_aead_aes256gcm_ABYTES];
size_t bs = 8;
while (!gt.quit && bs<=sizeof(buf)) {
size_t total_size = 0;
unsigned long long total_dt = 0.0;
double mbps = 0.0;
while (!gt.quit) {
unsigned long long now = gt_now();
size_t size = 0;
while (!gt.quit && size<16*1024*1024) {
if (chacha) {
crypto_aead_chacha20poly1305_encrypt(buf, NULL,
buf, bs, NULL, 0, NULL, npub, key);
} else {
crypto_aead_aes256gcm_encrypt_afternm(buf, NULL,
buf, bs, NULL, 0, NULL, npub,
(const crypto_aead_aes256gcm_state *)&ctx);
}
size += bs;
}
total_dt += gt_now()-now;
total_size += size;
double last_mbps = mbps;
mbps = total_size*8.0/total_dt;
double diff = mbps-last_mbps;
if (-0.1<diff && diff<0.1)
break;
}
gt_print("%6zu bytes %9.2f Mbps\n", bs, mbps);
bs *= 2;
}
}
static int gt_setup_secretkey (struct crypto_ctx *ctx, char *keyfile) static int gt_setup_secretkey (struct crypto_ctx *ctx, char *keyfile)
{ {
const size_t size = sizeof(ctx->skey); const size_t size = sizeof(ctx->skey);
@@ -995,7 +1090,7 @@ static int gt_setup_secretkey (struct crypto_ctx *ctx, char *keyfile)
randombytes_buf(ctx->skey, size); randombytes_buf(ctx->skey, size);
gt_tohex(buf, sizeof(buf), ctx->skey, size); gt_tohex(buf, sizeof(buf), ctx->skey, size);
state("SECRETKEY", buf); state_send(gt.state_fd, "SECRETKEY", buf);
return 0; return 0;
} }
@@ -1064,17 +1159,26 @@ static int gt_setup_crypto (struct crypto_ctx *ctx, int fd, int listener)
if (fd_read_all(fd, data_r, size)!=size) if (fd_read_all(fd, data_r, size)!=size)
return -1; return -1;
if (memcmp(&data_r[size-hash_size-sizeof(proto)], proto, 3)) if (memcmp(&data_r[size-hash_size-sizeof(proto)], proto, 3)) {
gt_log("bad packet [%02"PRIX8"%02"PRIX8"%02"PRIX8"] !\n",
&data_r[size-hash_size-sizeof(proto)+0],
&data_r[size-hash_size-sizeof(proto)+1],
&data_r[size-hash_size-sizeof(proto)+2]);
return -2; return -2;
}
if (data_r[size-hash_size-sizeof(proto)+3]) if (data_r[size-hash_size-sizeof(proto)+3] && !ctx->chacha) {
gt_log("peer wants chacha20\n");
ctx->chacha = 1; ctx->chacha = 1;
}
crypto_generichash(hash, hash_size, crypto_generichash(hash, hash_size,
data_r, size-hash_size, ctx->skey, sizeof(ctx->skey)); data_r, size-hash_size, ctx->skey, sizeof(ctx->skey));
if (sodium_memcmp(&data_r[size-hash_size], hash, hash_size)) if (sodium_memcmp(&data_r[size-hash_size], hash, hash_size)) {
gt_log("peer sends a bad hash!\n");
return -2; return -2;
}
if (listener && fd_write_all(fd, data_w, size)!=size) if (listener && fd_write_all(fd, data_w, size)!=size)
return -1; return -1;
@@ -1091,11 +1195,15 @@ static int gt_setup_crypto (struct crypto_ctx *ctx, int fd, int listener)
crypto_generichash(hash, hash_size, crypto_generichash(hash, hash_size,
data_w, size, ctx->skey, sizeof(ctx->skey)); data_w, size, ctx->skey, sizeof(ctx->skey));
if (sodium_memcmp(auth_r, hash, hash_size)) if (sodium_memcmp(auth_r, hash, hash_size)) {
gt_log("peer sends a bad hash (challenge-response)!\n");
return -2; return -2;
}
if (crypto_scalarmult(shared, secret, data_r)) if (crypto_scalarmult(shared, secret, data_r)) {
gt_log("I'm just gonna hurt you really, really, BAD\n");
return -2; return -2;
}
crypto_generichash_init(&state, ctx->skey, sizeof(ctx->skey), sizeof(key_r)); crypto_generichash_init(&state, ctx->skey, sizeof(ctx->skey), sizeof(key_r));
crypto_generichash_update(&state, shared, sizeof(shared)); crypto_generichash_update(&state, shared, sizeof(shared));
@@ -1182,6 +1290,7 @@ int main (int argc, char **argv)
{ "retry", &retry_opts, option_option }, { "retry", &retry_opts, option_option },
{ "statefile", &statefile, option_str }, { "statefile", &statefile, option_str },
{ "timeout", &gt.timeout, option_long }, { "timeout", &gt.timeout, option_long },
{ "bench", NULL, option_option },
{ "chacha20", NULL, option_option }, { "chacha20", NULL, option_option },
{ "mptcp", NULL, option_option }, { "mptcp", NULL, option_option },
{ "debug", NULL, option_option }, { "debug", NULL, option_option },
@@ -1207,6 +1316,21 @@ int main (int argc, char **argv)
gt.mptcp = option_is_set(opts, "mptcp"); gt.mptcp = option_is_set(opts, "mptcp");
if (sodium_init()==-1) {
gt_log("libsodium initialization has failed\n");
return 1;
}
if (!chacha && !crypto_aead_aes256gcm_is_available()) {
gt_na("AES-256-GCM");
chacha = 1;
}
if (option_is_set(opts, "bench")) {
gt_bench(chacha);
return 0;
}
if (buffer_size < GT_PKT_MAX) { if (buffer_size < GT_PKT_MAX) {
buffer_size = GT_PKT_MAX; buffer_size = GT_PKT_MAX;
gt_log("buffer size must be greater than or equal to %li\n", buffer_size); gt_log("buffer size must be greater than or equal to %li\n", buffer_size);
@@ -1227,22 +1351,14 @@ int main (int argc, char **argv)
return 1; return 1;
} }
if (sodium_init()==-1) {
gt_log("libsodium initialization has failed\n");
return 1;
}
if (!chacha && !crypto_aead_aes256gcm_is_available()) {
gt_na("AES-256-GCM");
chacha = 1;
}
struct addrinfo *ai = ai_create(host, port, listener); struct addrinfo *ai = ai_create(host, port, listener);
if (!ai) if (!ai)
return 1; return 1;
if (state_init(statefile)) gt.state_fd = state_create(statefile);
if (statefile && gt.state_fd==-1)
return 1; return 1;
struct fdbuf tun = { .fd = -1 }; struct fdbuf tun = { .fd = -1 };
@@ -1282,9 +1398,9 @@ int main (int argc, char **argv)
long retry = 0; long retry = 0;
uint8_t *db = NULL; uint8_t *db = NULL;
state("INITIALIZED", tun_name); state_send(gt.state_fd, "INITIALIZED", tun_name);
while (!gt_close) { while (!gt.quit) {
if (retry_count>=0 && retry>=retry_count+1) { if (retry_count>=0 && retry>=retry_count+1) {
gt_log("couldn't %s (%d attempt%s)\n", listener?"listen":"connect", gt_log("couldn't %s (%d attempt%s)\n", listener?"listen":"connect",
(int)retry, (retry>1)?"s":""); (int)retry, (retry>1)?"s":"");
@@ -1337,20 +1453,14 @@ int main (int argc, char **argv)
ctx.chacha = chacha; ctx.chacha = chacha;
switch (gt_setup_crypto(&ctx, sock.fd, listener)) { if (gt_setup_crypto(&ctx, sock.fd, listener)) {
case -2:
gt_log("%s: key exchange could not be verified!\n", sockname);
goto restart;
case -1:
gt_log("%s: key exchange failed\n", sockname); gt_log("%s: key exchange failed\n", sockname);
goto restart; goto restart;
default:
break;
} }
retry = 0; retry = 0;
state("STARTED", sockname); state_send(gt.state_fd, "STARTED", tun_name);
fd_set rfds; fd_set rfds;
FD_ZERO(&rfds); FD_ZERO(&rfds);
@@ -1361,7 +1471,7 @@ int main (int argc, char **argv)
buffer_format(&sock.read); buffer_format(&sock.read);
while (1) { while (1) {
if _0_(gt_close) if _0_(gt.quit)
stop_loop |= 1; stop_loop |= 1;
if _0_(stop_loop) { if _0_(stop_loop) {
@@ -1415,7 +1525,7 @@ int main (int argc, char **argv)
const ssize_t r = tun_read(tun.fd, tun.read.write, GT_MTU_MAX); const ssize_t r = tun_read(tun.fd, tun.read.write, GT_MTU_MAX);
if (r<=0) { if (r<=0) {
gt_close |= !r; gt.quit |= !r;
break; break;
} }
@@ -1511,7 +1621,7 @@ int main (int argc, char **argv)
if (r==ic.size) if (r==ic.size)
tun.write.read += r; tun.write.read += r;
} else { } else {
gt_close |= !r; gt.quit |= !r;
break; break;
} }
} }
@@ -1523,7 +1633,7 @@ int main (int argc, char **argv)
sock.fd = -1; sock.fd = -1;
} }
state("STOPPED", sockname); state_send(gt.state_fd, "STOPPED", tun_name);
if (sockname) { if (sockname) {
free(sockname); free(sockname);

View File

@@ -7,16 +7,14 @@
#include <fcntl.h> #include <fcntl.h>
#include <sys/stat.h> #include <sys/stat.h>
static int state_fd = -1; int state_create (const char *filename)
int state_init (const char *filename)
{ {
if (str_empty(filename)) if (str_empty(filename))
return 0; return -1;
state_fd = open(filename, O_WRONLY); int fd = open(filename, O_WRONLY);
if (state_fd==-1) { if (fd==-1) {
if (errno!=EINTR) if (errno!=EINTR)
perror("open"); perror("open");
return -1; return -1;
@@ -24,38 +22,41 @@ int state_init (const char *filename)
struct stat st = {0}; struct stat st = {0};
if (fstat(state_fd, &st)==-1) { if (fstat(fd, &st)==-1) {
perror("fstat"); perror("fstat");
close(state_fd); close(fd);
state_fd = -1;
return -1; return -1;
} }
if (!S_ISFIFO(st.st_mode)) { if (!S_ISFIFO(st.st_mode)) {
gt_log("`%s' is not a fifo\n", filename); gt_log("`%s' is not a fifo\n", filename);
close(state_fd); close(fd);
state_fd = -1;
return -1; return -1;
} }
return 0; return fd;
} }
void state (const char *state, const char *info) void state_send (int fd, const char *state, const char *info)
{ {
if (str_empty(state)) if (str_empty(state))
return; return;
if (fd==-1) {
gt_print("%s %s\n", state, info);
return;
}
const char *strs[] = { state, " ", info, "\n" }; const char *strs[] = { state, " ", info, "\n" };
char *str = str_cat(strs, COUNT(strs)); char *str = str_cat(strs, COUNT(strs));
if (!str) if (!str) {
perror("str_cat");
return; return;
if (state_fd==-1) {
gt_print("%s", str);
} else {
if (write(state_fd, str, str_len(str))==-1 && errno!=EINTR)
perror("write");
} }
if (write(fd, str, str_len(str))==-1 && errno!=EINTR)
perror("write");
free(str);
} }

View File

@@ -1,4 +1,4 @@
#pragma once #pragma once
int state_init (const char *); int state_create (const char *);
void state (const char *, const char *); void state_send (int, const char *, const char *);