diff --git a/logsrvd/Makefile.in b/logsrvd/Makefile.in index 5fd74ef3d..28aa4a5ee 100644 --- a/logsrvd/Makefile.in +++ b/logsrvd/Makefile.in @@ -108,7 +108,8 @@ SHELL = @SHELL@ PROGS = logsrvd sendlog -LOGSRVD_OBJS = iolog_writer.o logsrvd.o log_server.pb-c.o protobuf-c.o +LOGSRVD_OBJS = iolog_util.o iolog_writer.o logsrvd.o log_server.pb-c.o \ + protobuf-c.o SENDLOG_OBJS = sendlog.o iolog_util.o log_server.pb-c.o protobuf-c.o @@ -232,14 +233,16 @@ iolog_writer.o: $(srcdir)/iolog_writer.c $(devdir)/log_server.pb-c.h \ $(incdir)/sudo_debug.h $(incdir)/sudo_fatal.h \ $(incdir)/sudo_queue.h $(incdir)/sudo_util.h \ $(srcdir)/logsrvd.h $(srcdir)/protobuf-c/protobuf-c.h \ - $(sudoers_srcdir)/iolog.h $(top_builddir)/config.h + $(sudoers_srcdir)/iolog.h $(sudoers_srcdir)/iolog_util.h \ + $(top_builddir)/config.h $(CC) -c $(CPPFLAGS) $(CFLAGS) $(ASAN_CFLAGS) $(PIE_CFLAGS) $(SSP_CFLAGS) $(srcdir)/iolog_writer.c iolog_writer.i: $(srcdir)/iolog_writer.c $(devdir)/log_server.pb-c.h \ $(incdir)/compat/stdbool.h $(incdir)/sudo_compat.h \ $(incdir)/sudo_debug.h $(incdir)/sudo_fatal.h \ $(incdir)/sudo_queue.h $(incdir)/sudo_util.h \ $(srcdir)/logsrvd.h $(srcdir)/protobuf-c/protobuf-c.h \ - $(sudoers_srcdir)/iolog.h $(top_builddir)/config.h + $(sudoers_srcdir)/iolog.h $(sudoers_srcdir)/iolog_util.h \ + $(top_builddir)/config.h $(CC) -E -o $@ $(CPPFLAGS) $< iolog_writer.plog: iolog_writer.i rm -f $@; pvs-studio --cfg $(PVS_CFG) --sourcetree-root $(top_srcdir) --skip-cl-exe yes --source-file $(srcdir)/iolog_writer.c --i-file $< --output-file $@ diff --git a/logsrvd/iolog_writer.c b/logsrvd/iolog_writer.c index b70ee71d9..ba7fa1d53 100644 --- a/logsrvd/iolog_writer.c +++ b/logsrvd/iolog_writer.c @@ -42,7 +42,7 @@ #include "sudo_debug.h" #include "sudo_util.h" #include "sudo_fatal.h" -#include "iolog.h" +#include "iolog_util.h" #include "logsrvd.h" /* I/O log file names relative to iolog_dir. */ @@ -294,7 +294,7 @@ create_iolog_dir(struct iolog_details *details, struct connection_closure *closu closure->iolog_dir_fd = open(closure->iolog_dir, O_RDONLY); if (closure->iolog_dir_fd == -1) { sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, - "%s", path); + "%s", closure->iolog_dir); goto bad; } @@ -414,6 +414,151 @@ iolog_init(ExecMessage *msg, struct connection_closure *closure) debug_return_bool(true); } +/* + * Read the next record from the timing file. + * Return 0 on success, 1 on EOF and -1 on error. + */ +static int +read_timing_record(FILE *fp, struct timing_closure *timing) +{ + char line[LINE_MAX]; + debug_decl(read_timing_record, SUDO_DEBUG_UTIL) + + /* Read next record from timing file. */ + if (fgets(line, sizeof(line), fp) == NULL) { + /* EOF or error reading timing file, we are done. */ + if (feof(fp)) + debug_return_int(1); /* EOF */ + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "error reading timing file"); + debug_return_int(-1); + } + + /* Parse timing file record. */ + line[strcspn(line, "\n")] = '\0'; + if (!parse_timing(line, timing)) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "invalid timing file line: %s", line); + debug_return_int(-1); + } + + debug_return_int(0); +} + +bool +iolog_restart(RestartMessage *msg, struct connection_closure *closure) +{ + struct timespec target; + struct timing_closure timing; + FILE *fp = NULL; + int i, fd; + off_t length; + debug_decl(iolog_init, SUDO_DEBUG_UTIL) + + target.tv_sec = msg->resume_point->tv_sec; + target.tv_nsec = msg->resume_point->tv_nsec; + + /* We use iolog_dir_fd in calls to openat(2) */ + if ((closure->iolog_dir = strdup(msg->log_id)) == NULL) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "strdup"); + goto bad; + } + closure->iolog_dir_fd = open(closure->iolog_dir, O_RDONLY); + if (closure->iolog_dir_fd == -1) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "%s", closure->iolog_dir); + goto bad; + } + + /* Open existing I/O log files. */ + for (i = 0; i < IOFD_MAX; i++) { + closure->io_fds[i] = openat(closure->iolog_dir_fd, iolog_names[i], + O_RDWR, 0600); + } + + /* Dup IOFD_TIMING to a stream for easier processing. */ + if (closure->io_fds[IOFD_TIMING] == -1) + goto bad; + if ((fd = dup(closure->io_fds[IOFD_TIMING])) == -1) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "dup"); + goto bad; + } + if ((fp = fdopen(fd, "r")) == NULL) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "fdopen"); + goto bad; + } + + /* Parse timing file until we reach the target point. */ + /* XXX - split up */ + for (;;) { + if (read_timing_record(fp, &timing) != 0) + goto bad; + sudo_timespecadd(&timing.delay, &closure->elapsed_time, + &closure->elapsed_time); + if (timing.event < IOFD_TIMING) { + if (closure->io_fds[timing.event] == -1) { + /* Missing log file. */ + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "iofd %d referenced but not open", timing.event); + goto bad; + } + length = lseek(closure->io_fds[timing.event], timing.u.nbytes, + SEEK_CUR); + if (length == -1) { + sudo_debug_printf( + SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "lseek(%d, %lld, SEEK_CUR", closure->io_fds[timing.event], + (long long)timing.u.nbytes); + goto bad; + } + if (ftruncate(closure->io_fds[timing.event], length) == -1) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "ftruncate(%d, %lld)", closure->io_fds[IOFD_TIMING], length); + goto bad; + } + } + if (sudo_timespeccmp(&closure->elapsed_time, &target, >=)) { + if (sudo_timespeccmp(&closure->elapsed_time, &target, ==)) + break; + + /* Mismatch between resume point and stored log. */ + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "resume point mismatch, target [%lld, %ld], have [%lld, %ld]", + (long long)target.tv_sec, target.tv_nsec, + (long long)closure->elapsed_time.tv_sec, + closure->elapsed_time.tv_nsec); + goto bad; + } + } + /* Update timing file position after determining resume point. */ +#ifdef HAVE_FSEEKO + length = ftello(fp); +#else + length = ftell(fp); +#endif + fclose(fp); + if (lseek(closure->io_fds[IOFD_TIMING], length, SEEK_SET) == -1) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "lseek(%d, %lld, SEEK_SET", closure->io_fds[IOFD_TIMING], length); + goto bad; + } + if (ftruncate(closure->io_fds[IOFD_TIMING], length) == -1) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO, + "ftruncate(%d, %lld)", closure->io_fds[IOFD_TIMING], length); + goto bad; + } + + /* Ready to log I/O buffers. */ + debug_return_bool(true); +bad: + if (fp != NULL) + fclose(fp); + debug_return_bool(false); +} + static bool iolog_write(int iofd, void *buf, size_t len, struct connection_closure *closure) { diff --git a/logsrvd/logsrvd.c b/logsrvd/logsrvd.c index 5c8d6e873..12e45a80f 100644 --- a/logsrvd/logsrvd.c +++ b/logsrvd/logsrvd.c @@ -156,6 +156,18 @@ fmt_log_id_message(const char *id, struct connection_buffer *buf) debug_return_bool(fmt_server_message(buf, &msg)); } +static bool +fmt_error_message(const char *errstr, struct connection_buffer *buf) +{ + ServerMessage msg = SERVER_MESSAGE__INIT; + debug_decl(fmt_error_message, SUDO_DEBUG_UTIL) + + msg.error = (char *)errstr; + msg.type_case = SERVER_MESSAGE__TYPE_ERROR; + + debug_return_bool(fmt_server_message(buf, &msg)); +} + /* * Parse an ExecMessage */ @@ -211,6 +223,7 @@ handle_exit(ExitMessage *msg, struct connection_closure *closure) "unexpected state %d", closure->state); debug_return_bool(false); } + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: received ExitMessage", __func__); /* Sudo I/O logs don't store this info. */ if (msg->signal != NULL && msg->signal[0] != '\0') { @@ -250,12 +263,26 @@ handle_restart(RestartMessage *msg, struct connection_closure *closure) "unexpected state %d", closure->state); debug_return_bool(false); } + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: received RestartMessage for %s", + __func__, msg->log_id); - /* TODO */ - closure->state = RESTARTING; - sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, - "server restart not implemented"); - debug_return_bool(false); + if (!iolog_restart(msg, closure)) { + sudo_debug_printf(SUDO_DEBUG_WARN, "%s: unable to restart I/O log", __func__); + /* XXX - structured error message so client can send from beginning */ + if (!fmt_error_message("unable to restart log", &closure->write_buf)) + debug_return_bool(false); + sudo_ev_del(NULL, closure->read_ev); + if (sudo_ev_add(NULL, closure->write_ev, NULL, false) == -1) { + sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, + "unable to add server write event"); + debug_return_bool(false); + } + closure->state = ERROR; + debug_return_bool(true); + } + + closure->state = RUNNING; + debug_return_bool(true); } static bool @@ -491,7 +518,8 @@ server_msg_cb(int fd, int what, void *v) buf->off = 0; buf->len = 0; sudo_ev_del(NULL, closure->write_ev); - if (closure->state == FLUSHED || closure->state == SHUTDOWN) + if (closure->state == FLUSHED || closure->state == SHUTDOWN || + closure->state == ERROR) goto finished; } debug_return; diff --git a/logsrvd/logsrvd.h b/logsrvd/logsrvd.h index 63a4bb051..88af63331 100644 --- a/logsrvd/logsrvd.h +++ b/logsrvd/logsrvd.h @@ -66,10 +66,10 @@ struct iolog_details { enum connection_status { INITIAL, RUNNING, - RESTARTING, EXITED, SHUTDOWN, - FLUSHED + FLUSHED, + ERROR }; union sockaddr_union { @@ -109,6 +109,7 @@ struct connection_closure { /* iolog.c */ bool iolog_init(ExecMessage *msg, struct connection_closure *closure); +bool iolog_restart(RestartMessage *msg, struct connection_closure *closure); int store_iobuf(int iofd, IoBuffer *msg, struct connection_closure *closure); int store_suspend(CommandSuspend *msg, struct connection_closure *closure); int store_winsize(ChangeWindowSize *msg, struct connection_closure *closure); diff --git a/logsrvd/sendlog.c b/logsrvd/sendlog.c index 574548c91..90136627e 100644 --- a/logsrvd/sendlog.c +++ b/logsrvd/sendlog.c @@ -59,7 +59,7 @@ static gzFile io_fds[IOFD_MAX]; -/* I/O log file names relative to iolog_dir. */ +/* I/O log file names relative to the iolog dir. */ /* XXX - duplicated with server */ static const char *iolog_names[] = { "stdin", /* IOFD_STDIN */ @@ -74,8 +74,8 @@ static const char *iolog_names[] = { static void usage(void) { - fprintf(stderr, "usage: %s [-h host] [-p port] /path/to/iolog\n", - getprogname()); + fprintf(stderr, "usage: %s [-h host] [-i iolog-id] [-p port] " + "[-r restart-point] /path/to/iolog\n", getprogname()); exit(1); } @@ -138,7 +138,7 @@ connect_server(const char *host, const char *port) } /* - * Free client closure allocated by client_closure_alloc() + * Free client closure contents. */ static void client_closure_free(struct client_closure *closure) @@ -151,7 +151,8 @@ client_closure_free(struct client_closure *closure) free(closure->read_buf.data); free(closure->write_buf.data); free(closure->buf); - free(closure); + // XXX - avoid use after free + //free(closure); } debug_return; @@ -431,6 +432,42 @@ done: debug_return_bool(ret); } +/* + * Build and format a RestartMessage wrapped in a ClientMessage. + * Stores the wire format message in the closure's write buffer. + * Returns true on success, false on failure. + */ +static bool +fmt_restart_message(struct client_closure *closure) +{ + ClientMessage client_msg = CLIENT_MESSAGE__INIT; + RestartMessage restart_msg = RESTART_MESSAGE__INIT; + TimeSpec tv = TIME_SPEC__INIT; + bool ret = false; + debug_decl(fmt_restart_message, SUDO_DEBUG_UTIL) + + sudo_debug_printf(SUDO_DEBUG_INFO, + "%s: sending RestartMessage, [%lld, %ld]", __func__, + (long long)closure->restart->tv_sec, closure->restart->tv_nsec); + + tv.tv_sec = closure->restart->tv_sec; + tv.tv_nsec = closure->restart->tv_nsec; + restart_msg.resume_point = &tv; + restart_msg.log_id = (char *)closure->iolog_id; + + /* Schedule ClientMessage */ + client_msg.restart_msg = &restart_msg; + client_msg.type_case = CLIENT_MESSAGE__TYPE_RESTART_MSG; + ret = fmt_client_message(&closure->write_buf, &client_msg); + if (ret) { + if (sudo_ev_add(NULL, closure->write_ev, NULL, false) == -1) + ret = false; + } + ret = true; + + debug_return_bool(ret); +} + /* * Build and format an ExitMessage wrapped in a ClientMessage. * Stores the wire format message in the closure's write buffer. @@ -603,6 +640,7 @@ fmt_next_iolog(struct client_closure *closure) } /* TODO: fill write buffer with multiple messages */ +again: switch (read_timing_record(timing)) { case 0: /* OK */ @@ -616,6 +654,16 @@ fmt_next_iolog(struct client_closure *closure) debug_return_bool(false); } + /* Track elapsed time for comparison with commit points. */ + sudo_timespecadd(&timing->delay, &closure->elapsed, &closure->elapsed); + + /* If we have a restart point, ignore records until we hit it. */ + if (closure->restart != NULL) { + if (sudo_timespeccmp(closure->restart, &closure->elapsed, >=)) + goto again; + closure->restart = NULL; /* caught up */ + } + switch (timing->event) { case IO_EVENT_STDIN: ret = fmt_io_buf(CLIENT_MESSAGE__TYPE_STDIN_BUF, closure, buf); @@ -643,9 +691,6 @@ fmt_next_iolog(struct client_closure *closure) break; } - /* Track elapsed time for comparison with commit points. */ - sudo_timespecadd(&timing->delay, &closure->elapsed, &closure->elapsed); - debug_return_bool(ret); } @@ -660,6 +705,7 @@ client_message_completion(struct client_closure *closure) switch (closure->state) { case SEND_EXEC: + case SEND_RESTART: closure->state = SEND_IO; /* FALLTHROUGH */ case SEND_IO: @@ -744,7 +790,7 @@ handle_log_id(char *id, struct client_closure *closure) debug_decl(handle_log_id, SUDO_DEBUG_UTIL) printf("remote log ID: %s\n", id); - if ((closure->iolog_dir = strdup(id)) == NULL) + if ((closure->iolog_id = strdup(id)) == NULL) sudo_fatal(NULL); debug_return_bool(true); } @@ -797,8 +843,13 @@ handle_server_message(uint8_t *buf, size_t len, switch (msg->type_case) { case SERVER_MESSAGE__TYPE_HELLO: if ((ret = handle_server_hello(msg->hello, closure))) { - closure->state = SEND_EXEC; - ret = fmt_exec_message(closure); + if (sudo_timespecisset(closure->restart)) { + closure->state = SEND_RESTART; + ret = fmt_restart_message(closure); + } else { + closure->state = SEND_EXEC; + ret = fmt_exec_message(closure); + } } break; case SERVER_MESSAGE__TYPE_COMMIT_POINT: @@ -930,20 +981,23 @@ bad: } /* - * Allocate a new connection closure. + * Initialize a new client closure */ -static struct client_closure * -client_closure_alloc(int sock, struct iolog_info *log_info) +static bool +client_closure_fill(struct client_closure *closure, int sock, + struct timespec *restart, const char *iolog_id, + struct iolog_info *log_info) { - struct client_closure *closure; - debug_decl(client_closure_alloc, SUDO_DEBUG_UTIL) + debug_decl(client_closure_fill, SUDO_DEBUG_UTIL) - if ((closure = calloc(1, sizeof(*closure))) == NULL) - debug_return_ptr(NULL); + memset(closure, 0, sizeof(*closure)); closure->state = RECV_HELLO; closure->log_info = log_info; + closure->restart = restart; + closure->iolog_id = iolog_id; + closure->bufsize = 10240; closure->buf = malloc(closure->bufsize); if (closure->buf == NULL) @@ -969,10 +1023,10 @@ client_closure_alloc(int sock, struct iolog_info *log_info) if (closure->write_ev == NULL) goto bad; - debug_return_ptr(closure); + debug_return_bool(true); bad: client_closure_free(closure); - debug_return_ptr(NULL); + debug_return_bool(false); } /* @@ -1003,16 +1057,55 @@ iolog_open_all(const char *iolog_path) debug_return_bool(true); } +/* + * Parse a timespec on the command line of the form + * seconds[,nanoseconds] + */ +static bool +parse_timespec(struct timespec *ts, const char *strval) +{ + long long tv_sec; + long tv_nsec; + char *ep; + debug_decl(parse_timespec, SUDO_DEBUG_UTIL) + + errno = 0; + tv_sec = strtoll(strval, &ep, 10); + if (strval == ep || (*ep != ',' && *ep != '\0')) + debug_return_bool(false); +#if TIME_T_MAX != LLONG_MAX + if (tv_sec > TIME_T_MAX) + debug_return_bool(false); +#endif + if (tv_sec < 0 || (errno == ERANGE && tv_sec == LLONG_MAX)) + debug_return_bool(false); + strval = ep + 1; + + errno = 0; + tv_nsec = strtol(strval, &ep, 10); + if (strval == ep || *ep != '\0') + debug_return_bool(false); + if (tv_nsec < 0 || (errno == ERANGE && tv_nsec == LONG_MAX)) + debug_return_bool(false); + + sudo_debug_printf(SUDO_DEBUG_INFO, "%s: parsed timespec [%lld, %ld]", + __func__, tv_sec, tv_nsec); + ts->tv_sec = (time_t)tv_sec; + ts->tv_nsec = tv_nsec; + debug_return_bool(true); +} + int main(int argc, char *argv[]) { - struct client_closure *closure; + struct client_closure closure; struct sudo_event_base *evbase; struct iolog_info *log_info; const char *host = "localhost"; const char *port = DEFAULT_PORT_STR; - char fname[PATH_MAX]; - char *iolog_path; + struct timespec restart = { 0, 0 }; + char fname[PATH_MAX], *iolog_path; + const char *iolog_id = NULL; int ch, sock; debug_decl_vars(main, SUDO_DEBUG_MAIN) @@ -1030,14 +1123,21 @@ main(int argc, char *argv[]) if (protobuf_c_version_number() < 1003000) sudo_fatalx("Protobuf-C version 1.3 or higher required"); - while ((ch = getopt(argc, argv, "h:p:")) != -1) { + while ((ch = getopt(argc, argv, "h:i:p:r:")) != -1) { switch (ch) { case 'h': host = optarg; break; + case 'i': + iolog_id = optarg; + break; case 'p': port = optarg; break; + case 'r': + if (!parse_timespec(&restart, optarg)) + goto bad; + break; default: usage(); } @@ -1045,6 +1145,11 @@ main(int argc, char *argv[]) argc -= optind; argv += optind; + if (sudo_timespecisset(&restart) != (iolog_id != NULL)) { + sudo_warnx("must specify both restart point and iolog ID"); + usage(); + } + /* Remaining arg should be path to I/O log file to send. */ if (argc != 1) usage(); @@ -1071,22 +1176,20 @@ main(int argc, char *argv[]) sudo_fatal(NULL); sudo_ev_base_setdef(evbase); - if ((closure = client_closure_alloc(sock, log_info)) == NULL) + if (!client_closure_fill(&closure, sock, &restart, iolog_id, log_info)) goto bad; /* Add read event for the server hello message and enter event loop. */ - if (sudo_ev_add(evbase, closure->read_ev, NULL, false) == -1) + if (sudo_ev_add(evbase, closure.read_ev, NULL, false) == -1) goto bad; sudo_ev_dispatch(evbase); - if (!sudo_timespeccmp(&closure->elapsed, &closure->committed, ==)) { - sudo_warnx("commit point mismatch, expected [%lld, %ld], got [%lld, %ld]", - (long long)closure->elapsed.tv_sec, closure->elapsed.tv_nsec, - (long long)closure->committed.tv_sec, closure->committed.tv_nsec); - } - - if (closure->state != FINISHED) { - sudo_warnx("exited prematurely with state %d", closure->state); + if (closure.state != FINISHED) { + sudo_warnx("exited prematurely with state %d", closure.state); + sudo_warnx("elapsed time sent to server [%lld, %ld]", + (long long)closure.elapsed.tv_sec, closure.elapsed.tv_nsec); + sudo_warnx("commit point received from server [%lld, %ld]", + (long long)closure.committed.tv_sec, closure.committed.tv_nsec); goto bad; } printf("I/O log transmitted successfully\n"); diff --git a/logsrvd/sendlog.h b/logsrvd/sendlog.h index a4f3b780f..0880accf9 100644 --- a/logsrvd/sendlog.h +++ b/logsrvd/sendlog.h @@ -33,25 +33,10 @@ #define IOFD_TIMING 5 #define IOFD_MAX 6 -#if 0 -struct timing_closure { - struct timespec delay; - int event; - union { - struct { - int lines; - int columns; - } winsize; - size_t nbytes; - } u; - char *buf; - size_t bufsize; -}; -#endif - enum client_state { ERROR, RECV_HELLO, + SEND_RESTART, SEND_EXEC, SEND_IO, SEND_EXIT, @@ -68,6 +53,7 @@ struct connection_buffer { }; struct client_closure { + struct timespec *restart; struct timespec elapsed; struct timespec committed; struct timing_closure timing; @@ -76,7 +62,7 @@ struct client_closure { struct sudo_event *read_ev; struct sudo_event *write_ev; struct iolog_info *log_info; - char *iolog_dir; + const char *iolog_id; char *buf; /* XXX */ size_t bufsize; /* XXX */ enum client_state state;