From d9750dcf73c4095a92fcaf3de085bf7998bd177e Mon Sep 17 00:00:00 2001 From: "Wladimir J. van der Laan" Date: Fri, 21 Feb 2020 15:40:43 +0000 Subject: [PATCH] Add k210 linux utilities --- linux/.gitignore | 3 + linux/Makefile | 15 ++ linux/README.md | 66 ++++++ linux/esptun.c | 588 +++++++++++++++++++++++++++++++++++++++++++++++ linux/term.c | 130 +++++++++++ 5 files changed, 802 insertions(+) create mode 100644 linux/.gitignore create mode 100644 linux/Makefile create mode 100644 linux/README.md create mode 100644 linux/esptun.c create mode 100644 linux/term.c diff --git a/linux/.gitignore b/linux/.gitignore new file mode 100644 index 0000000..e35f8ad --- /dev/null +++ b/linux/.gitignore @@ -0,0 +1,3 @@ +esptun +term +*.gdb diff --git a/linux/Makefile b/linux/Makefile new file mode 100644 index 0000000..2133dd7 --- /dev/null +++ b/linux/Makefile @@ -0,0 +1,15 @@ +CROSS ?= /opt/riscv64-uclibc/bin/riscv64-buildroot-linux-uclibc- +CFLAGS = -fPIC -Wl,-elf2flt=-r -Wall +BINARIES = term esptun + +all: $(BINARIES) + +clean: + rm -f $(BINARIES) + +term: term.c + ${CROSS}gcc $< -o $@ $(CFLAGS) + +esptun: esptun.c + ${CROSS}gcc $< -o $@ $(CFLAGS) + diff --git a/linux/README.md b/linux/README.md new file mode 100644 index 0000000..1fcb4c4 --- /dev/null +++ b/linux/README.md @@ -0,0 +1,66 @@ +esptun +====== + +A tool to tunnel IP packets over UDP over WIFI through an UART connected to a ESP8285 with the +standard AT firmware. + +Usage +----- + + esptun + +This will create tun interface `ifname`. Then, with the with the ESP WIFI +device on `uart` it connects to the AP `ssid` with password `passwd`. +It creates a UDP over IP tunnel with the other endpoint `host:port`. + +The new tun device will not be configured, use the `ip` utility to assign an +IP address and switch the interface on. + +For example: + + /root/esptun tun0 /dev/ttyS1 "accesspointname" "secretpassword" 192.168.122.21 23232 + /sbin/ip addr add 10.0.1.2/24 dev tun0 + /sbin/ip link set tun0 up + /sbin/ip route add default via 10.0.1.1 dev tun0 + +Host side +--------- + +On the other endpoint the tunnel is expected to be a host running `socat` or similar to unwrap +the tunnel. For example: + + sudo socat UDP:192.168.2.127:23232,bind=192.168.122.21:23232 \ + TUN:10.0.1.1/24,tun-name=tundudp,iff-no-pi,tun-type=tun,su=$USER,iff-up + +Optionally, enable forwarding and masquerading: + + sudo iptables -t nat -F + sudo iptables -t nat -A POSTROUTING -o ens3 -j MASQUERADE + echo 1 | sudo tee /proc/sys/net/ipv4/ip_forward + +Linux kernel +------------ + +Until there is a proper solution, you can use the following kernel branch +to get the UART working on Kendryte K210 under Linux: + +https://github.com/laanwj/linux/tree/kendryte-5.6-rc1-wifi + +This is a hack that configures the FPIOA and GPIOHS manually, and configures UART1 +from the device tree. + +To enable TUN/TAP, enable the following kernel settings: +``` +CONFIG_NET=y +CONFIG_INET=y +CONFIG_TUN=y +``` + +While building the root filesystem, make sure you enable at least `ip` and `ping` +(and possibly other network tools) for busybox. + +Author +------ + +Copyright (c) 2020 W.J. van der Laan +Distributed under the MIT software license, diff --git a/linux/esptun.c b/linux/esptun.c new file mode 100644 index 0000000..68e1191 --- /dev/null +++ b/linux/esptun.c @@ -0,0 +1,588 @@ +/** esptun: UDP tunnel over UART, using ESP8285 AT command set. + * Based on "simpletun" by Davide Brini. + * + * Copyright (c) 2020 W.J. van der Laan + * Distributed under the MIT software license, + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* buffer for reading from UART, must be >= 1500 */ +#define ESP_BUFSIZE 2000 +/* buffer for reading from tun/tap interface, must be >= 1500 */ +#define TAP_BUFSIZE 2000 +/* baud rate (max 115200*40 = 4608000) */ +#define BAUDRATE (115200 * 40) + +/** Macro for writing static strings without needing strlen. */ +#define S(s) (const uint8_t*)(s), (sizeof(s)-1) + +bool debug = false; +char *progname; +uint8_t esp_buffer[ESP_BUFSIZE]; +uint8_t tap_buffer[TAP_BUFSIZE]; +size_t tap2net; +size_t net2tap; + +/** + * Prints debugging. + */ +static void log_debug(const char *msg, ...) +{ + va_list argp; + + if (debug) { + va_start(argp, msg); + vfprintf(stderr, msg, argp); + va_end(argp); + } +} + +#define INFO1 0 +#define INFO2 1 +#define WARNING 2 +/** + * Prints warning/info. + */ +static void log_info(int cls, const char *msg, ...) +{ + va_list argp; + int attr; + switch (cls) { + case INFO1: attr = 95; break; + case INFO2: attr = 35; break; + case WARNING: attr = 91; break; + } + fprintf(stderr, "\x1b[%dm", attr); + va_start(argp, msg); + vfprintf(stderr, msg, argp); + va_end(argp); + fprintf(stderr, "\x1b[0m"); +} + +static const char hexchars[16] = "0123456789abcdef"; +/** + * Log a raw response for debugging. + */ +static void debug_response(const uint8_t *esp_buffer, size_t n) { + if (debug) { + for(size_t i = 0; i < n; ++i) { + if (esp_buffer[i] < 32 || esp_buffer[i] >= 127) { + fputc('\\', stderr); + fputc('x', stderr); + fputc(hexchars[esp_buffer[i] >> 4], stderr); + fputc(hexchars[esp_buffer[i] & 0xf], stderr); + } else { + fputc(esp_buffer[i], stderr); + } + } + } +} + +/** + * Prints error message on stderr and exits the program. + */ +static void my_err(char *msg, ...) +{ + va_list argp; + + fprintf(stderr, "error: "); + va_start(argp, msg); + vfprintf(stderr, msg, argp); + va_end(argp); + fprintf(stderr, "\n"); + exit(1); +} + +/** + * Allocates or reconnects to a tun/tap device. *dev specifies the name of the + * interface (e.g. tunX). This will be overwritten with the actual name. The + * caller must reserve enough space (IFNAMSIZ) in *dev. + */ +int tun_alloc(char *dev, int flags) +{ + struct ifreq ifr; + int fd, err; + const char *clonedev = "/dev/net/tun"; + + if ((fd = open(clonedev, O_RDWR)) < 0) { + perror("Opening /dev/net/tun"); + return fd; + } + + memset(&ifr, 0, sizeof(ifr)); + + ifr.ifr_flags = flags; + + if (*dev) { + strncpy(ifr.ifr_name, dev, IFNAMSIZ); + } + + if ((err = ioctl(fd, TUNSETIFF, (void *)&ifr)) < 0) { + perror("ioctl(TUNSETIFF)"); + close(fd); + return err; + } + + strcpy(dev, ifr.ifr_name); + + return fd; +} + +/** + * Read routine that checks for errors and exits if an error is + * returned (including unexpected EOF). + */ +static int cread(int fd, uint8_t *buf, int n) +{ + int nread; + + if ((nread = read(fd, buf, n)) <= 0) { + perror("Reading data"); + exit(1); + } + return nread; +} + +/** + * Write routine that checks for errors and exits if an error is + * returned (including unexpected EOF). + */ +static int cwrite(int fd, const uint8_t *buf, int n) +{ + int nwrite; + + if ((nwrite = write(fd, buf, n)) <= 0) { + perror("Writing data"); + exit(1); + } + return nwrite; +} + +/** + * Ensures we write exactly n bytes. Exit in case of error or EOF. + */ +static void write_all(int fd, const uint8_t *buf, int n) +{ + int nwrite, left = n; + + while (left > 0) { + nwrite = cwrite(fd, buf, left); + left -= nwrite; + buf += nwrite; + } +} + +/** + * Ensures we write exactly n bytes, escaping special characters. Exit in case + * of error or EOF. + */ +static void write_esc(int fd, const char *buf, int n) +{ + for (int i = 0; i < n; ++i) { + if (buf[i] == '\\' || buf[i] == ',' || buf[i] == '"') { + write_all(fd, S("\\")); + } + write_all(fd, (const uint8_t*)&buf[i], 1); + } +} + +/** + * Write a 32-bit unsigned integer. + */ +static void write_uint(int fd, uint32_t x) +{ + char buf[11]; + size_t ptr = sizeof(buf); + do { + ptr -= 1; + buf[ptr] = '0' + (x % 10); + x /= 10; + } while (x != 0); + write_all(fd, (const uint8_t*)&buf[ptr], sizeof(buf) - ptr); +} + +/** + * Prints usage and exits. + */ +static void usage(void) +{ + fprintf(stderr, "Usage:\n"); + fprintf(stderr, "%s \n", progname); + exit(1); +} + +/** + * Set UART attributes and speed. + */ +static int setup_uart(int fd, int speed) +{ + struct termios2 tty; + + if (ioctl(fd, TCGETS2, &tty) != 0) { + my_err("Error from TCGETS2: %s", strerror(errno)); + } + + tty.c_cflag |= (CLOCAL | CREAD); /* ignore modem controls */ + tty.c_cflag &= ~CSIZE; + tty.c_cflag |= CS8; /* 8-bit characters */ + tty.c_cflag &= ~PARENB; /* no parity bit */ + tty.c_cflag &= ~CSTOPB; /* only need 1 stop bit */ + tty.c_cflag &= ~CRTSCTS; /* no hardware flowcontrol */ + + /* custom baudrate setting */ + tty.c_cflag &= ~CBAUD; + tty.c_cflag |= BOTHER; + tty.c_ispeed = speed; + tty.c_ospeed = speed; + + /* setup for non-canonical mode */ + tty.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON); + tty.c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN); + tty.c_oflag &= ~OPOST; + + /* fetch bytes as they become available */ + tty.c_cc[VMIN] = 1; + tty.c_cc[VTIME] = 1; + + if (ioctl(fd, TCSETS2, &tty) != 0) { + my_err("Error from TCSETS2: %s", strerror(errno)); + } + return 0; +} + +/** Prefix for receive packet. */ +static const uint8_t pktprefix[] = {'+', 'I', 'P', 'D', ','}; + +struct packet_info { + /* header: part after +IPD, and before : */ + const uint8_t *hdr; + size_t hdr_len; + /* packet payload */ + const uint8_t *payload; + size_t payload_len; +}; + +/** Determine if a esp_buffer[0..end] contains a complete ESP8285 response. + * If so, return the size (including \r\n line terminators). + * If not, return 0. + */ +static size_t esp_response_is_complete(const uint8_t *b, size_t end, struct packet_info *pinfo) +{ + size_t i; + /* Count number of bytes matching prefix. */ + for (i = 0; i < sizeof(pktprefix) && i < end && b[i] == pktprefix[i]; ++i) + ; + if (i == sizeof(pktprefix)) { + /* Received packet. + * Syntax is +IPD,[,,]: + * Look for end of header (':') first. + */ + for (i = 5; i < end && b[i] != ':'; ++i) + ; + if (i != end) { + size_t hdr_end = i + 1; + /* Parse length. */ + size_t length = atoi((const char*)&b[5]); + if ((hdr_end + length) <= end) { + if (pinfo != NULL) { + pinfo->hdr = &b[5]; + pinfo->hdr_len = i - 5; + pinfo->payload = &b[hdr_end]; + pinfo->payload_len = length; + } + return hdr_end + length; + } + } + } else if (i == end) { + /* Possive start of received packet response. These are not "\r\n" + * terminated and may contain any character. + */ + return 0; + } else { + /** Other response - look for line terminator. */ + for (i = 0; i < end && b[i] != '\n'; ++i) + ; + if (i != end) { + return i + 1; + } + } + return 0; +} + +static bool is_prefix(const uint8_t *esp_buffer, size_t len, const uint8_t *prefix, size_t prefix_len) +{ + if (len < prefix_len) { + return false; + } + for (size_t i = 0; i < prefix_len; ++i) + { + if (esp_buffer[i] != prefix[i]) { + return false; + } + } + return true; +} + +/** Get OK/FAIL status after running a command on the ESP. */ +static bool esp_read_responses(int fd) +{ + /** TODO handle timeouts. */ + size_t end = 0; + while (true) { + ssize_t n = read(fd, &esp_buffer[end], ESP_BUFSIZE - end); + if (n <= 0) { + perror("Reading from UART"); + exit(1); + } + end += n; + size_t ptr = 0; + while (ptr < end) { + const uint8_t *resp = &esp_buffer[ptr]; + size_t len = esp_response_is_complete(resp, end - ptr, NULL); + if (len) { + log_debug("<-: "); + debug_response(resp, len); + log_debug("\n"); + /* Check for final response */ + if (is_prefix(resp, len, S("OK")) + || is_prefix(resp, len, S("SEND OK"))) { + return true; + } + if (is_prefix(resp, len, S("FAIL")) + || is_prefix(resp, len, S("ERROR")) + || is_prefix(resp, len, S("ALREADY CONNECTED"))) { + return false; + } + /* Log interesting info responses */ + if (is_prefix(resp, len, S("+CIPSTA_CUR"))) { + log_info(INFO2, " %.*s\n", len - 11 - 2, resp + 11); + } + if (is_prefix(resp, len, S("WIFI "))) { + log_info(INFO2, " %.*s\n", len - 2, resp); + } + + /* On non-final response, keep reading. */ + } else { + if (end == ESP_BUFSIZE) { + /* Buffer full but command wasn't complete - this isn't good, + * exit to prevent looping forever. */ + my_err("Buffer full with unterminated command"); + } + break; + } + ptr += len; + } + /* Remove processed responses from esp_buffer by shifting bytes. */ + memmove(esp_buffer, &esp_buffer[ptr], end - ptr); + end -= ptr; + } +} + +/** Send a packet to ESP interface. */ +static void esp_tx_packet(int fd, const uint8_t *esp_buffer, size_t size) { + write_all(fd, S("AT+CIPSEND=")); + write_uint(fd, size); + write_all(fd, S("\r\n")); + if (esp_read_responses(fd)) { + write_all(fd, esp_buffer, size); + esp_read_responses(fd); + } +} + +/** Send a packet to tun interface. */ +static void tun_tx_packet(int fd, const uint8_t *esp_buffer, size_t size) { + if (write(fd, esp_buffer, size) <= 0) { + log_info(WARNING, "warning: error writing to tun: %s\n", strerror(errno)); + } +} + +int main(int argc, char **argv) +{ + if (argc < 7) { + usage(); + } + char if_name[IFNAMSIZ] = ""; + const char *portname = argv[2]; + const char *ssid = argv[3]; + const char *passwd = argv[4]; + + strncpy(if_name, argv[1], IFNAMSIZ-1); + const char *host = argv[5]; + int port = atoi(argv[6]); + + int esp_fd = open(portname, O_RDWR | O_NOCTTY | O_SYNC); + if (esp_fd < 0) { + my_err("Error opening %s: %s", portname, strerror(errno)); + } + + /* initial: baudrate 115200, 8 bits, no parity, 1 stop bit */ + setup_uart(esp_fd, 115200); + + /* initialize tun/tap interface */ + int tun_fd; + if ((tun_fd = tun_alloc(if_name, IFF_TUN | IFF_NO_PI)) < 0) { + my_err("Error connecting to tun interface %s!\n", if_name); + } + + log_info(INFO1, "Successfully connected to interface %s\n", if_name); + + log_info(INFO1, "Initializing device\n"); + write_all(esp_fd, S("ATE0\r\n")); + if (!esp_read_responses(esp_fd)) { + my_err("Could not disable echo"); + } + + write_all(esp_fd, S("AT+CWMODE_CUR=1\r\n")); + if (!esp_read_responses(esp_fd)) { + my_err("Could not switch to station mode"); + } + + write_all(esp_fd, S("AT+CIPMUX=0\r\n")); + if (!esp_read_responses(esp_fd)) { + my_err("Could not switch to single-connection mode"); + } + + log_info(INFO1, "Changing baudrate to %d\n", BAUDRATE); + write_all(esp_fd, S("AT+UART_CUR=")); + write_uint(esp_fd, BAUDRATE); + write_all(esp_fd, S(",8,1,0,0\r\n")); + if (!esp_read_responses(esp_fd)) { + my_err("Could not set faster baudrate"); + } + setup_uart(esp_fd, BAUDRATE); + + write_all(esp_fd, S("AT\r\n")); + if (!esp_read_responses(esp_fd)) { + my_err("AT test unsuccesful: new baudrate unstable?"); + } + + log_info(INFO1, "Connecting to AP\n"); + write_all(esp_fd, S("AT+CWJAP_CUR=\"")); + write_esc(esp_fd, ssid, strlen(ssid)); + write_all(esp_fd, S("\",\"")); + write_esc(esp_fd, passwd, strlen(passwd)); + write_all(esp_fd, S("\"\r\n")); + if (!esp_read_responses(esp_fd)) { + my_err("Could not connect to AP"); + } + + write_all(esp_fd, S("AT+CIPSTA_CUR?\r\n")); + if (!esp_read_responses(esp_fd)) { + my_err("Could not query IP"); + } + + log_info(INFO1, "Opening UDP connection to %s:%d from port %d\n", host, port, port); + write_all(esp_fd, S("AT+CIPSTART=\"UDP\",\"")); + write_esc(esp_fd, host, strlen(host)); + write_all(esp_fd, S("\",")); + write_uint(esp_fd, port); + write_all(esp_fd, S(",")); + write_uint(esp_fd, port); /* local port is same as remote port for now */ + write_all(esp_fd, S("\r\n")); + if (!esp_read_responses(esp_fd)) { + my_err("Could not open UDP connection"); + } + + log_info(INFO1, "Starting packet loop, moving to background\n"); + + /* Process to background */ + if (daemon(0, 0) < 0) { + my_err("Could not go to background\n"); + } + debug = 0; /* no use to log anything after this */ + + struct pollfd fds[2] = { + {esp_fd, POLLIN, 0}, + {tun_fd, POLLIN, 0}, + }; + do { + for (int i=0; i<2; ++i) { + fds[i].revents = 0; + } + + if (poll(fds, 2, 0) < 0) { + perror("poll"); + exit(1); + } + + /* Handle input from UART */ + if (fds[0].revents & POLLIN) { + /* Read all available responses entirely */ + size_t end = 0; + while (true) { + ssize_t n = read(esp_fd, &esp_buffer[end], ESP_BUFSIZE - end); + if (n <= 0) { + perror("Reading from UART"); + exit(1); + } + end += n; + + size_t ptr = 0; + while (ptr < end) { + const uint8_t *resp = &esp_buffer[ptr]; + struct packet_info pkt_info; + size_t len = esp_response_is_complete(resp, end - ptr, &pkt_info); + if (len) { + log_debug("<-: "); + debug_response(resp, len); + log_debug("\n"); + + if (is_prefix(resp, len, pktprefix, sizeof(pktprefix))) { + /* Received UDP packet. */ + log_debug("NET2TUN %lu: Read %d bytes from the esp interface\n", net2tap, pkt_info.payload_len); + net2tap++; + tun_tx_packet(tun_fd, pkt_info.payload, pkt_info.payload_len); + } + } else { + if (end == ESP_BUFSIZE) { + /* Buffer full but command wasn't complete - this isn't good, + * exit to prevent looping forever. */ + my_err("Buffer full with unterminated command"); + } + break; + } + ptr += len; + } + /* Remove processed responses from esp_buffer */ + memmove(esp_buffer, &esp_buffer[ptr], end - ptr); + end -= ptr; + if (!end) { + /* All responses processed, back to select. */ + break; + } + } + } + + // Handle input from TUN + if (fds[1].revents & POLLIN) { + /* data from tun/tap: just read it and write it to the network */ + size_t nread = cread(tun_fd, tap_buffer, TAP_BUFSIZE); + + log_debug("TUN2NET %lu: Read %d bytes from the tap interface\n", tap2net, nread); + + tap2net++; + esp_tx_packet(esp_fd, tap_buffer, nread); + } + } while (1); + + return 0; +} diff --git a/linux/term.c b/linux/term.c new file mode 100644 index 0000000..636f92e --- /dev/null +++ b/linux/term.c @@ -0,0 +1,130 @@ +/** term: Minimal interactive serial console. + * Usage: term /dev/ttyS1 + * + * Copyright (c) 2020 W.J. van der Laan + * Distributed under the MIT software license, + */ +#include +#include +#include +#include +#include +#include +#include +#include + +int set_raw(int fd) +{ + struct termios tty; + + if (tcgetattr(fd, &tty) < 0) { + printf("Error from tcgetattr: %s\n", strerror(errno)); + return -1; + } + + tty.c_iflag &= ~(ICRNL | IXON); + tty.c_lflag &= ~(ECHO | ICANON); + + if (tcsetattr(fd, TCSANOW, &tty) != 0) { + printf("Error from tcsetattr: %s\n", strerror(errno)); + return -1; + } + return 0; +} + +int set_interface_attribs(int fd, int speed) +{ + struct termios tty; + + if (tcgetattr(fd, &tty) < 0) { + printf("Error from tcgetattr: %s\n", strerror(errno)); + return -1; + } + + cfsetospeed(&tty, (speed_t)speed); + cfsetispeed(&tty, (speed_t)speed); + + tty.c_cflag |= (CLOCAL | CREAD); /* ignore modem controls */ + tty.c_cflag &= ~CSIZE; + tty.c_cflag |= CS8; /* 8-bit characters */ + tty.c_cflag &= ~PARENB; /* no parity bit */ + tty.c_cflag &= ~CSTOPB; /* only need 1 stop bit */ + tty.c_cflag &= ~CRTSCTS; /* no hardware flowcontrol */ + + /* setup for non-canonical mode */ + tty.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON); + tty.c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN); + tty.c_oflag &= ~OPOST; + + /* fetch bytes as they become available */ + tty.c_cc[VMIN] = 1; + tty.c_cc[VTIME] = 1; + + if (tcsetattr(fd, TCSANOW, &tty) != 0) { + printf("Error from tcsetattr: %s\n", strerror(errno)); + return -1; + } + return 0; +} + +int main(int argc, char **argv) { + if (argc < 2) { + fprintf(stderr, "Pass serial device on command line\n"); + exit(1); + } + const char *portname = argv[1]; + int fd = open(portname, O_RDWR | O_NOCTTY | O_SYNC); + if (fd < 0) { + fprintf(stderr, "Error opening %s: %s\n", portname, strerror(errno)); + exit(1); + } + + set_raw(STDIN_FILENO); + /*baudrate 115200, 8 bits, no parity, 1 stop bit */ + set_interface_attribs(fd, B115200); + + struct pollfd fds[2] = { + {STDIN_FILENO, POLLIN, 0}, + {fd, POLLIN, 0}, + }; + do { + for (int i=0; i<2; ++i) { + fds[i].revents = 0; + } + + if (poll(fds, 2, 0) < 0) { + fprintf(stderr, "Poll error: %s\n", strerror(errno)); + exit(1); + } + + for (int i=0; i<2; ++i) { + if (fds[i].revents & POLLIN) { + char ch; + int rdlen = read(fds[i].fd, &ch, 1); + if (rdlen < 0) { + fprintf(stderr, "Error from read: %d: %s\n", rdlen, strerror(errno)); + exit(1); + } else if (rdlen == 0) { + /* EOF */ + exit(0); + } + + if (i == 1 && ch == '\r') { + /* Don't print \r */ + continue; + } + if (i == 0 && ch == '\n') { + /* Add extra \r before newline */ + char cr = '\r'; + write(fds[1-i].fd, &cr, 1); + } + + int wlen = write(fds[1-i].fd, &ch, 1); + if (wlen <= 0) { + fprintf(stderr, "Error from write: %d: %s\n", wlen, strerror(errno)); + exit(1); + } + } + } + } while (1); +}