diff --git a/ChangeLog.md b/ChangeLog.md index bb116bda..74dca131 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -14,9 +14,11 @@ * add option to inject start-up commands * add verbose raw data option to "read" and "write" commands * add option to allow write direction in "read" command when inline defining a new message +* add option to discover device via mDNS ## Breaking Changes * change default config path to https://ebus.github.io/ serving files generated from new TypeSpec message definition sources +* change default device connection to be resolved automatically via mDNS # 23.3 (2023-12-26) diff --git a/README.md b/README.md index accbb4c2..a555939b 100644 --- a/README.md +++ b/README.md @@ -24,10 +24,11 @@ The main features of the daemon are: * TCP * UDP * enhanced ebusd protocol allowing arbitration to be done directly by the hardware, e.g. for recent - * [eBUS Adapter Shield](https://adapter.ebusd.eu/v5/), + * [eBUS Adapter Shields C6](https://adapter.ebusd.eu/v5-c6/) and [v5](https://adapter.ebusd.eu/v5/), * [adapter v3.1](https://adapter.ebusd.eu/v31)/[v3.0](https://adapter.ebusd.eu/v3), or * [ebusd-esp firmware](https://github.com/john30/ebusd-esp/) - * actively send messages to and receive answers from the eBUS + * auto-discover device connection via mDNS + * actively send messages to and receive answers from the eBUS * passively listen to messages sent on the eBUS * answer to messages received from the eBUS * regularly poll for messages diff --git a/src/ebusd/main.cpp b/src/ebusd/main.cpp index e5ee6c30..e85686fd 100644 --- a/src/ebusd/main.cpp +++ b/src/ebusd/main.cpp @@ -23,6 +23,7 @@ #include "ebusd/main.h" #include #include +#include #include #include #include @@ -34,6 +35,7 @@ #include "ebusd/network.h" #include "lib/utils/log.h" #include "lib/utils/httpclient.h" +#include "lib/utils/tcpsocket.h" #include "ebusd/scan.h" namespace ebusd { @@ -411,11 +413,52 @@ int main(int argc, char* argv[], char* envp[]) { return overallResult == RESULT_OK ? EXIT_SUCCESS : EXIT_FAILURE; } + const char* device = s_opt.device; + if (!s_opt.checkConfig && strncmp(device, "mdns:", 5) == 0) { + // auto discovery + mdns_oneshot_t address; + #define MAX_ADDRESSES 10 + mdns_oneshot_t addresses[MAX_ADDRESSES]; + size_t otherCount = MAX_ADDRESSES; + logWrite(lf_main, ll_notice, "discovering device from \"%s\"", device); + int ret = 0; + for (int i=0; i < 3 && ret == 0; i++) { // 3*up to 5 seconds = 15 seconds max + ret = resolveMdnsOneShot(device+5, &address, addresses, &otherCount); + } + if (ret < 0) { + logWrite(lf_main, ll_error, "unable to discover device \"%s\", error %d", device, ret); + cleanup(); + return EINVAL; + } + const char *ip; + for (size_t pos=0; pos < otherCount; pos++) { + mdns_oneshot_t *addr = addresses+pos; + ip = inet_ntoa(addr->address); + logWrite(lf_main, ll_info, "discovered another device with ID %s and device string %s:%s", + addr->id, addr->proto, ip); + } + if (ret == 0) { + logWrite(lf_main, ll_error, "unable to discover device \"%s\", %s found", device, otherCount ? "ID not" : "none"); + cleanup(); + return EINVAL; + } + if (ret > 1) { + logWrite(lf_main, ll_notice, + "found several devices from \"%s\", better limit to the desired one using e.g. \"mdns:%s\"", device, + address.id); + } + ip = inet_ntoa(address.address); + char *mdnsDevice = reinterpret_cast(malloc(4*4+3+1)); // ens:xxx.xxx.xxx.xxx + snprintf(mdnsDevice, 4*4+3+1, "%s:%s", address.proto, ip); + device = mdnsDevice; + logWrite(lf_main, ll_notice, "using discovered device with ID %s and device string %s", address.id, device); + } + s_busHandler = new BusHandler(s_messageMap, s_scanHelper, s_opt.pollInterval); // create the protocol and open the device ebus_protocol_config_t config = { - .device = s_opt.device, + .device = device, .noDeviceCheck = s_opt.noDeviceCheck, .readOnly = s_opt.readOnly, .extraLatency = s_opt.extraLatency, @@ -431,7 +474,7 @@ int main(int argc, char* argv[], char* envp[]) { }; s_protocol = ProtocolHandler::create(config, s_busHandler); if (s_protocol == nullptr) { - logWrite(lf_main, ll_error, "unable to create protocol/device %s", s_opt.device); // force logging on exit + logWrite(lf_main, ll_error, "unable to create protocol/device %s", config.device); // force logging on exit cleanup(); return EINVAL; } diff --git a/src/ebusd/main.h b/src/ebusd/main.h index 7bf4dbb8..9ab60efd 100755 --- a/src/ebusd/main.h +++ b/src/ebusd/main.h @@ -40,7 +40,7 @@ namespace ebusd { /** A structure holding all program options. */ typedef struct options { - const char* device; //!< eBUS device (serial device or [udp:]ip[:port]) [/dev/ttyUSB0] + const char* device; //!< eBUS device (serial device or mdns:[id] or [udp:]ip[:port]) [mdns:] bool noDeviceCheck; //!< skip serial eBUS device test bool readOnly; //!< read-only access to the device bool initialSend; //!< send an initial escape symbol after connecting device diff --git a/src/ebusd/main_args.cpp b/src/ebusd/main_args.cpp index b3b56724..2afce0f7 100755 --- a/src/ebusd/main_args.cpp +++ b/src/ebusd/main_args.cpp @@ -31,7 +31,7 @@ namespace ebusd { /** the default program options. */ static const options_t s_default_opt = { - .device = "/dev/ttyUSB0", + .device = "mdns:", .noDeviceCheck = false, .readOnly = false, .initialSend = false, @@ -134,11 +134,14 @@ static string s_configPath = CONFIG_PATH; static const argDef argDefs[] = { {nullptr, 0, nullptr, 0, "Device options:"}, {"device", 'd', "DEV", 0, "Use DEV as eBUS device (" + "\"mdns:\" for auto discovery via mDNS (" + "optional suffix with specific HW ID as well as specific IP interface after '@', " + "otherwise: " "prefix \"ens:\" for enhanced high speed device or " "\"enh:\" for enhanced device, with " "\"IP[:PORT]\" for network device or " "\"DEVICE\" for serial device" - ") [/dev/ttyUSB0]"}, + ") [mdns:]"}, {"nodevicecheck", 'n', nullptr, 0, "Skip serial eBUS device test"}, {"readonly", 'r', nullptr, 0, "Only read from device, never write to it"}, {"initsend", O_INISND, nullptr, 0, "Send an initial escape symbol after connecting device"}, @@ -232,7 +235,7 @@ static int parse_opt(int key, char *arg, const argParseOpt *parseOpt, struct opt switch (key) { // Device options: - case 'd': // --device=/dev/ttyUSB0 + case 'd': // --device=mdns: if (arg == nullptr || arg[0] == 0) { argParseError(parseOpt, "invalid device"); return EINVAL; diff --git a/src/lib/ebus/transport.cpp b/src/lib/ebus/transport.cpp index ecb29a5b..e9522162 100644 --- a/src/lib/ebus/transport.cpp +++ b/src/lib/ebus/transport.cpp @@ -325,7 +325,7 @@ void SerialTransport::checkDevice() { } result_t NetworkTransport::openInternal() { - m_fd = socketConnect(m_hostOrIp, m_port, m_udp, nullptr, 5, 2); // wait up to 5 seconds for established connection + m_fd = socketConnect(m_hostOrIp, m_port, m_udp ? IPPROTO_UDP : 0, nullptr, 5, 2); // wait up to 5 seconds for established connection if (m_fd < 0) { return RESULT_ERR_GENERIC_IO; } diff --git a/src/lib/knx/knxnet.h b/src/lib/knx/knxnet.h index aefe77c1..5041e59b 100644 --- a/src/lib/knx/knxnet.h +++ b/src/lib/knx/knxnet.h @@ -40,6 +40,7 @@ #include #include #include "lib/knx/knx.h" +#include "lib/utils/tcpsocket.h" namespace ebusd { @@ -228,7 +229,7 @@ typedef struct __attribute__ ((packed)) { #define SYSTEM_MULTICAST_PORT 3671 // the default system multicast address 224.0.23.12 -#define SYSTEM_MULTICAST_IP 0xe000170c +#define SYSTEM_MULTICAST_IP_STR "224.0.23.12" #define LAST_FRAME_TIMEOUT 2 @@ -400,96 +401,17 @@ class KnxNetConnection : public KnxConnection { // @copydoc const char* open() override { close(); - int ret; - struct in_addr mcast = {}; - mcast.s_addr = htonl(SYSTEM_MULTICAST_IP); - m_interface.s_addr = INADDR_ANY; - m_port = SYSTEM_MULTICAST_PORT; - if (m_url && m_url[0]) { // non-empty - string urlStr = m_url; // "[mcast][@intf]" for non-default 224.0.23.12:3671) - if (!urlStr.empty()) { - auto pos = urlStr.find('@'); - if (pos != string::npos) { - string intfStr = urlStr.substr(pos+1); - const char* intfCstr = intfStr.c_str(); - ret = inet_aton(intfCstr, &m_interface); - if (ret == 0) { - return "intf addr"; - } - urlStr = urlStr.substr(0, pos); - } - } - if (!urlStr.empty()) { - const char *mcastStr = urlStr.c_str(); - ret = inet_aton(mcastStr, &mcast); - if (ret == 0) { - return "multicast addr"; - } - } - } - - sockaddr_in address = {}; - address.sin_family = AF_INET; - address.sin_port = htons(m_port); - address.sin_addr.s_addr = INADDR_ANY; - - int fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + int fd = socketConnect(m_url && m_url[0] ? m_url : SYSTEM_MULTICAST_IP_STR, + SYSTEM_MULTICAST_PORT, IPPROTO_UDP, nullptr, 0x02); if (fd < 0) { return "create socket"; } - // set non-blocking - ret = fcntl(fd, F_SETFL, O_NONBLOCK); - if (ret != 0) { + if (fcntl(fd, F_SETFL, O_NONBLOCK) != 0) { ::close(fd); return "non-blocking"; } - // set reuse address option - int optint = 1; - ret = setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &optint, sizeof(optint)); - if (ret != 0) { - ::close(fd); - return "reuse"; - } - - // allow multiple processes using the same port for multicast on the same host - unsigned char optchar = 1; - ret = setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, &optchar, sizeof(optchar)); - if (ret != 0) { - ::close(fd); - return "mcast loop"; - } - - if (m_interface.s_addr != INADDR_ANY) { - // set outgoing interface to other than default (determined by routing table) - ret = setsockopt(fd, IPPROTO_IP, IP_MULTICAST_IF, &m_interface, sizeof(m_interface)); - if (ret != 0) { - ::close(fd); - return "mcast intf"; - } - } - - // bind for incoming multicast - ret = bind(fd, (struct sockaddr*) &address, sizeof(address)); - if (ret != 0) { - ::close(fd); - return "bind socket"; - } - - // set the target address for later use by sendto() - m_multicast = address; - m_multicast.sin_addr = mcast; - - // join the multicast inbound - ip_mreq req = {}; - req.imr_multiaddr = mcast; - req.imr_interface = m_interface; - if (setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &req, sizeof(req)) < 0) { - ::close(fd); - return "join multicast"; - } - m_sock = fd; return nullptr; } @@ -674,7 +596,7 @@ class KnxNetConnection : public KnxConnection { } d[0] = tpci; logTelegram(true, c, l, d); - ssize_t sent = sendto(m_sock, buf, totalLen, MSG_NOSIGNAL, (sockaddr*)&m_multicast, sizeof(m_multicast)); + ssize_t sent = ::send(m_sock, buf, totalLen, MSG_NOSIGNAL); if (sent < 0) { return "send error"; } @@ -711,15 +633,6 @@ class KnxNetConnection : public KnxConnection { /** the URL to connect to. */ const char* m_url; - /** the multicast address to join. */ - struct sockaddr_in m_multicast; - - /** the port to listen to. */ - in_port_t m_port; - - /** the optional interface address to bind to. */ - struct in_addr m_interface; - /** the socket if connected, or 0. */ int m_sock; diff --git a/src/lib/utils/tcpsocket.cpp b/src/lib/utils/tcpsocket.cpp index 19ef1a4a..2114b07b 100755 --- a/src/lib/utils/tcpsocket.cpp +++ b/src/lib/utils/tcpsocket.cpp @@ -46,38 +46,91 @@ bool TCPSocket::isValid() { return fcntl(m_sfd, F_GETFL) != -1; } +bool parseIp(const char* server, struct in_addr *sin_addr) { + if (inet_aton(server, sin_addr) == 1) { + return true; + } + struct hostent* he = gethostbyname(server); + if (he == nullptr) { + return false; + } + memcpy(sin_addr, he->h_addr_list[0], he->h_length); + return true; +} -int socketConnect(const char* server, uint16_t port, bool udp, socketaddress* storeAddress, int tcpConnectTimeout, -int tcpKeepAliveInterval) { +int socketConnect(const char* server, uint16_t port, int udpProto, socketaddress* storeAddress, +int tcpConnToUdpOptions, int tcpKeepAliveInterval, struct in_addr* storeIntf) { socketaddress localAddress; socketaddress* address = storeAddress ? storeAddress : &localAddress; memset(reinterpret_cast(address), 0, sizeof(*address)); - if (inet_aton(server, &address->sin_addr) == 0) { - struct hostent* he = gethostbyname(server); - if (he == nullptr) { + // parse "address[@intf]" + const char* pos = strchr(server, '@'); + struct in_addr intf; + intf.s_addr = INADDR_ANY; + if (pos) { + char* str = strdupa(server); + char* ifa = strchr(str, '@'); + ifa[0] = 0; + ifa++; + if (!str[0] || !parseIp(str, &address->sin_addr)) { return -1; } - memcpy(&address->sin_addr, he->h_addr_list[0], he->h_length); + if (!parseIp(ifa, &intf)) { + return -1; + } + } else if (!parseIp(server, &address->sin_addr)) { + return -1; + } + if (storeIntf) { + *storeIntf = intf; } address->sin_family = AF_INET; address->sin_port = (in_port_t)htons(port); - int sfd = socket(AF_INET, udp ? SOCK_DGRAM : SOCK_STREAM, 0); + int sfd = socket(AF_INET, udpProto ? SOCK_DGRAM : SOCK_STREAM, udpProto); if (sfd < 0) { - return -1; + return -2; } - int ret; - if (udp) { + int ret = 0; + if (udpProto) { + #define RET(chk, next) if (ret >= 0) { ret = chk; if (ret < 0) ret = next;} struct sockaddr_in bindAddress = *address; - bindAddress.sin_addr.s_addr = INADDR_ANY; - ret = bind(sfd, (struct sockaddr*)&bindAddress, sizeof(bindAddress)); - if (ret >= 0) { - ret = ::connect(sfd, (struct sockaddr*)address, sizeof(*address)); + // allow multiple processes using the same port for multicast on the same host + int optint = 1; + RET(setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR, &optint, sizeof(optint)), -3); +#ifdef SO_REUSEPORT + RET(setsockopt(sfd, SOL_SOCKET, SO_REUSEPORT, &optint, sizeof(optint)), -3); +#endif + bool isMcast = IN_MULTICAST(ntohl(address->sin_addr.s_addr)); + if (isMcast) { + // loop-back sent multicast packets + unsigned char optchar = 1; + RET(setsockopt(sfd, IPPROTO_IP, IP_MULTICAST_LOOP, &optchar, sizeof(optchar)), -3); + if (ret >= 0) { + // join the multicast inbound + ip_mreq req = {}; + req.imr_multiaddr = address->sin_addr; + req.imr_interface = intf; + RET(setsockopt(sfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &req, sizeof(req)), -7); + } + if (ret >= 0 && intf.s_addr != INADDR_ANY) { + // set outgoing interface to other than default (determined by routing table) + RET(setsockopt(sfd, IPPROTO_IP, IP_MULTICAST_IF, &intf, sizeof(intf)), -3); + } } - if (ret < 0) { - close(sfd); - return -1; + bindAddress.sin_addr = intf; + if (!(tcpConnToUdpOptions&0x01)) { + bindAddress.sin_port = 0; // do not bind to same source port for outgoing packets + } + RET(bind(sfd, (struct sockaddr*)&bindAddress, sizeof(bindAddress)), -4); + if (tcpConnToUdpOptions&0x02) { + // set the default target address for later use by send() + RET(::connect(sfd, (struct sockaddr*)address, sizeof(*address)), -5); + if (ret < 0) { + close(sfd); + return ret; + } } return sfd; } @@ -85,7 +138,7 @@ int tcpKeepAliveInterval) { ret = setsockopt(sfd, IPPROTO_TCP, TCP_NODELAY, reinterpret_cast(&value), sizeof(value)); if (ret < 0) { close(sfd); - return -1; + return -3; } if (tcpKeepAliveInterval > 0) { value = 1; @@ -124,66 +177,80 @@ int tcpKeepAliveInterval) { } #endif } - if (tcpConnectTimeout > 0 && fcntl(sfd, F_SETFL, O_NONBLOCK) < 0) { // set non-blocking + if (tcpConnToUdpOptions > 0 && fcntl(sfd, F_SETFL, O_NONBLOCK) < 0) { // set non-blocking close(sfd); - return -1; + return -4; } ret = ::connect(sfd, (struct sockaddr*)address, sizeof(*address)); if (ret != 0) { - if (ret < 0 && (tcpConnectTimeout <= 0 || errno != EINPROGRESS)) { + if (ret < 0 && (tcpConnToUdpOptions <= 0 || errno != EINPROGRESS)) { close(sfd); - return -1; + return -5; } - if (tcpConnectTimeout > 0) { -#if defined(HAVE_PPOLL) || defined(HAVE_PSELECT) - struct timespec tdiff; - tdiff.tv_sec = tcpConnectTimeout; - tdiff.tv_nsec = 0; -#else - struct timeval tdiff; - tdiff.tv_sec = tcpConnectTimeout; - tdiff.tv_usec = 0; -#endif -#ifdef HAVE_PPOLL - nfds_t nfds = 1; - struct pollfd fds[nfds]; - memset(fds, 0, sizeof(fds)); - fds[0].fd = sfd; - fds[0].events = POLLIN|POLLOUT; - ret = ppoll(fds, nfds, &tdiff, nullptr); - if (ret == 1 && fds[0].revents & POLLERR) { - ret = -1; - } -#else - fd_set readfds, writefds, exceptfds; - FD_ZERO(&readfds); - FD_ZERO(&writefds); - FD_ZERO(&exceptfds); - FD_SET(sfd, &readfds); - FD_SET(sfd, &writefds); - FD_SET(sfd, &exceptfds); -#ifdef HAVE_PSELECT - ret = pselect(sfd + 1, &readfds, &writefds, &exceptfds, &tdiff, nullptr); -#else - ret = select(sfd + 1, &readfds, &writefds, &exceptfds, &tdiff); -#endif - if (ret >= 1 && FD_ISSET(sfd, &exceptfds)) { - ret = -1; - } -#endif - if (ret == -1 || ret == 0) { + if (tcpConnToUdpOptions > 0) { + ret = socketPoll(sfd, POLLIN|POLLOUT, tcpConnToUdpOptions); + if (ret <= 0) { close(sfd); - return -1; + return -6; } if (fcntl(sfd, F_SETFL, 0) < 0) { // set blocking again close(sfd); - return -1; + return -4; } } } return sfd; } +int socketPoll(int sfd, int which, int timeoutSeconds) { + int ret; +#if defined(HAVE_PPOLL) || defined(HAVE_PSELECT) + struct timespec tdiff; + tdiff.tv_sec = timeoutSeconds; + tdiff.tv_nsec = 0; +#else + struct timeval tdiff; + tdiff.tv_sec = timeoutSeconds; + tdiff.tv_usec = 0; +#endif +#ifdef HAVE_PPOLL + nfds_t nfds = 1; + struct pollfd fds[nfds]; + memset(fds, 0, sizeof(fds)); + fds[0].fd = sfd; + fds[0].events = which; + ret = ppoll(fds, nfds, &tdiff, nullptr); + if (ret >= 1 && fds[0].revents & POLLERR) { + ret = -1; + } else if (ret >= 1) { + ret = fds[0].revents; + } +#else + fd_set readfds, writefds, exceptfds; + FD_ZERO(&readfds); + FD_ZERO(&writefds); + FD_ZERO(&exceptfds); + if (which & POLLIN) { + FD_SET(sfd, &readfds); + } + if (which & POLLOUT) { + FD_SET(sfd, &writefds); + } + FD_SET(sfd, &exceptfds); +#ifdef HAVE_PSELECT + ret = pselect(sfd + 1, &readfds, &writefds, &exceptfds, &tdiff, nullptr); +#else + ret = select(sfd + 1, &readfds, &writefds, &exceptfds, &tdiff); +#endif + if (ret >= 1 && FD_ISSET(sfd, &exceptfds)) { + ret = -1; + } else if (ret >= 1) { + ret = (FD_ISSET(sfd, &readfds) ? POLLIN : 0) | (FD_ISSET(sfd, &writefds) ? POLLOUT : 0); + } +#endif + return ret; +} + TCPSocket* TCPSocket::connect(const string& server, const uint16_t& port, int timeout) { socketaddress address; @@ -243,4 +310,335 @@ TCPSocket* TCPServer::newSocket() { return new TCPSocket(sfd, &address); } +size_t readNameRecursive(uint8_t *data, size_t len, size_t pos, size_t maxPos, int maxDepth, char* str, size_t slen, + size_t* spos) { + size_t nlen = data[pos++]; + if ((nlen&0xc0) == 0xc0) { + // pointer + size_t p = ((nlen&0x3f) << 8) | data[pos]; + if (p >= len || maxDepth < 1) { + return 0; + } + readNameRecursive(data, len, p, len, maxDepth-1, str, slen, spos); + return 2; + } + if (!nlen) { + return 1; + } + if (pos+nlen > maxPos || *spos+1+nlen > slen) { + return 0; + } + if (*spos > 0) { + str[*spos] = '.'; + *spos += 1; + } + memcpy(str+*spos, data+pos, nlen); + *spos += nlen; + pos += nlen; + size_t add; + if (pos >= maxPos || maxDepth < 1) { + add = 0; + } else { + add = readNameRecursive(data, len, pos, maxPos, maxDepth-1, str, slen, spos); + if (add == 0) { + return 0; + } + } + return 1+nlen+add; +} + +size_t readName(uint8_t *data, size_t len, size_t pos, size_t maxPos, char* str, size_t slen, size_t* spos) { + return readNameRecursive(data, len, pos, maxPos, 4, str, slen, spos); +} + +typedef struct __attribute__ ((packed)) { + uint16_t id; + struct { +#if __BYTE_ORDER == __BIG_ENDIAN + bool qr: 1; // 0=query, 1=answer + uint8_t opcode: 4; // 0=standard query, 1=inverse query, 2=status request + bool aa: 1; // authoritive answer + bool tc: 1; // truncation + bool rd: 1; // recursion desired +#else + bool rd: 1; // recursion desired + bool tc: 1; // truncation + bool aa: 1; // authoritive answer + uint8_t opcode: 4; // 0=standard query, 1=inverse query, 2=status request + bool qr: 1; // 0=query, 1=answer +#endif + }; + struct { +#if __BYTE_ORDER == __BIG_ENDIAN + bool ra: 1; // recursion available + uint8_t z: 3; // zero + uint8_t rcode: 4; // response code: 0=OK +#else + uint8_t rcode: 4; // response code: 0=OK + uint8_t z: 3; // zero + bool ra: 1; // recursion available +#endif + }; + uint16_t qdCount; // question section entry count + uint16_t anCount; // answer section entry count + uint16_t nsCount; // name server section entry count + uint16_t arCount; // additional records section entry count +} dns_query_t; + +typedef struct __attribute__ ((packed)) { + uint8_t len; + // unsigned char *name; +} dns_qname_t; + +typedef struct __attribute__ ((packed)) { + dns_qname_t qname; + uint16_t qtype; + uint16_t qclass; // top bit used for unicast-response +} dns_question_t; + +#define DNS_TYPE_A 0x01 +#define DNS_TYPE_PTR 0x0c +#define DNS_TYPE_TXT 0x10 +#define DNS_TYPE_SRV 0x21 +#define DNS_CLASS_AA 0x01 + +typedef struct __attribute__ ((packed)) { + dns_qname_t aname; + uint16_t atype; + uint16_t aclass; + uint32_t ttl; + uint16_t rdLength; + // uint8_t *rData; +} dns_answer_t; + +typedef struct __attribute__ ((packed)) { + uint16_t priority; + uint16_t weight; + uint16_t port; + dns_qname_t target; +} dns_rr_srv_t; + +int resolveMdnsOneShot(const char* url, mdns_oneshot_t *result, mdns_oneshot_t *moreResults, size_t *moreCount) { + memset(result, 0, sizeof(mdns_oneshot_t)); + socketaddress address; + const char* pos = strchr(url, '@'); + string limitId = string(url); + string device = "224.0.0.251"; + if (pos) { + limitId = limitId.substr(0, pos-url); + device += string(pos); + } + int sock = socketConnect(device.c_str(), 5353, IPPROTO_UDP, &address); + if (sock < 0) { + return -1; + } + + uint8_t record[1500]; + memset(record, 0, sizeof(record)); + dns_query_t *dnsr = reinterpret_cast(record); + dnsr->qdCount = htons(1); + size_t len = sizeof(dns_query_t); + dns_question_t *q = reinterpret_cast(reinterpret_cast(dnsr)+len); + const uint8_t serviceName[] = { + 0x06, 0x5f, 0x65, 0x62, 0x75, 0x73, 0x64, // _ebusd + 0x04, 0x5f, 0x74, 0x63, 0x70, // _tcp + 0x05, 0x6c, 0x6f, 0x63, 0x61, 0x6c, // local + 0x00 + }; + memcpy(&q->qname.len, serviceName, sizeof(serviceName)); + len += sizeof(serviceName)-1; // -1 for final empty qname + q = reinterpret_cast(reinterpret_cast(dnsr)+len); + q->qtype = htons(DNS_TYPE_PTR); + q->qclass = htons( + 0x8000 | // unicast response bit + DNS_CLASS_AA); + len += sizeof(dns_question_t); + ssize_t done = sendto(sock, record, len, 0, reinterpret_cast(&address), sizeof(address)); +#ifdef DEBUG_MDNS + printf("mdns: sent %ld, err %d\n", done, errno); +#endif + fcntl(sock, F_SETFL, O_NONBLOCK); + bool found = false, foundMore = false; + size_t moreRemain = moreResults && moreCount && *moreCount > 0 ? *moreCount : 0; + if (moreRemain > 0) { + *moreCount = 0; + } +#ifdef DEBUG_MDNS + socketaddress aaddr; + socklen_t aaddrlen = 0; +#endif + for (int i=0; i < (found ? 3 : 5); i++) { // up to 5 seconds, at least 3 seconds + int ret = socketPoll(sock, POLLIN, 1); + done = 0; + if (ret > 0 && (ret&POLLIN)) { +#ifdef DEBUG_MDNS + aaddrlen = sizeof(aaddr); + done = recvfrom(sock, record, sizeof(record), 0, reinterpret_cast(&aaddr), &aaddrlen); +#else + done = recv(sock, record, sizeof(record), 0); +#endif + } + if (done == 0 || (done < 0 && errno == EAGAIN) || done < sizeof(dns_query_t)) { + continue; + } + dnsr = reinterpret_cast(record); + // todo length check +#ifdef DEBUG_MDNS + printf("mdns: got %d from %2.2x:%d, q=%d, an=%d, ns=%d, ar=%d\n", done, aaddr.sin_addr.s_addr, + ntohs(aaddr.sin_port), ntohs(dnsr->qdCount), ntohs(dnsr->anCount), ntohs(dnsr->nsCount), + ntohs(dnsr->arCount)); +#endif + if (dnsr->qdCount || done < sizeof(dns_query_t)+sizeof(serviceName)+4*sizeof(dns_answer_t)+(26+2)+4+1+1+ + sizeof(dns_rr_srv_t)+(2+1+sizeof(mdns_oneshot_t::id)-1+1+5+1+sizeof(mdns_oneshot_t::proto)-1)+4 + // "eBUS Adapter Shield xxxxxx", "id=xxxxxxxxxxxx.proto=ens" + ) { + continue; + } + int anCnt = ntohs(dnsr->anCount); + int arCnt = ntohs(dnsr->arCount); + if (anCnt < 1 || dnsr->nsCount || arCnt < 1) { + continue; + } + len = sizeof(dns_query_t); + char name[256]; + bool validPort = false; + struct in_addr validAddress; + validAddress.s_addr = INADDR_ANY; + char id[sizeof(mdns_oneshot_t::id)] = {0}; + char proto[sizeof(mdns_oneshot_t::proto)] = {0}; + for (int i=0; i < anCnt+arCnt && len < done; i++) { + dns_answer_t *a = reinterpret_cast(reinterpret_cast(dnsr)+len); + if (i == 0) { + if (memcmp(&a->aname.len, serviceName, sizeof(serviceName)) != 0) { +#ifdef DEBUG_MDNS + printf("mdns: an 0 mismatch\n"); +#endif + anCnt = 0; + break; // skip this one + } +#ifdef DEBUG_MDNS + printf("mdns: an 0 match\n"); +#endif + len += sizeof(serviceName)-1; // -1 for final empty qname + } else { + // read name + size_t pos = 0; + size_t nlen = readName(record, done, len, done, name, sizeof(name), &pos); + if (nlen == 0) { + anCnt = 0; + break; // skip this one + } + len += nlen-1; // -1 for final empty qname / right pointer for below + name[pos] = 0; +#ifdef DEBUG_MDNS + printf("mdns: a%c %d name=%s\n", i >= anCnt ? 'r' : 'n', i >= anCnt ? i-anCnt : i, name); +#endif + } + a = reinterpret_cast(reinterpret_cast(dnsr)+len); + int atype = ntohs(a->atype); + int aclass = ntohs(a->aclass); +#ifdef DEBUG_MDNS + printf(" atype %d, aclass %d\n", atype, aclass); +#endif + if (i == 0 && (atype != DNS_TYPE_PTR + || aclass != DNS_CLASS_AA)) { + anCnt = 0; + break; // skip this one + } + len += sizeof(dns_answer_t); + int rdLen = ntohs(a->rdLength); +#ifdef DEBUG_MDNS + printf(" rd %d @%2.2x = ", rdLen, len); + for (int i=0; i < rdLen && len+i < done; i++) { + printf("%2.2x ", reinterpret_cast(dnsr)[len+i]); + } + printf("\n"); +#endif + if (atype == DNS_TYPE_PTR || atype == DNS_TYPE_TXT) { + size_t pos = 0; + if (readName(record, done, len, len+rdLen, name, sizeof(name), &pos) == 0) { + anCnt = 0; + break; // skip this one + } + name[pos] = 0; +#ifdef DEBUG_MDNS + printf(" %s=%s\n", (atype == DNS_TYPE_TXT) ? "txt" : "ptr", name); +#endif + if (atype == DNS_TYPE_TXT && name[0]) { + char* sep = strchr(name, '='); + char* sep2; + if (sep && strncmp(name, "id", sep-name) == 0) { + sep2 = strchr(name, '.'); + if (sep2-sep-1 == sizeof(mdns_oneshot_t::id)-1) { + memcpy(id, sep+1, sizeof(mdns_oneshot_t::id)-1); + } else { + sep = nullptr; + } + sep = sep ? strchr(sep2+1, '=') : nullptr; + } + if (sep && strncmp(sep2+1, "proto", sep-sep2-1) == 0 && ( + pos == sep+1+sizeof(mdns_oneshot_t::proto)-1-name + || strchr(sep+1, '.') == sep+1+sizeof(mdns_oneshot_t::proto)-1)) { + memcpy(proto, sep+1, sizeof(mdns_oneshot_t::proto)-1); + } + } + } else if (atype == DNS_TYPE_SRV && rdLen >= sizeof(dns_rr_srv_t)) { + dns_rr_srv_t *srv = reinterpret_cast(record+len); + size_t pos = 0; + if (readName(record, done, len+sizeof(dns_rr_srv_t)-1, len+rdLen, name, sizeof(name), &pos) == 0) { + anCnt = 0; + break; // skip this one + } + name[pos] = 0; + validPort = ntohs(srv->port) == 9999; +#ifdef DEBUG_MDNS + printf(" srv port %d target %s\n", ntohs(srv->port), name); +#endif + } else if (atype == DNS_TYPE_A) { + // ipv4 address +#ifdef DEBUG_MDNS + printf(" address %d.%d.%d.%d\n", record[len], record[len+1], record[len+2], record[len+3]); +#endif + memcpy(reinterpret_cast(&validAddress.s_addr), record+len, 4); + } + len += rdLen; + } + if (!anCnt) { + continue; + } + if (validPort && validAddress.s_addr != INADDR_ANY && validAddress.s_addr != INADDR_NONE && proto[0]) { + mdns_oneshot_t *storeTo; + if (!found && (!limitId.length() || limitId.compare(id) == 0)) { + storeTo = result; + found = true; + if (limitId.length()) { + break; // found the desired one, no need to wait for another + } + } else if (found && strcmp(id, result->id) == 0) { + // skip duplicate answer + continue; + } else { + foundMore = !limitId.length(); + if (moreRemain > 0) { + storeTo = moreResults++; + moreRemain--; + (*moreCount)++; + } else if (!found) { + continue; + } else { + break; + } + } + storeTo->address = validAddress; + strncpy(storeTo->id, id, sizeof(mdns_oneshot_t::id)); + strncpy(storeTo->proto, proto, sizeof(mdns_oneshot_t::proto)); + if (found && moreRemain == 0) { + break; + } + } + } + close(sock); + return found ? foundMore ? 2 : 1 : 0; +} + } // namespace ebusd diff --git a/src/lib/utils/tcpsocket.h b/src/lib/utils/tcpsocket.h index 429b487c..139ad196 100755 --- a/src/lib/utils/tcpsocket.h +++ b/src/lib/utils/tcpsocket.h @@ -25,6 +25,11 @@ #include #include #include +#ifdef __FreeBSD__ + #include +#else + #include +#endif /** typedef for referencing @a sockaddr_in within namespace. */ typedef struct sockaddr_in socketaddress; @@ -43,16 +48,29 @@ using std::string; /** * Connect a TCP or UDP socket. - * @param server the server name or ip address to connect to. + * @param server the server name or ip address to connect to, optionally + * followed by "@intf" to bind to a certain interface address. * @param port the port number. - * @param udp true for UDP, false for TCP. + * @param udpProto the protocol to use for UDP (e.g. IPPROTO_UDP), or 0 for TCP. * @param storeAddress optional pointer to where the socket address will be stored. - * @param tcpConnectTimeout the TCP connect timeout in seconds, or 0. + * @param tcpConnectTimeoutUdpOptions the connect timeout in seconds for TCP (or 0), + * or a bit set of options for UDP (0x01 for binding to the same source port, + * 0x02 for connecting to the target address). * @param tcpKeepAliveInterval optional interval in seconds for sending TCP keepalive. + * @param storeIntf optional pointer to where the interface address will be stored. * @return the connected socket file descriptor on success, or -1 on error. */ -int socketConnect(const char* server, uint16_t port, bool udp, socketaddress* storeAddress = nullptr, -int tcpConnectTimeout = 0, int tcpKeepAliveInterval = 0); +int socketConnect(const char* server, uint16_t port, int udpProto, socketaddress* storeAddress = nullptr, +int tcpConnectTimeoutUdpOptions = 0, int tcpKeepAliveInterval = 0, struct in_addr* storeIntf = nullptr); + +/** + * Poll a socket. + * @param sfd the socket file descriptor. + * @param which the set of bits of the event(s) to wait for (e.g. POLLIN and/or POLLOUT). + * @param timeoutSeconds the poll timeout in seconds. + * @return a set of bits indicating the received event (e.g. POLLIN and/or POLLOUT), or -1 on error. + */ +int socketPoll(int sfd, int which, int timeoutSeconds); /** @@ -198,6 +216,31 @@ class TCPServer { bool m_listening; }; +/** + * Structure for resolving device address via mDNS one-shot query. + */ +typedef struct __attribute__ ((packed)) { + /** the device IP address. */ + struct in_addr address; + /** the device ID. */ + char id[6*2+1]; + /** the announced ebusd protocol. */ + char proto[3+1]; +} mdns_oneshot_t; + +/** + * Use an mDNS one-shot query to resolve an eBUS device. + * @param url the desired ID (or empty) followed by an optional host interface IP to use after an '@' sign. + * @param result pointer to an mdns_oneshot_t structure to store the result in. + * @param moreRequests optional pointer to further results not matching the desired ID. + * @param moreCount optional pointer to the size of the moreRequests argument that will be updated with the number of + * further results found upon success. + * @return 1 on success, 2 when another device was found, 0 when no device was found or no found device matched the + * desired ID, or less than 0 on error. + */ +int resolveMdnsOneShot(const char* url, mdns_oneshot_t *result, + mdns_oneshot_t *moreResults = nullptr, size_t *moreCount = nullptr); + } // namespace ebusd #endif // LIB_UTILS_TCPSOCKET_H_ diff --git a/src/tools/ebuspicloader.cpp b/src/tools/ebuspicloader.cpp index 6ebb0362..7822d811 100644 --- a/src/tools/ebuspicloader.cpp +++ b/src/tools/ebuspicloader.cpp @@ -807,7 +807,7 @@ int openSerial(std::string port) { int openNet(std::string host, uint16_t port) { // open network port - int fd = socketConnect(host.c_str(), port, false, nullptr, 5); + int fd = socketConnect(host.c_str(), port, 0, nullptr, 5); if (fd < 0) { std::cerr << "unable to open " << host << std::endl; return -1;