/* * Copyright (c) 2018 Lars Thrane A/S * SPDX-License-Identifier: GPL-2.0 */ #include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "telnet.h" #define RX_BUF_SIZE 1024 static struct ttynvt_param { unsigned int major; unsigned int minor; char *dev_name; char *host; unsigned int port; unsigned int debug; } ttynvt_param; typedef struct ttynvt { int nonblock; pthread_mutex_t tty_lock; pthread_mutex_t poll_lock; pthread_t thread_id; pthread_cond_t cond; struct fuse_pollhandle *ph; struct pollfd fds[1]; struct termios tio; unsigned char mcr; char buf[RX_BUF_SIZE]; size_t blen; size_t rxcnt; int txbuf_size; int epipe; volatile sig_atomic_t error; tn_ctx_t *tn; } ttynvt_t; static void ttynvt_log(int prio, const char *fmt, ...) { va_list arg; if (prio >= LOG_DEBUG && ttynvt_param.debug <= 0) return; va_start(arg, fmt); vsyslog(prio, fmt, arg); va_end(arg); } static void ttynvt_release(fuse_req_t req, struct fuse_file_info *info) { ttynvt_t *tty = (ttynvt_t *) (uintptr_t) info->fh; ttynvt_log(LOG_INFO, "connection closed\n"); close(tty->fds[0].fd); (void)pthread_cancel(tty->thread_id); (void)pthread_join(tty->thread_id, NULL); (void)pthread_mutex_destroy(&tty->tty_lock); (void)pthread_mutex_destroy(&tty->poll_lock); (void)pthread_cond_destroy(&tty->cond); if (tty->ph) fuse_pollhandle_destroy(tty->ph); free(tty->tn); free(tty); fuse_reply_err(req, 0); } static void ttynvt_notify(ttynvt_t * tty) { pthread_mutex_lock(&tty->poll_lock); if (tty->ph) { fuse_lowlevel_notify_poll(tty->ph); fuse_pollhandle_destroy(tty->ph); tty->ph = NULL; } pthread_mutex_unlock(&tty->poll_lock); pthread_cond_signal(&tty->cond); } static void ttynvt_update_notify(ttynvt_t * tty, struct fuse_pollhandle *ph) { pthread_mutex_lock(&tty->poll_lock); struct fuse_pollhandle *tmp_ph = tty->ph; tty->ph = ph; pthread_mutex_unlock(&tty->poll_lock); if (tmp_ph) fuse_pollhandle_destroy(tmp_ph); } static unsigned int ttynvt_write_avail(ttynvt_t * tty) { size_t write_avail; int used; pthread_mutex_lock(&tty->tty_lock); if (ioctl(tty->fds[0].fd, SIOCOUTQ, &used) < 0) return 0; write_avail = tty->txbuf_size - used; pthread_mutex_unlock(&tty->tty_lock); return write_avail; } static int ttynvt_read_avail(ttynvt_t * tty) { int read_avail; pthread_mutex_lock(&tty->tty_lock); read_avail = tty->rxcnt; pthread_mutex_unlock(&tty->tty_lock); return read_avail; } static void *ttynvt_read_net(void *arg) { ttynvt_t *tty = (ttynvt_t *) arg; ssize_t res; int tn_res = -1; pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); while (1) { res = poll(tty->fds, 1, -1); if (res < 0) { if (errno == EINTR) continue; else break; } else if (tty->fds[0].revents & (POLLERR | POLLHUP | POLLNVAL)) { break; } else if (tty->fds[0].revents & POLLIN) { pthread_mutex_lock(&tty->tty_lock); int read_avail = RX_BUF_SIZE - tty->blen; if (read_avail == 0) { /*buffer overflow */ tty->blen = 0; read_avail = RX_BUF_SIZE; ttynvt_log(LOG_WARNING, "Rx buffer overflow\n"); } res = read(tty->fds[0].fd, tty->buf + tty->blen, read_avail); if (res <= 0) { pthread_mutex_unlock(&tty->tty_lock); break; } else { tty->blen += res; tn_res = telnet_rx(tty->tn, tty->buf, &tty->blen); if (tn_res == 0) { tty->rxcnt = tty->blen; ttynvt_notify(tty); } pthread_mutex_unlock(&tty->tty_lock); } } } if (res == 0) ttynvt_log(LOG_INFO, "Connection closed by remote host\n"); else ttynvt_log(LOG_ERR, "Rx read error:%s\n", strerror(errno)); tty->error = 1; ttynvt_notify(tty); return 0; } static int ttynvt_connect(const char *host, unsigned int port) { struct sockaddr_in serveraddr; struct hostent *server; int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) { ttynvt_log(LOG_ERR, "Socket open failed: %s\n", strerror(errno)); return sockfd; } server = gethostbyname(host); if (server == NULL) { ttynvt_log(LOG_ERR, "Cannot resolve host name: %s\n", strerror(errno)); return -1; } bzero((char *)&serveraddr, sizeof(serveraddr)); serveraddr.sin_family = AF_INET; bcopy((char *)server->h_addr, (char *)&serveraddr.sin_addr.s_addr, server->h_length); serveraddr.sin_port = htons(port); if (connect (sockfd, (const struct sockaddr *)&serveraddr, sizeof(serveraddr)) < 0) { ttynvt_log(LOG_ERR, "Failed to connect to: %s:%u:%s\n", host, port, strerror(errno)); return -1; } return sockfd; } int srv_write(void *cctx, const void *buf, int len) { ttynvt_t *tty = cctx; return write(tty->fds[0].fd, buf, len); } int srv_read(void *cctx, int timeout) { ttynvt_t *tty = cctx; int res; while (1) { res = poll(tty->fds, 1, timeout); if (res <= 0) return res; if (tty->fds[0].revents & POLLIN) { res = read(tty->fds[0].fd, tty->buf + tty->blen, RX_BUF_SIZE - tty->blen); if (res <= 0) return res; tty->blen += (size_t) res; res = telnet_rx(tty->tn, tty->buf, &tty->blen); if (res == 0) { tty->rxcnt = tty->blen; break; } } } return 1; } void modem_status_cb(void *cctx, int status) { ttynvt_t *tty = cctx; tty->mcr &= ~(TIOCM_CD | TIOCM_RI | TIOCM_CTS | TIOCM_DSR); ttynvt_log(LOG_INFO, "Modem status: 0x%02x\n", status); if (status & TNS_STATE_CD) tty->mcr |= TIOCM_CD; if (status & TNS_STATE_RI) tty->mcr |= TIOCM_RI; if (status & TNS_STATE_CTS) tty->mcr |= TIOCM_CTS; if (status & TNS_STATE_DSR) tty->mcr |= TIOCM_DSR; } static void ttynvt_open(fuse_req_t req, struct fuse_file_info *info) { ttynvt_t *tty; socklen_t len; int sockfd; int res; tty = calloc(1, sizeof(*tty)); if (!tty) { fuse_reply_err(req, ENOMEM); return; } tty->tn = telnet_ctx_init(tty, srv_write, srv_read, modem_status_cb); if (!tty->tn) { free(tty); fuse_reply_err(req, ENOMEM); return; } sockfd = ttynvt_connect(ttynvt_param.host, ttynvt_param.port); if (sockfd < 0) { free(tty->tn); free(tty); fuse_reply_err(req, EFAULT); return; } tty->fds[0].fd = sockfd; tty->fds[0].events = POLLIN; tty->blen = 0; tty->rxcnt = 0; res = telnet_open(tty->tn); if (res < 0) { free(tty->tn); free(tty); fuse_reply_err(req, EBADE); return; } len = sizeof(tty->txbuf_size); if (getsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &tty->txbuf_size, &len) < 0) { free(tty->tn); free(tty); fuse_reply_err(req, EIO); return; } if (pthread_mutex_init(&tty->tty_lock, NULL) < 0 || pthread_mutex_init(&tty->poll_lock, NULL) < 0 || pthread_cond_init(&tty->cond, NULL) < 0 || pthread_create(&tty->thread_id, NULL, &ttynvt_read_net, tty)) { free(tty->tn); free(tty); fuse_reply_err(req, ENOMEM); return; } info->fh = (uintptr_t) tty; info->nonseekable = 1; info->direct_io = 1; ttynvt_log(LOG_INFO, "Open connection to server: %s:%d\n", ttynvt_param.host, ttynvt_param.port); fuse_reply_open(req, info); } static void ttynvt_read(fuse_req_t req, size_t size, off_t off, struct fuse_file_info *info) { ttynvt_t *tty = (ttynvt_t *) (uintptr_t) info->fh; if (tty->error) { if (tty->epipe == 0) { ttynvt_log(LOG_WARNING, "Rx EPIPE\n"); tty->epipe = 1; fuse_reply_buf(req, tty->buf, 0); } else fuse_reply_err(req, EBADFD); return; } pthread_mutex_lock(&tty->tty_lock); if (tty->rxcnt == 0 && !(info->flags & O_NONBLOCK)) { pthread_cond_wait(&tty->cond, &tty->tty_lock); } if (size > tty->rxcnt) size = tty->rxcnt; if (size > 0) { fuse_reply_buf(req, tty->buf, size); memmove(tty->buf, tty->buf + size, tty->blen - size); tty->blen -= size; tty->rxcnt -= size; } else { fuse_reply_err(req, EINTR); } pthread_mutex_unlock(&tty->tty_lock); } static void ttynvt_write(fuse_req_t req, const char *data, size_t size, off_t off, struct fuse_file_info *info) { int res; ttynvt_t *tty = (ttynvt_t *) (uintptr_t) info->fh; if (size == 0) { fuse_reply_write(req, 0); return; } if (tty->error) { fuse_reply_err(req, EPIPE); return; } if (tty->nonblock || (info->flags & O_NONBLOCK)) { size_t write_avail = ttynvt_write_avail(tty); if (size > write_avail) size = write_avail; } res = telnet_tx(tty->tn, data, size); if (res < 0) { tty->error = 1; fuse_reply_err(req, EPIPE); return; } #if 0 else if (res == 0) { fuse_reply_err(req, EAGAIN); return; } #endif fuse_reply_write(req, size); } static int baudrate(speed_t speed) { return speed == B50 ? 50 : speed == B75 ? 75 : speed == B110 ? 110 : speed == B300 ? 300 : speed == B600 ? 600 : speed == B1200 ? 1200 : speed == B1800 ? 1800 : speed == B2400 ? 2400 : speed == B4800 ? 4800 : speed == B9600 ? 9600 : speed == B19200 ? 19200 : speed == B38400 ? 38400 : speed == B57600 ? 57600 : speed == B115200 ? 115200 : speed == B230400 ? 230400 : 0; } static const char pstr[] = { ' ', 'N', 'O', 'E', 'M', 'S' }; static void ttynvt_ioctl(fuse_req_t req, int cmd, void *arg, struct fuse_file_info *info, unsigned int flags, const void *in_buf, size_t in_bufsz, size_t out_bufsz) { ttynvt_t *tty = (ttynvt_t *) (uintptr_t) info->fh; unsigned int tiocm = 0; static unsigned int mcr = 0; static struct termios tio = { .c_cflag = B115200 | CS8 | PARODD | CREAD | CLOCAL, }; int val; switch (cmd) { case TCSETSF: case TCSETSW: case TCSETS: if (!in_bufsz) { struct iovec iov = { arg, sizeof(struct termios) }; fuse_reply_ioctl_retry(req, &iov, 1, NULL, 0); } else { speed_t speed; int csize, par, sb; memcpy(&tio, in_buf, in_bufsz); speed = cfgetospeed(&tio); val = htonl(baudrate(speed)); telnet_rfc2217_cfg(tty->tn, TNS_SET_BAUDRATE, &val, 4); if ((tio.c_cflag & CSIZE) == CS5) csize = 5; else if ((tio.c_cflag & CSIZE) == CS6) csize = 6; else if ((tio.c_cflag & CSIZE) == CS7) csize = 7; else csize = 8; telnet_rfc2217_cfg(tty->tn, TNS_SET_DATASIZE, &csize, 1); if (tio.c_cflag & PARENB && tio.c_iflag & IGNPAR) par = 1; #ifdef CMSPAR else if (tio.c_cflag & PARENB && tio.c_iflag & CMSPAR) { if (tio.c_cflag & PARODD) par = 4; else par = 5; } #endif else if (tio.c_cflag & PARENB) par = 3; else par = 1; telnet_rfc2217_cfg(tty->tn, TNS_SET_PARITY, &par, 1); if (tio.c_cflag & CSTOPB) sb = 2; else sb = 1; telnet_rfc2217_cfg(tty->tn, TNS_SET_STOPSIZE, &sb, 1); if (tio.c_cflag & CRTSCTS) telnet_rfc2217_ctl(tty->tn, TNS_CTL_CTSRTS); if (!(mcr & TIOCM_DTR) && speed != B0) { mcr |= TIOCM_DTR; telnet_rfc2217_ctl(tty->tn, TNS_CTL_DTR_ON); } ttynvt_log (LOG_DEBUG, "CFG ioctl: %u%c%u%u %cCTSRTS %cCREAD %cHUPCL %cCLOCAL\n", baudrate(speed), pstr[par], csize, sb, tio.c_cflag & CRTSCTS ? '+' : '-', tio.c_cflag & CREAD ? '+' : '-', tio.c_cflag & HUPCL ? '+' : '-', tio.c_cflag & CLOCAL ? '+' : '-'); fuse_reply_ioctl(req, 0, 0, 0); } break; case TCGETS: if (out_bufsz) { fuse_reply_ioctl(req, 0, &tio, sizeof(struct termio)); } else { struct iovec iov = { arg, sizeof(struct termio) }; fuse_reply_ioctl_retry(req, NULL, 0, &iov, 1); } break; case TIOCINQ: /* Get the number of bytes in the input * buffer */ if (out_bufsz) { size_t rx_size = ttynvt_read_avail(tty); fuse_reply_ioctl(req, 0, &rx_size, sizeof(size_t)); } else { struct iovec iov = { arg, sizeof(size_t) }; fuse_reply_ioctl_retry(req, NULL, 0, &iov, 1); } break; case TIOCOUTQ: /* Get the number of bytes in the output * buffer */ if (out_bufsz) { size_t tx_size = ttynvt_write_avail(tty); fuse_reply_ioctl(req, 0, &tx_size, sizeof(size_t)); } else { struct iovec iov = { arg, sizeof(size_t) }; fuse_reply_ioctl_retry(req, NULL, 0, &iov, 1); } break; case TCFLSH: if (arg == TCIFLUSH) { /*todo */ } else if ((size_t) arg == TCOFLUSH) { /*todo */ } else if ((size_t) arg == TCIOFLUSH) { /*todo */ } fuse_reply_ioctl(req, 0, 0, 0); break; case TCIFLUSH: val = 1; telnet_rfc2217_cfg(tty->tn, TNS_SET_PURGE, &val, 1); pthread_mutex_lock(&tty->tty_lock); tty->blen = 0; pthread_mutex_unlock(&tty->tty_lock); fuse_reply_ioctl(req, 0, 0, 0); break; case TCOFLUSH: val = 2; telnet_rfc2217_cfg(tty->tn, TNS_SET_PURGE, &val, 1); fuse_reply_ioctl(req, 0, 0, 0); break; case TCIOFLUSH: val = 3; telnet_rfc2217_cfg(tty->tn, TNS_SET_PURGE, &val, 1); fuse_reply_ioctl(req, 0, 0, 0); break; case TIOCMGET: if (out_bufsz) { fuse_reply_ioctl(req, 0, &mcr, sizeof(unsigned int)); } else { struct iovec iov = { arg, sizeof(unsigned int) }; fuse_reply_ioctl_retry(req, NULL, 0, &iov, 1); } break; case TIOCMBIS: if (!in_bufsz) { struct iovec iov = { arg, sizeof(unsigned int) }; fuse_reply_ioctl_retry(req, &iov, 1, NULL, 0); } else { tiocm = *(unsigned int *)in_buf; fuse_reply_ioctl(req, 0, 0, 0); } if (tiocm & TIOCM_RTS) { mcr |= TIOCM_RTS; telnet_rfc2217_ctl(tty->tn, TNS_CTL_RTS_ON); } if (tiocm & TIOCM_DTR) { mcr |= TIOCM_DTR; telnet_rfc2217_ctl(tty->tn, TNS_CTL_DTR_ON); } break; case TIOCMBIC: if (!in_bufsz) { struct iovec iov = { arg, sizeof(unsigned int) }; fuse_reply_ioctl_retry(req, &iov, 1, NULL, 0); } else { tiocm = *(unsigned int *)in_buf; fuse_reply_ioctl(req, 0, 0, 0); } if (tiocm & TIOCM_RTS) { mcr &= ~TIOCM_RTS; telnet_rfc2217_ctl(tty->tn, TNS_CTL_RTS_OFF); } if (tiocm & TIOCM_DTR) { mcr &= ~TIOCM_DTR; telnet_rfc2217_ctl(tty->tn, TNS_CTL_DTR_OFF); } break; case TIOCMSET: if (!in_bufsz) { struct iovec iov = { arg, sizeof(unsigned int) }; fuse_reply_ioctl_retry(req, &iov, 1, NULL, 0); } else { tiocm = *(unsigned int *)in_buf; fuse_reply_ioctl(req, 0, 0, 0); ttynvt_log(LOG_DEBUG, "CFG ioctl: DTR=%c RTS=%c\n", tiocm & TIOCM_DTR ? '1' : '0', tiocm & TIOCM_RTS ? '1' : '0'); mcr &= ~(TIOCM_RTS | TIOCM_DTR); mcr |= tiocm & (TIOCM_RTS | TIOCM_DTR); telnet_rfc2217_ctl(tty->tn, (tiocm & TIOCM_DTR) ? TNS_CTL_DTR_ON : TNS_CTL_DTR_OFF); telnet_rfc2217_ctl(tty->tn, (tiocm & TIOCM_RTS) ? TNS_CTL_RTS_ON : TNS_CTL_RTS_OFF); } break; case TCSBRK: fuse_reply_ioctl(req, 0, 0, 0); break; case TIOCGWINSZ: if (out_bufsz) { struct winsize ws = {.ws_row = 25,.ws_col = 80 }; fuse_reply_ioctl(req, 0, &ws, sizeof(struct winsize)); } else { struct iovec iov = { arg, sizeof(struct winsize) }; fuse_reply_ioctl_retry(req, NULL, 0, &iov, 1); } break; default: fuse_reply_err(req, ENOSYS); return; } } static void ttynvt_poll(fuse_req_t req, struct fuse_file_info *info, struct fuse_pollhandle *ph) { ttynvt_t *tty = (ttynvt_t *) (uintptr_t) info->fh; int revents = 0; ttynvt_update_notify(tty, ph); if (ttynvt_write_avail(tty) > 0) revents |= POLLOUT; if (ttynvt_read_avail(tty) > 0) revents |= POLLIN; if (tty->error) { if (tty->epipe == 0) revents = POLLHUP | POLLIN; else revents = 0; } fuse_reply_poll(req, revents); } static const struct cuse_lowlevel_ops ttynvt_op = { .open = ttynvt_open, .read = ttynvt_read, .write = ttynvt_write, .ioctl = ttynvt_ioctl, .poll = ttynvt_poll, .release = ttynvt_release, }; #define TTYNET_OPT(t, p, v) { t, offsetof(struct ttynvt_param, p), v } static const struct fuse_opt ttynvt_opts[] = { TTYNET_OPT("-D", debug, 0), TTYNET_OPT("-M %u", major, 1), TTYNET_OPT("--maj=%u", major, 1), TTYNET_OPT("-m %u", minor, 1), TTYNET_OPT("--min=%u", minor, 1), TTYNET_OPT("-n %s", dev_name, 0), TTYNET_OPT("--name=%s", dev_name, 0), TTYNET_OPT("-s %s", host, 0), TTYNET_OPT("--server=%s", host, 0), TTYNET_OPT("-p %u", port, 0), TTYNET_OPT("--port=%u", port, 0), FUSE_OPT_KEY("-h", 0), FUSE_OPT_KEY("--help", 0), FUSE_OPT_END }; static void print_help(void) { fprintf(stderr, "ttynvt Usage:\n" "\t-D\tEnable debug\n" "\t-M major, --maj=major\n" "\t-m minor, --min=minor\n" "\t-n name, --name=name (default: ttyNVT0)\n" "\t\tDevice will be created in /dev/$name.\n" "\t-s server, --server=host name or IP address\n" "\t-p port, --port=host port number\n" "\n"); } static int process_arg(void *data, const char *arg, int key, struct fuse_args *outargs) { switch (key) { case 0: print_help(); return fuse_opt_add_arg(outargs, "-ho"); default: return 1; } } int main(int argc, char *argv[]) { struct fuse_args args = FUSE_ARGS_INIT(argc, argv); char dev_name[128] = { 0 }; const char *dev_info_argv[] = { dev_name }; openlog("ttynvt", LOG_PID, LOG_DAEMON); if (fuse_opt_parse(&args, &ttynvt_param, ttynvt_opts, process_arg)) { fprintf(stderr, "Failed to parse arguments\n"); return 1; } snprintf(dev_name, sizeof(dev_name), "DEVNAME=%s", ttynvt_param.dev_name ? ttynvt_param.dev_name : "ttyNVT0"); struct cuse_info ci = { .dev_major = ttynvt_param.major, .dev_minor = ttynvt_param.minor, .dev_info_argc = 1, .dev_info_argv = dev_info_argv, .flags = CUSE_UNRESTRICTED_IOCTL, }; return cuse_lowlevel_main(args.argc, args.argv, &ci, &ttynvt_op, NULL); }