From 306f426b89a8c3a6fda8126e7934d66082202c7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20Lindel=C3=B6w?= Date: Mon, 13 Apr 2026 09:51:59 +0200 Subject: [PATCH 1/4] posix: Add ioctl SIOCGIFCONF fallback for systems without getifaddrs --- .github/workflows/ci.yml | 145 ++++++++++++++++++++++++ CMakeLists.txt | 7 ++ README.md | 57 +++++----- include/tinycsocket.h | 223 +++++++++++++++++++++++++++++++++++-- src/tinycsocket_common.c | 2 +- src/tinycsocket_internal.h | 7 +- src/tinycsocket_posix.c | 214 +++++++++++++++++++++++++++++++++-- tests/CMakeLists.txt | 6 + tests/tests.cpp | 9 ++ 9 files changed, 619 insertions(+), 51 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 23083fc..026b23f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -352,6 +352,52 @@ jobs: build/tests_openbsd_x86_64 build/tests_header_only_openbsd_x86_64 + omnios-x86_64-build: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + - uses: vmactions/omnios-vm@v1 + with: + usesh: true + prepare: | + pkg install cmake ninja gcc14 + run: | + mkdir build && cd build + cmake -G "Ninja" ../ -DCMAKE_VERBOSE_MAKEFILE=ON -DTCS_ENABLE_TESTS=ON -DTCS_ENABLE_EXAMPLES=ON -DTCS_WARNINGS_AS_ERRORS=ON + cmake --build . + mv ./tests/tests ./tests_omnios_x86_64 + mv ./tests/tests_header_only ./tests_header_only_omnios_x86_64 + - uses: actions/upload-artifact@v4 + with: + name: omnios-x86_64 + path: | + build/tests_omnios_x86_64 + build/tests_header_only_omnios_x86_64 + + omnios-x86_64-noifaddrs-build: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + - uses: vmactions/omnios-vm@v1 + with: + usesh: true + prepare: | + pkg install cmake ninja gcc14 + run: | + mkdir build && cd build + cmake -G "Ninja" ../ -DCMAKE_VERBOSE_MAKEFILE=ON -DTCS_ENABLE_TESTS=ON -DTCS_ENABLE_EXAMPLES=ON -DTCS_WARNINGS_AS_ERRORS=ON -DCMAKE_C_FLAGS="-DTCS_HAS_GETIFADDRS=0" + cmake --build . + mv ./tests/tests ./tests_omnios_x86_64_noifaddrs + mv ./tests/tests_header_only ./tests_header_only_omnios_x86_64_noifaddrs + - uses: actions/upload-artifact@v4 + with: + name: omnios-x86_64-noifaddrs + path: | + build/tests_omnios_x86_64_noifaddrs + build/tests_header_only_omnios_x86_64_noifaddrs + linux-s390x-build: runs-on: ubuntu-latest steps: @@ -404,6 +450,33 @@ jobs: build/tests_android_x86_64 build/tests_header_only_android_x86_64 + android-x86_64-api21-build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: nttld/setup-ndk@v1 + with: + ndk-version: r27 + id: ndk + - name: Build + run: | + mkdir build && cd build + cmake -G "Ninja" ../ \ + -DCMAKE_TOOLCHAIN_FILE=${{ steps.ndk.outputs.ndk-path }}/build/cmake/android.toolchain.cmake \ + -DANDROID_ABI=x86_64 \ + -DANDROID_PLATFORM=android-21 \ + -DTCS_ENABLE_TESTS=ON \ + -DTCS_WARNINGS_AS_ERRORS=ON + cmake --build . + mv ./tests/tests ./tests_android_x86_64_api21 + mv ./tests/tests_header_only ./tests_header_only_android_x86_64_api21 + - uses: actions/upload-artifact@v4 + with: + name: android-x86_64-api21 + path: | + build/tests_android_x86_64_api21 + build/tests_header_only_android_x86_64_api21 + cygwin-x86_64-build: runs-on: windows-latest steps: @@ -711,6 +784,52 @@ jobs: cat ./test_output.txt >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY + omnios-x86_64-test: + runs-on: ubuntu-latest + timeout-minutes: 30 + needs: [omnios-x86_64-build] + steps: + - uses: actions/download-artifact@v4 + with: + name: omnios-x86_64 + path: build/ + - uses: vmactions/omnios-vm@v1 + with: + usesh: true + run: | + chmod +x ./build/tests_omnios_x86_64 ./build/tests_header_only_omnios_x86_64 + ./build/tests_omnios_x86_64 2>&1 | tee ./test_output.txt + ./build/tests_header_only_omnios_x86_64 2>&1 | tee -a ./test_output.txt + - name: Write step summary + if: always() + run: | + echo '```' >> $GITHUB_STEP_SUMMARY + cat ./test_output.txt >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + + omnios-x86_64-noifaddrs-test: + runs-on: ubuntu-latest + timeout-minutes: 30 + needs: [omnios-x86_64-noifaddrs-build] + steps: + - uses: actions/download-artifact@v4 + with: + name: omnios-x86_64-noifaddrs + path: build/ + - uses: vmactions/omnios-vm@v1 + with: + usesh: true + run: | + chmod +x ./build/tests_omnios_x86_64_noifaddrs ./build/tests_header_only_omnios_x86_64_noifaddrs + ./build/tests_omnios_x86_64_noifaddrs 2>&1 | tee ./test_output.txt + ./build/tests_header_only_omnios_x86_64_noifaddrs 2>&1 | tee -a ./test_output.txt + - name: Write step summary + if: always() + run: | + echo '```' >> $GITHUB_STEP_SUMMARY + cat ./test_output.txt >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + linux-s390x-test: runs-on: ubuntu-latest timeout-minutes: 30 @@ -755,6 +874,32 @@ jobs: adb shell /data/local/tmp/tests adb shell /data/local/tmp/tests_header_only + android-x86_64-api21-test: + runs-on: ubuntu-latest + timeout-minutes: 30 + needs: [android-x86_64-api21-build] + steps: + - uses: actions/download-artifact@v4 + with: + name: android-x86_64-api21 + path: build/ + - name: Enable KVM + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + - uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: 30 + arch: x86_64 + script: | + adb root + adb push build/tests_android_x86_64_api21 /data/local/tmp/tests + adb push build/tests_header_only_android_x86_64_api21 /data/local/tmp/tests_header_only + adb shell chmod +x /data/local/tmp/tests /data/local/tmp/tests_header_only + adb shell /data/local/tmp/tests + adb shell /data/local/tmp/tests_header_only + cygwin-x86_64-test: runs-on: windows-latest timeout-minutes: 30 diff --git a/CMakeLists.txt b/CMakeLists.txt index 29ca33d..285b1ca 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -91,6 +91,10 @@ add_library(tinycsocket STATIC ${TINYCSOCKET_SRC}) add_library(tinycsockets ALIAS tinycsocket) target_include_directories(tinycsocket PUBLIC include PRIVATE src) target_link_libraries(tinycsocket PRIVATE tinycsocket_header) +# illumos/Solaris needs __EXTENSIONS__ for full POSIX visibility (like _GNU_SOURCE on glibc) +if(CMAKE_SYSTEM_NAME STREQUAL "SunOS") + target_compile_definitions(tinycsocket PRIVATE __EXTENSIONS__ _XOPEN_SOURCE=500) +endif() set_target_properties(tinycsocket PROPERTIES FOLDER tinycsocket) if(TCS_WARNINGS_AS_ERRORS) @@ -107,6 +111,9 @@ if(TCS_ENABLE_TESTS) add_library(tinycsocket_wrapped STATIC ${TINYCSOCKET_SRC} "src/dbg_wrap.h") target_include_directories(tinycsocket_wrapped PUBLIC include src) target_link_libraries(tinycsocket_wrapped INTERFACE tinycsocket_header) + if(CMAKE_SYSTEM_NAME STREQUAL "SunOS") + target_compile_definitions(tinycsocket_wrapped PRIVATE __EXTENSIONS__ _XOPEN_SOURCE=500) + endif() target_compile_options(tinycsocket_wrapped PUBLIC "-DDO_WRAP") endif() diff --git a/README.md b/README.md index 8810c42..272cc5f 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ See the example folder for information of how to use tinycsocket. Currently supported platforms: - Windows NT 5.0 SP1 (Windows 2000 SP1) or newer. - POSIX.1-2001 compliant systems (Linux, FreeBSD, OpenBSD, MacOS, Solaris etc.) -- Android (API level 24+, partial support for earlier versions) +- Android (API level 24+ for full IPv6 interface support) # Installation instructions @@ -128,29 +128,32 @@ like. These platforms are tested for every pull request. All configurations are tested as both static library and header-only. -| OS | Compiler | Arch | Notes | -|-----------------------------------|------------------------------------|----------|--------------| -| Windows Server 2025 | MSVC v19.44 | x86 | | -| Windows Server 2025 | MSVC v19.44 | x64 | | -| Windows Server 2025 | i686-w64-mingw32-gcc-posix v13 | x86 | WINVER 0x500 | -| Windows Server 2025 | i686-w64-mingw32-gcc-posix v13 | x86 | WINVER 0x502 | -| Windows Server 2025 | i686-w64-mingw32-gcc v15.2 | x86 | WINVER 0x603 | -| Windows Server 2025 | x86_64-w64-mingw32-gcc-posix v13 | x64 | WINVER 0x502 | -| Windows Server 2025 | x86_64-w64-mingw32-gcc v15.2 | x64 | WINVER 0x603 | -| Wine (Alpine 3.23) | MSVC v19.44 | x86 | | -| Wine (Alpine 3.23) | MSVC v19.44 | x64 | | -| Wine (Alpine 3.23) | i686-w64-mingw32-gcc-posix v13 | x86 | WINVER 0x500 | -| Wine (Alpine 3.23) | i686-w64-mingw32-gcc-posix v13 | x86 | WINVER 0x502 | -| Wine (Alpine 3.23) | i686-w64-mingw32-gcc v15.2 | x86 | WINVER 0x603 | -| Wine (Alpine 3.23) | x86_64-w64-mingw32-gcc-posix v13 | x64 | WINVER 0x502 | -| Wine (Alpine 3.23) | x86_64-w64-mingw32-gcc v15.2 | x64 | WINVER 0x603 | -| Linux Alpine 3.23 (musl) | gcc v15.2 | x86_64 | | -| Linux Alpine 3.23 (musl) | gcc v15.2 | x86 | | -| Linux Ubuntu 24.04 (glibc) | gcc v13.3 | x86_64 | | -| Linux Ubuntu 24.04 (glibc) | gcc v13.3 | x86 | | -| Linux Ubuntu 24.04 (glibc) | s390x-linux-gnu-gcc | s390x | Big-endian | -| MacOS (latest) | Apple Clang | arm64 | | -| FreeBSD 15.0 | Clang 19.1 | x86_64 | | -| OpenBSD (latest) | Clang | x86_64 | | -| Android API 24 (Bionic) | NDK Clang | x86_64 | Emulator | -| Cygwin (Windows Server 2025) | gcc | x86_64 | | +| OS | Compiler | Arch | Notes | +|-----------------------------------|------------------------------------|----------|--------------------------| +| Windows Server 2025 | MSVC v19.44 | x86 | | +| Windows Server 2025 | MSVC v19.44 | x64 | | +| Windows Server 2025 | i686-w64-mingw32-gcc-posix v13 | x86 | WINVER 0x500 | +| Windows Server 2025 | i686-w64-mingw32-gcc-posix v13 | x86 | WINVER 0x502 | +| Windows Server 2025 | i686-w64-mingw32-gcc v15.2 | x86 | WINVER 0x603 | +| Windows Server 2025 | x86_64-w64-mingw32-gcc-posix v13 | x64 | WINVER 0x502 | +| Windows Server 2025 | x86_64-w64-mingw32-gcc v15.2 | x64 | WINVER 0x603 | +| Wine (Alpine 3.23) | MSVC v19.44 | x86 | | +| Wine (Alpine 3.23) | MSVC v19.44 | x64 | | +| Wine (Alpine 3.23) | i686-w64-mingw32-gcc-posix v13 | x86 | WINVER 0x500 | +| Wine (Alpine 3.23) | i686-w64-mingw32-gcc-posix v13 | x86 | WINVER 0x502 | +| Wine (Alpine 3.23) | i686-w64-mingw32-gcc v15.2 | x86 | WINVER 0x603 | +| Wine (Alpine 3.23) | x86_64-w64-mingw32-gcc-posix v13 | x64 | WINVER 0x502 | +| Wine (Alpine 3.23) | x86_64-w64-mingw32-gcc v15.2 | x64 | WINVER 0x603 | +| Linux Alpine 3.23 (musl) | gcc v15.2 | x86_64 | | +| Linux Alpine 3.23 (musl) | gcc v15.2 | x86 | | +| Linux Ubuntu 24.04 (glibc) | gcc v13.3 | x86_64 | | +| Linux Ubuntu 24.04 (glibc) | gcc v13.3 | x86 | | +| Linux Ubuntu 24.04 (glibc) | s390x-linux-gnu-gcc | s390x | Big-endian | +| MacOS (latest) | Apple Clang | arm64 | | +| FreeBSD 15.0 | Clang 19.1 | x86_64 | | +| OpenBSD (latest) | Clang | x86_64 | | +| Android API 24 (Bionic) | NDK Clang | x86_64 | Emulator | +| Android API 21 (Bionic) | NDK Clang | x86_64 | Emulator, ioctl fallback | +| OmniOS (illumos) | gcc | x86_64 | | +| OmniOS (illumos) | gcc | x86_64 | ioctl fallback | +| Cygwin (Windows Server 2025) | gcc | x86_64 | | diff --git a/include/tinycsocket.h b/include/tinycsocket.h index 683a97f..9192b20 100644 --- a/include/tinycsocket.h +++ b/include/tinycsocket.h @@ -29,7 +29,7 @@ #ifndef TINYCSOCKET_INTERNAL_H_ #define TINYCSOCKET_INTERNAL_H_ -static const char* const TCS_VERSION_TXT = "v0.3.64"; +static const char* const TCS_VERSION_TXT = "v0.3.65"; static const char* const TCS_LICENSE_TXT = "Copyright 2018 Markus Lindelöw\n" "\n" @@ -197,6 +197,11 @@ typedef unsigned int TcsInterfaceId; // TODO: GUID is used for in vista at newer "POSIX symbols may be hidden. Use -std=gnu99 instead of -std=c99, " \ "or define _POSIX_C_SOURCE=200112L and _DEFAULT_SOURCE before including this header.") #endif +#if defined(__sun) && !defined(__EXTENSIONS__) +#pragma message( \ + "tinycsocket: illumos/Solaris detected without __EXTENSIONS__. " \ + "Define __EXTENSIONS__ before including this header for full POSIX support.") +#endif #endif typedef int TcsSocket; @@ -2687,9 +2692,14 @@ static inline int tds_map_remove(void** keys, #endif #ifndef TCS_HAS_GETIFADDRS -#if defined(__ANDROID__) && (__ANDROID_API__ >= 24) +#if defined(__ANDROID__) +#if __ANDROID_API__ >= 24 #define TCS_HAS_GETIFADDRS 1 -#elif defined(__linux__) || defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) +#else +#define TCS_HAS_GETIFADDRS 0 +#endif +#elif defined(__linux__) || defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) || \ + defined(__NetBSD__) || defined(__CYGWIN__) #define TCS_HAS_GETIFADDRS 1 #else #define TCS_HAS_GETIFADDRS 0 @@ -2707,10 +2717,13 @@ static inline int tds_map_remove(void** keys, #include // malloc()/free() #include // strcpy, memset #include // Flags for ifaddrs -#include // pretty much everything -#include // POSIX.1 compatibility -#include // struct iovec -#include // close() +#ifdef __sun +#include // SIOCGIFCONF on Solaris/illumos +#endif +#include // pretty much everything +#include // POSIX.1 compatibility +#include // struct iovec +#include // close() #if TCS_HAS_GETIFADDRS #include // getifaddr() @@ -4012,6 +4025,7 @@ TcsResult tcs_opt_membership_drop_from(TcsSocket socket_ctx, // ######## Address and Interface Utilities ######## +#if TCS_HAS_GETIFADDRS TcsResult tcs_interface_list(struct TcsInterface* found_interfaces, size_t interfaces_length, size_t* interfaces_populated) @@ -4049,6 +4063,90 @@ TcsResult tcs_interface_list(struct TcsInterface* found_interfaces, if_freenameindex(interfaces); return TCS_SUCCESS; } +#else +// Fallback using ioctl(SIOCGIFCONF) for systems without if_nameindex (e.g. Android < API 24) +TcsResult tcs_interface_list(struct TcsInterface* found_interfaces, + size_t interfaces_length, + size_t* interfaces_populated) +{ + if (found_interfaces == NULL && interfaces_populated == NULL) + return TCS_ERROR_INVALID_ARGUMENT; + + if (found_interfaces == NULL && interfaces_length != 0) + return TCS_ERROR_INVALID_ARGUMENT; + + if (interfaces_populated != NULL) + *interfaces_populated = 0; + + int fd = socket(AF_INET, SOCK_DGRAM, 0); + if (fd < 0) + return errno2retcode(errno); + + // Start with stack buffer, grow on heap if truncated + char stack_buf[512]; + char* buf = stack_buf; + int buf_len = (int)sizeof(stack_buf); + struct ifconf ifc; + + for (;;) + { + ifc.ifc_len = buf_len; + ifc.ifc_buf = buf; + if (ioctl(fd, SIOCGIFCONF, &ifc) != 0) + { + if (buf != stack_buf) + free(buf); + close(fd); + return errno2retcode(errno); + } + if (ifc.ifc_len < buf_len) + break; + // If caller only wants N results (not counting), don't grow beyond what's needed + if (found_interfaces != NULL && interfaces_populated == NULL && + (size_t)ifc.ifc_len / sizeof(struct ifreq) >= interfaces_length) + break; + buf_len *= 2; + if (buf != stack_buf) + free(buf); + buf = (char*)malloc((size_t)buf_len); + if (buf == NULL) + { + close(fd); + return TCS_ERROR_MEMORY; + } + } + + size_t count = 0; + int offset = 0; + while (offset < ifc.ifc_len) + { + struct ifreq* ifr = (struct ifreq*)(buf + offset); + int entry_len = (int)sizeof(struct ifreq); + + if (found_interfaces != NULL && count < interfaces_length) + { + strncpy(found_interfaces[count].name, ifr->ifr_name, TCS_INTERFACE_NAME_SIZE - 1); + found_interfaces[count].name[TCS_INTERFACE_NAME_SIZE - 1] = '\0'; + found_interfaces[count].id = (unsigned int)(count + 1); + } + count++; + + // Stop early if caller only wants results, not a total count + if (found_interfaces != NULL && interfaces_populated == NULL && count >= interfaces_length) + break; + + offset += entry_len; + } + + if (interfaces_populated != NULL) + *interfaces_populated = count; + + if (buf != stack_buf) + free(buf); + close(fd); + return TCS_SUCCESS; +} +#endif TcsResult tcs_address_resolve(const char* hostname, TcsAddressFamily address_family, @@ -4238,16 +4336,117 @@ TcsResult tcs_address_list(unsigned int interface_id_filter, return TCS_SUCCESS; } #else -// SunOS before 2010, HP and AIX does not support getifaddrs -// ioctl implementation, -// https://stackoverflow.com/questions/4139405/how-can-i-get-to-know-the-ip-address-for-interfaces-in-c/4139811#4139811 +// Fallback using ioctl(SIOCGIFCONF) for systems without getifaddrs (e.g. Android < API 24, old SunOS) +// Limitation: SIOCGIFCONF only returns IPv4 addresses. IPv6 enumeration would require +// platform-specific mechanisms (Linux: /proc/net/if_inet6 or netlink, Solaris: SIOCGLIFCONF). TcsResult tcs_address_list(unsigned int interface_id_filter, TcsAddressFamily address_family_filter, struct TcsInterfaceAddress interface_addresses[], size_t capacity, size_t* out_count) { - return TCS_ERROR_NOT_IMPLEMENTED; + if (interface_addresses == NULL && out_count == NULL) + return TCS_ERROR_INVALID_ARGUMENT; + if (interface_addresses == NULL && capacity != 0) + return TCS_ERROR_INVALID_ARGUMENT; + if (out_count != NULL) + *out_count = 0; + + // ioctl SIOCGIFCONF only supports IPv4 + if (address_family_filter != TCS_AF_ANY && address_family_filter != TCS_AF_IP4) + return TCS_SUCCESS; + + int fd = socket(AF_INET, SOCK_DGRAM, 0); + if (fd < 0) + return errno2retcode(errno); + + // Start with stack buffer, grow on heap if truncated + char stack_buf[512]; + char* buf = stack_buf; + int buf_len = (int)sizeof(stack_buf); + struct ifconf ifc; + + for (;;) + { + ifc.ifc_len = buf_len; + ifc.ifc_buf = buf; + if (ioctl(fd, SIOCGIFCONF, &ifc) != 0) + { + if (buf != stack_buf) + free(buf); + close(fd); + return errno2retcode(errno); + } + if (ifc.ifc_len < buf_len) + break; + // If caller only wants N results (not counting), don't grow beyond what's needed + if (interface_addresses != NULL && out_count == NULL && + (size_t)ifc.ifc_len / sizeof(struct ifreq) >= capacity) + break; + buf_len *= 2; + if (buf != stack_buf) + free(buf); + buf = (char*)malloc((size_t)buf_len); + if (buf == NULL) + { + close(fd); + return TCS_ERROR_MEMORY; + } + } + + size_t populated = 0; + int offset = 0; + unsigned int iface_index = 0; + while (offset < ifc.ifc_len) + { + struct ifreq* ifr = (struct ifreq*)(buf + offset); + int entry_len = (int)sizeof(struct ifreq); + iface_index++; + + if (interface_id_filter != 0 && iface_index != interface_id_filter) + { + offset += entry_len; + continue; + } + + struct TcsAddress address = TCS_ADDRESS_NONE; + TcsResult convert_status = native2sockaddr((struct sockaddr*)&ifr->ifr_addr, &address); + if (convert_status == TCS_ERROR_NOT_IMPLEMENTED) + { + offset += entry_len; + continue; + } + if (convert_status != TCS_SUCCESS) + { + if (buf != stack_buf) + free(buf); + close(fd); + return convert_status; + } + + if (interface_addresses != NULL && populated < capacity) + { + strncpy(interface_addresses[populated].iface.name, ifr->ifr_name, TCS_INTERFACE_NAME_SIZE - 1); + interface_addresses[populated].iface.name[TCS_INTERFACE_NAME_SIZE - 1] = '\0'; + interface_addresses[populated].iface.id = iface_index; + interface_addresses[populated].address = address; + populated++; + } + + if (out_count != NULL) + (*out_count)++; + + // Stop early if caller only wants results, not a total count + if (interface_addresses != NULL && out_count == NULL && populated >= capacity) + break; + + offset += entry_len; + } + + if (buf != stack_buf) + free(buf); + close(fd); + return TCS_SUCCESS; } #endif @@ -7449,7 +7648,7 @@ TcsResult tcs_address_parse(const char str[], struct TcsAddress* out_address) break; case A_PORT_ACCUM: ctx.port_val = ctx.port_val * 10 + (*c - '0'); - if (ctx.port_val > UINT16_MAX) + if ((uint32_t)ctx.port_val > UINT16_MAX) return TCS_ERROR_INVALID_ARGUMENT; break; case A_SCOPE_ACCUM: diff --git a/src/tinycsocket_common.c b/src/tinycsocket_common.c index 231be87..c95fbaa 100644 --- a/src/tinycsocket_common.c +++ b/src/tinycsocket_common.c @@ -1322,7 +1322,7 @@ TcsResult tcs_address_parse(const char str[], struct TcsAddress* out_address) break; case A_PORT_ACCUM: ctx.port_val = ctx.port_val * 10 + (*c - '0'); - if (ctx.port_val > UINT16_MAX) + if ((uint32_t)ctx.port_val > UINT16_MAX) return TCS_ERROR_INVALID_ARGUMENT; break; case A_SCOPE_ACCUM: diff --git a/src/tinycsocket_internal.h b/src/tinycsocket_internal.h index a7a347e..f638427 100644 --- a/src/tinycsocket_internal.h +++ b/src/tinycsocket_internal.h @@ -23,7 +23,7 @@ #ifndef TINYCSOCKET_INTERNAL_H_ #define TINYCSOCKET_INTERNAL_H_ -static const char* const TCS_VERSION_TXT = "v0.3.64"; +static const char* const TCS_VERSION_TXT = "v0.3.65"; static const char* const TCS_LICENSE_TXT = "Copyright 2018 Markus Lindelöw\n" "\n" @@ -191,6 +191,11 @@ typedef unsigned int TcsInterfaceId; // TODO: GUID is used for in vista at newer "POSIX symbols may be hidden. Use -std=gnu99 instead of -std=c99, " \ "or define _POSIX_C_SOURCE=200112L and _DEFAULT_SOURCE before including this header.") #endif +#if defined(__sun) && !defined(__EXTENSIONS__) +#pragma message( \ + "tinycsocket: illumos/Solaris detected without __EXTENSIONS__. " \ + "Define __EXTENSIONS__ before including this header for full POSIX support.") +#endif #endif typedef int TcsSocket; diff --git a/src/tinycsocket_posix.c b/src/tinycsocket_posix.c index 2f46571..125f2fc 100644 --- a/src/tinycsocket_posix.c +++ b/src/tinycsocket_posix.c @@ -43,9 +43,14 @@ #endif #ifndef TCS_HAS_GETIFADDRS -#if defined(__ANDROID__) && (__ANDROID_API__ >= 24) +#if defined(__ANDROID__) +#if __ANDROID_API__ >= 24 #define TCS_HAS_GETIFADDRS 1 -#elif defined(__linux__) || defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) +#else +#define TCS_HAS_GETIFADDRS 0 +#endif +#elif defined(__linux__) || defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) || \ + defined(__NetBSD__) || defined(__CYGWIN__) #define TCS_HAS_GETIFADDRS 1 #else #define TCS_HAS_GETIFADDRS 0 @@ -63,10 +68,13 @@ #include // malloc()/free() #include // strcpy, memset #include // Flags for ifaddrs -#include // pretty much everything -#include // POSIX.1 compatibility -#include // struct iovec -#include // close() +#ifdef __sun +#include // SIOCGIFCONF on Solaris/illumos +#endif +#include // pretty much everything +#include // POSIX.1 compatibility +#include // struct iovec +#include // close() #if TCS_HAS_GETIFADDRS #include // getifaddr() @@ -1368,6 +1376,7 @@ TcsResult tcs_opt_membership_drop_from(TcsSocket socket_ctx, // ######## Address and Interface Utilities ######## +#if TCS_HAS_GETIFADDRS TcsResult tcs_interface_list(struct TcsInterface* found_interfaces, size_t interfaces_length, size_t* interfaces_populated) @@ -1405,6 +1414,90 @@ TcsResult tcs_interface_list(struct TcsInterface* found_interfaces, if_freenameindex(interfaces); return TCS_SUCCESS; } +#else +// Fallback using ioctl(SIOCGIFCONF) for systems without if_nameindex (e.g. Android < API 24) +TcsResult tcs_interface_list(struct TcsInterface* found_interfaces, + size_t interfaces_length, + size_t* interfaces_populated) +{ + if (found_interfaces == NULL && interfaces_populated == NULL) + return TCS_ERROR_INVALID_ARGUMENT; + + if (found_interfaces == NULL && interfaces_length != 0) + return TCS_ERROR_INVALID_ARGUMENT; + + if (interfaces_populated != NULL) + *interfaces_populated = 0; + + int fd = socket(AF_INET, SOCK_DGRAM, 0); + if (fd < 0) + return errno2retcode(errno); + + // Start with stack buffer, grow on heap if truncated + char stack_buf[512]; + char* buf = stack_buf; + int buf_len = (int)sizeof(stack_buf); + struct ifconf ifc; + + for (;;) + { + ifc.ifc_len = buf_len; + ifc.ifc_buf = buf; + if (ioctl(fd, SIOCGIFCONF, &ifc) != 0) + { + if (buf != stack_buf) + free(buf); + close(fd); + return errno2retcode(errno); + } + if (ifc.ifc_len < buf_len) + break; + // If caller only wants N results (not counting), don't grow beyond what's needed + if (found_interfaces != NULL && interfaces_populated == NULL && + (size_t)ifc.ifc_len / sizeof(struct ifreq) >= interfaces_length) + break; + buf_len *= 2; + if (buf != stack_buf) + free(buf); + buf = (char*)malloc((size_t)buf_len); + if (buf == NULL) + { + close(fd); + return TCS_ERROR_MEMORY; + } + } + + size_t count = 0; + int offset = 0; + while (offset < ifc.ifc_len) + { + struct ifreq* ifr = (struct ifreq*)(buf + offset); + int entry_len = (int)sizeof(struct ifreq); + + if (found_interfaces != NULL && count < interfaces_length) + { + strncpy(found_interfaces[count].name, ifr->ifr_name, TCS_INTERFACE_NAME_SIZE - 1); + found_interfaces[count].name[TCS_INTERFACE_NAME_SIZE - 1] = '\0'; + found_interfaces[count].id = (unsigned int)(count + 1); + } + count++; + + // Stop early if caller only wants results, not a total count + if (found_interfaces != NULL && interfaces_populated == NULL && count >= interfaces_length) + break; + + offset += entry_len; + } + + if (interfaces_populated != NULL) + *interfaces_populated = count; + + if (buf != stack_buf) + free(buf); + close(fd); + return TCS_SUCCESS; +} +#endif TcsResult tcs_address_resolve(const char* hostname, TcsAddressFamily address_family, @@ -1594,16 +1687,117 @@ TcsResult tcs_address_list(unsigned int interface_id_filter, return TCS_SUCCESS; } #else -// SunOS before 2010, HP and AIX does not support getifaddrs -// ioctl implementation, -// https://stackoverflow.com/questions/4139405/how-can-i-get-to-know-the-ip-address-for-interfaces-in-c/4139811#4139811 +// Fallback using ioctl(SIOCGIFCONF) for systems without getifaddrs (e.g. Android < API 24, old SunOS) +// Limitation: SIOCGIFCONF only returns IPv4 addresses. IPv6 enumeration would require +// platform-specific mechanisms (Linux: /proc/net/if_inet6 or netlink, Solaris: SIOCGLIFCONF). TcsResult tcs_address_list(unsigned int interface_id_filter, TcsAddressFamily address_family_filter, struct TcsInterfaceAddress interface_addresses[], size_t capacity, size_t* out_count) { - return TCS_ERROR_NOT_IMPLEMENTED; + if (interface_addresses == NULL && out_count == NULL) + return TCS_ERROR_INVALID_ARGUMENT; + if (interface_addresses == NULL && capacity != 0) + return TCS_ERROR_INVALID_ARGUMENT; + if (out_count != NULL) + *out_count = 0; + + // ioctl SIOCGIFCONF only supports IPv4 + if (address_family_filter != TCS_AF_ANY && address_family_filter != TCS_AF_IP4) + return TCS_SUCCESS; + + int fd = socket(AF_INET, SOCK_DGRAM, 0); + if (fd < 0) + return errno2retcode(errno); + + // Start with stack buffer, grow on heap if truncated + char stack_buf[512]; + char* buf = stack_buf; + int buf_len = (int)sizeof(stack_buf); + struct ifconf ifc; + + for (;;) + { + ifc.ifc_len = buf_len; + ifc.ifc_buf = buf; + if (ioctl(fd, SIOCGIFCONF, &ifc) != 0) + { + if (buf != stack_buf) + free(buf); + close(fd); + return errno2retcode(errno); + } + if (ifc.ifc_len < buf_len) + break; + // If caller only wants N results (not counting), don't grow beyond what's needed + if (interface_addresses != NULL && out_count == NULL && + (size_t)ifc.ifc_len / sizeof(struct ifreq) >= capacity) + break; + buf_len *= 2; + if (buf != stack_buf) + free(buf); + buf = (char*)malloc((size_t)buf_len); + if (buf == NULL) + { + close(fd); + return TCS_ERROR_MEMORY; + } + } + + size_t populated = 0; + int offset = 0; + unsigned int iface_index = 0; + while (offset < ifc.ifc_len) + { + struct ifreq* ifr = (struct ifreq*)(buf + offset); + int entry_len = (int)sizeof(struct ifreq); + iface_index++; + + if (interface_id_filter != 0 && iface_index != interface_id_filter) + { + offset += entry_len; + continue; + } + + struct TcsAddress address = TCS_ADDRESS_NONE; + TcsResult convert_status = native2sockaddr((struct sockaddr*)&ifr->ifr_addr, &address); + if (convert_status == TCS_ERROR_NOT_IMPLEMENTED) + { + offset += entry_len; + continue; + } + if (convert_status != TCS_SUCCESS) + { + if (buf != stack_buf) + free(buf); + close(fd); + return convert_status; + } + + if (interface_addresses != NULL && populated < capacity) + { + strncpy(interface_addresses[populated].iface.name, ifr->ifr_name, TCS_INTERFACE_NAME_SIZE - 1); + interface_addresses[populated].iface.name[TCS_INTERFACE_NAME_SIZE - 1] = '\0'; + interface_addresses[populated].iface.id = iface_index; + interface_addresses[populated].address = address; + populated++; + } + + if (out_count != NULL) + (*out_count)++; + + // Stop early if caller only wants results, not a total count + if (interface_addresses != NULL && out_count == NULL && populated >= capacity) + break; + + offset += entry_len; + } + + if (buf != stack_buf) + free(buf); + close(fd); + return TCS_SUCCESS; } #endif diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ccffd66..0ec21b7 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -37,6 +37,9 @@ if(MINGW) ) endif() +if(CMAKE_SYSTEM_NAME STREQUAL "SunOS") + target_compile_definitions(test_translation_units PRIVATE __EXTENSIONS__ _XOPEN_SOURCE=500) +endif() set_target_properties( test_translation_units PROPERTIES FOLDER tinycsocket/tests/header_only @@ -73,6 +76,9 @@ target_include_directories( tests_header_only PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ) +if(CMAKE_SYSTEM_NAME STREQUAL "SunOS") + target_compile_definitions(tests_header_only PRIVATE __EXTENSIONS__ _XOPEN_SOURCE=500) +endif() set_target_properties(tests_header_only PROPERTIES FOLDER tinycsocket/tests) add_test(NAME tests COMMAND tests) diff --git a/tests/tests.cpp b/tests/tests.cpp index 6e8ea91..745d2d7 100644 --- a/tests/tests.cpp +++ b/tests/tests.cpp @@ -23,6 +23,15 @@ #include "tinycsocket.h" #include "tinydatastructures.h" +// illumos lacks the C++11 required integer overload for log10 (section 26.8) +#ifdef __sun +#include +static inline double log10(unsigned int x) +{ + return std::log10(static_cast(x)); +} +#endif + #define DOCTEST_CONFIG_IMPLEMENT #ifndef _MSC_VER #pragma GCC diagnostic push From a25c7c13e15cd959d0bca4eec17b69456fceb5ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20Lindel=C3=B6w?= Date: Mon, 13 Apr 2026 14:15:27 +0200 Subject: [PATCH 2/4] common: Make out_count always return total available count --- CMakeLists.txt | 2 ++ examples/CMakeLists.txt | 6 ++++ include/tinycsocket.h | 63 ++++++++++---------------------------- src/tinycsocket_internal.h | 4 +-- src/tinycsocket_posix.c | 22 +++---------- src/tinycsocket_win32.c | 37 ++++++---------------- 6 files changed, 40 insertions(+), 94 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 285b1ca..852081f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -84,6 +84,8 @@ target_include_directories( ) if(WIN32) target_link_libraries(tinycsocket_header INTERFACE wsock32 ws2_32 iphlpapi) +elseif(CMAKE_SYSTEM_NAME STREQUAL "SunOS") + target_link_libraries(tinycsocket_header INTERFACE socket nsl) endif() # Tinycsocket static library diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index d2de9a8..a31bc17 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -14,6 +14,12 @@ target_link_libraries(udp_client PRIVATE tinycsocket_header) add_executable(udp_server udp_server.c) target_link_libraries(udp_server PRIVATE tinycsocket_header) +if(CMAKE_SYSTEM_NAME STREQUAL "SunOS") + target_compile_definitions(tcp_server PRIVATE __EXTENSIONS__ _XOPEN_SOURCE=500) + target_compile_definitions(tcp_client PRIVATE __EXTENSIONS__ _XOPEN_SOURCE=500) + target_compile_definitions(udp_client PRIVATE __EXTENSIONS__ _XOPEN_SOURCE=500) + target_compile_definitions(udp_server PRIVATE __EXTENSIONS__ _XOPEN_SOURCE=500) +endif() set_target_properties( tcp_server tcp_client diff --git a/include/tinycsocket.h b/include/tinycsocket.h index 9192b20..abaa0fc 100644 --- a/include/tinycsocket.h +++ b/include/tinycsocket.h @@ -2169,7 +2169,7 @@ TcsResult tcs_opt_nonblocking_get(TcsSocket socket_ctx, bool* is_nonblocking); * * @param interfaces array to receive interface information, or NULL to only count. * @param capacity number of elements in the interfaces array. -* @param out_count pointer to receive the number of interfaces found. +* @param out_count pointer to receive the total number of interfaces available, which may exceed capacity. * @return #TCS_SUCCESS if successful, otherwise the error code. */ TcsResult tcs_interface_list(struct TcsInterface interfaces[], size_t capacity, size_t* out_count); @@ -2197,7 +2197,7 @@ TcsResult tcs_address_resolve(const char* hostname, * @param address_family_filter address family filter, or ::TCS_AF_ANY for all. * @param interface_addresses array to receive results, or NULL to only count. * @param capacity number of elements in the array. -* @param out_count pointer to receive the number of results. +* @param out_count pointer to receive the total number of results available, which may exceed capacity. * @return #TCS_SUCCESS if successful, otherwise the error code. */ TcsResult tcs_address_list(unsigned int interface_id_filter, @@ -4043,20 +4043,15 @@ TcsResult tcs_interface_list(struct TcsInterface* found_interfaces, if (interfaces == NULL) return errno2retcode(errno); - if (found_interfaces != NULL) + for (size_t i = 0; interfaces[i].if_index != 0; ++i) { - for (size_t i = 0; i < interfaces_length && interfaces[i].if_index != 0; ++i) + if (found_interfaces != NULL && i < interfaces_length) { strncpy(found_interfaces[i].name, interfaces[i].if_name, TCS_INTERFACE_NAME_SIZE - 1); found_interfaces[i].name[TCS_INTERFACE_NAME_SIZE - 1] = '\0'; found_interfaces[i].id = interfaces[i].if_index; - if (interfaces_populated != NULL) - *interfaces_populated += 1; } - } - else // found_interfaces == NULL && interface_populated != NULL - { - for (size_t i = 0; interfaces[i].if_index != 0; ++i) + if (interfaces_populated != NULL) *interfaces_populated += 1; } @@ -4131,10 +4126,6 @@ TcsResult tcs_interface_list(struct TcsInterface* found_interfaces, } count++; - // Stop early if caller only wants results, not a total count - if (found_interfaces != NULL && interfaces_populated == NULL && count >= interfaces_length) - break; - offset += entry_len; } @@ -4380,8 +4371,7 @@ TcsResult tcs_address_list(unsigned int interface_id_filter, if (ifc.ifc_len < buf_len) break; // If caller only wants N results (not counting), don't grow beyond what's needed - if (interface_addresses != NULL && out_count == NULL && - (size_t)ifc.ifc_len / sizeof(struct ifreq) >= capacity) + if (interface_addresses != NULL && out_count == NULL && (size_t)ifc.ifc_len / sizeof(struct ifreq) >= capacity) break; buf_len *= 2; if (buf != stack_buf) @@ -4436,10 +4426,6 @@ TcsResult tcs_address_list(unsigned int interface_id_filter, if (out_count != NULL) (*out_count)++; - // Stop early if caller only wants results, not a total count - if (interface_addresses != NULL && out_count == NULL && populated >= capacity) - break; - offset += entry_len; } @@ -6012,10 +5998,9 @@ TcsResult tcs_interface_list(struct TcsInterface interfaces[], size_t capacity, return TCS_ERROR_UNKNOWN; } - if (interfaces != NULL) { size_t i = 0; - for (PIP_ADAPTER_ADDRESSES iter = adapters; iter != NULL && i < capacity; iter = iter->Next) + for (PIP_ADAPTER_ADDRESSES iter = adapters; iter != NULL; iter = iter->Next) { bool is_up = false; TcsResult up_sts = adapter_is_up(iter, &is_up); @@ -6027,38 +6012,22 @@ TcsResult tcs_interface_list(struct TcsInterface interfaces[], size_t capacity, if (!is_up) continue; - memset(interfaces[i].name, '\0', TCS_INTERFACE_NAME_SIZE); - TcsResult name_sts = adapter_get_friendly_name(iter, interfaces[i].name, TCS_INTERFACE_NAME_SIZE - 1); - if (name_sts != TCS_SUCCESS) + if (interfaces != NULL && i < capacity) { - free(adapters); - return TCS_ERROR_SYSTEM; + memset(interfaces[i].name, '\0', TCS_INTERFACE_NAME_SIZE); + TcsResult name_sts = adapter_get_friendly_name(iter, interfaces[i].name, TCS_INTERFACE_NAME_SIZE - 1); + if (name_sts != TCS_SUCCESS) + { + free(adapters); + return TCS_ERROR_SYSTEM; + } + interfaces[i].id = iter->IfIndex; } - interfaces[i].id = iter->IfIndex; if (out_count != NULL) (*out_count)++; ++i; } } - else - { - size_t i = 0; - for (PIP_ADAPTER_ADDRESSES iter = adapters; iter != NULL; iter = iter->Next) - { - bool is_up = false; - TcsResult up_sts = adapter_is_up(iter, &is_up); - if (up_sts != TCS_SUCCESS) - { - free(adapters); - return TCS_ERROR_SYSTEM; - } - if (!is_up) - continue; - ++i; - } - if (out_count != NULL) - *out_count = i; - } free(adapters); return TCS_SUCCESS; diff --git a/src/tinycsocket_internal.h b/src/tinycsocket_internal.h index f638427..a22ffce 100644 --- a/src/tinycsocket_internal.h +++ b/src/tinycsocket_internal.h @@ -2163,7 +2163,7 @@ TcsResult tcs_opt_nonblocking_get(TcsSocket socket_ctx, bool* is_nonblocking); * * @param interfaces array to receive interface information, or NULL to only count. * @param capacity number of elements in the interfaces array. -* @param out_count pointer to receive the number of interfaces found. +* @param out_count pointer to receive the total number of interfaces available, which may exceed capacity. * @return #TCS_SUCCESS if successful, otherwise the error code. */ TcsResult tcs_interface_list(struct TcsInterface interfaces[], size_t capacity, size_t* out_count); @@ -2191,7 +2191,7 @@ TcsResult tcs_address_resolve(const char* hostname, * @param address_family_filter address family filter, or ::TCS_AF_ANY for all. * @param interface_addresses array to receive results, or NULL to only count. * @param capacity number of elements in the array. -* @param out_count pointer to receive the number of results. +* @param out_count pointer to receive the total number of results available, which may exceed capacity. * @return #TCS_SUCCESS if successful, otherwise the error code. */ TcsResult tcs_address_list(unsigned int interface_id_filter, diff --git a/src/tinycsocket_posix.c b/src/tinycsocket_posix.c index 125f2fc..3397a32 100644 --- a/src/tinycsocket_posix.c +++ b/src/tinycsocket_posix.c @@ -1394,20 +1394,15 @@ TcsResult tcs_interface_list(struct TcsInterface* found_interfaces, if (interfaces == NULL) return errno2retcode(errno); - if (found_interfaces != NULL) + for (size_t i = 0; interfaces[i].if_index != 0; ++i) { - for (size_t i = 0; i < interfaces_length && interfaces[i].if_index != 0; ++i) + if (found_interfaces != NULL && i < interfaces_length) { strncpy(found_interfaces[i].name, interfaces[i].if_name, TCS_INTERFACE_NAME_SIZE - 1); found_interfaces[i].name[TCS_INTERFACE_NAME_SIZE - 1] = '\0'; found_interfaces[i].id = interfaces[i].if_index; - if (interfaces_populated != NULL) - *interfaces_populated += 1; } - } - else // found_interfaces == NULL && interface_populated != NULL - { - for (size_t i = 0; interfaces[i].if_index != 0; ++i) + if (interfaces_populated != NULL) *interfaces_populated += 1; } @@ -1482,10 +1477,6 @@ TcsResult tcs_interface_list(struct TcsInterface* found_interfaces, } count++; - // Stop early if caller only wants results, not a total count - if (found_interfaces != NULL && interfaces_populated == NULL && count >= interfaces_length) - break; - offset += entry_len; } @@ -1731,8 +1722,7 @@ TcsResult tcs_address_list(unsigned int interface_id_filter, if (ifc.ifc_len < buf_len) break; // If caller only wants N results (not counting), don't grow beyond what's needed - if (interface_addresses != NULL && out_count == NULL && - (size_t)ifc.ifc_len / sizeof(struct ifreq) >= capacity) + if (interface_addresses != NULL && out_count == NULL && (size_t)ifc.ifc_len / sizeof(struct ifreq) >= capacity) break; buf_len *= 2; if (buf != stack_buf) @@ -1787,10 +1777,6 @@ TcsResult tcs_address_list(unsigned int interface_id_filter, if (out_count != NULL) (*out_count)++; - // Stop early if caller only wants results, not a total count - if (interface_addresses != NULL && out_count == NULL && populated >= capacity) - break; - offset += entry_len; } diff --git a/src/tinycsocket_win32.c b/src/tinycsocket_win32.c index 2ba58c3..01086e5 100644 --- a/src/tinycsocket_win32.c +++ b/src/tinycsocket_win32.c @@ -1499,10 +1499,9 @@ TcsResult tcs_interface_list(struct TcsInterface interfaces[], size_t capacity, return TCS_ERROR_UNKNOWN; } - if (interfaces != NULL) { size_t i = 0; - for (PIP_ADAPTER_ADDRESSES iter = adapters; iter != NULL && i < capacity; iter = iter->Next) + for (PIP_ADAPTER_ADDRESSES iter = adapters; iter != NULL; iter = iter->Next) { bool is_up = false; TcsResult up_sts = adapter_is_up(iter, &is_up); @@ -1514,38 +1513,22 @@ TcsResult tcs_interface_list(struct TcsInterface interfaces[], size_t capacity, if (!is_up) continue; - memset(interfaces[i].name, '\0', TCS_INTERFACE_NAME_SIZE); - TcsResult name_sts = adapter_get_friendly_name(iter, interfaces[i].name, TCS_INTERFACE_NAME_SIZE - 1); - if (name_sts != TCS_SUCCESS) + if (interfaces != NULL && i < capacity) { - free(adapters); - return TCS_ERROR_SYSTEM; + memset(interfaces[i].name, '\0', TCS_INTERFACE_NAME_SIZE); + TcsResult name_sts = adapter_get_friendly_name(iter, interfaces[i].name, TCS_INTERFACE_NAME_SIZE - 1); + if (name_sts != TCS_SUCCESS) + { + free(adapters); + return TCS_ERROR_SYSTEM; + } + interfaces[i].id = iter->IfIndex; } - interfaces[i].id = iter->IfIndex; if (out_count != NULL) (*out_count)++; ++i; } } - else - { - size_t i = 0; - for (PIP_ADAPTER_ADDRESSES iter = adapters; iter != NULL; iter = iter->Next) - { - bool is_up = false; - TcsResult up_sts = adapter_is_up(iter, &is_up); - if (up_sts != TCS_SUCCESS) - { - free(adapters); - return TCS_ERROR_SYSTEM; - } - if (!is_up) - continue; - ++i; - } - if (out_count != NULL) - *out_count = i; - } free(adapters); return TCS_SUCCESS; From 1ca238b3951e60fefb11251da574b1b3ebf00471 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20Lindel=C3=B6w?= Date: Mon, 13 Apr 2026 15:20:35 +0200 Subject: [PATCH 3/4] posix: Add AF_PACKET support to ioctl fallback via SIOCGIFHWADDR --- include/tinycsocket.h | 83 ++++++++++++++++++++++++-------------- src/tinycsocket_internal.h | 2 +- src/tinycsocket_posix.c | 81 ++++++++++++++++++++++++------------- tests/CMakeLists.txt | 1 + 4 files changed, 107 insertions(+), 60 deletions(-) diff --git a/include/tinycsocket.h b/include/tinycsocket.h index abaa0fc..302679a 100644 --- a/include/tinycsocket.h +++ b/include/tinycsocket.h @@ -200,7 +200,7 @@ typedef unsigned int TcsInterfaceId; // TODO: GUID is used for in vista at newer #if defined(__sun) && !defined(__EXTENSIONS__) #pragma message( \ "tinycsocket: illumos/Solaris detected without __EXTENSIONS__. " \ - "Define __EXTENSIONS__ before including this header for full POSIX support.") + "Define __EXTENSIONS__ and _XOPEN_SOURCE=500 before including this header.") #endif #endif @@ -4313,14 +4313,9 @@ TcsResult tcs_address_list(unsigned int interface_id_filter, interface_addresses[populated].iface.id = interface_id; interface_addresses[populated].address = address; populated++; - - if (out_count != NULL) - (*out_count)++; } - else if (interface_addresses == NULL && out_count != NULL) - { + if (out_count != NULL) (*out_count)++; - } } freeifaddrs(ifap); @@ -4343,8 +4338,13 @@ TcsResult tcs_address_list(unsigned int interface_id_filter, if (out_count != NULL) *out_count = 0; - // ioctl SIOCGIFCONF only supports IPv4 - if (address_family_filter != TCS_AF_ANY && address_family_filter != TCS_AF_IP4) + // Check if we support the requested address family in the ioctl fallback. + // IPv4 is always available. AF_PACKET is available on Linux via SIOCGIFHWADDR. + bool supported = (address_family_filter == TCS_AF_ANY || address_family_filter == TCS_AF_IP4); +#if TCS_HAS_AF_PACKET + supported = supported || (address_family_filter == TCS_AF_PACKET); +#endif + if (!supported) return TCS_SUCCESS; int fd = socket(AF_INET, SOCK_DGRAM, 0); @@ -4370,7 +4370,6 @@ TcsResult tcs_address_list(unsigned int interface_id_filter, } if (ifc.ifc_len < buf_len) break; - // If caller only wants N results (not counting), don't grow beyond what's needed if (interface_addresses != NULL && out_count == NULL && (size_t)ifc.ifc_len / sizeof(struct ifreq) >= capacity) break; buf_len *= 2; @@ -4386,45 +4385,69 @@ TcsResult tcs_address_list(unsigned int interface_id_filter, size_t populated = 0; int offset = 0; - unsigned int iface_index = 0; while (offset < ifc.ifc_len) { struct ifreq* ifr = (struct ifreq*)(buf + offset); int entry_len = (int)sizeof(struct ifreq); - iface_index++; - if (interface_id_filter != 0 && iface_index != interface_id_filter) + unsigned int iface_id = if_nametoindex(ifr->ifr_name); + if (iface_id == 0) { offset += entry_len; continue; } - struct TcsAddress address = TCS_ADDRESS_NONE; - TcsResult convert_status = native2sockaddr((struct sockaddr*)&ifr->ifr_addr, &address); - if (convert_status == TCS_ERROR_NOT_IMPLEMENTED) + if (interface_id_filter != 0 && iface_id != interface_id_filter) { offset += entry_len; continue; } - if (convert_status != TCS_SUCCESS) + + // IPv4 addresses + if (address_family_filter == TCS_AF_ANY || address_family_filter == TCS_AF_IP4) { - if (buf != stack_buf) - free(buf); - close(fd); - return convert_status; + struct TcsAddress address = TCS_ADDRESS_NONE; + TcsResult convert_status = native2sockaddr((struct sockaddr*)&ifr->ifr_addr, &address); + if (convert_status == TCS_SUCCESS) + { + if (interface_addresses != NULL && populated < capacity) + { + strncpy(interface_addresses[populated].iface.name, ifr->ifr_name, TCS_INTERFACE_NAME_SIZE - 1); + interface_addresses[populated].iface.name[TCS_INTERFACE_NAME_SIZE - 1] = '\0'; + interface_addresses[populated].iface.id = iface_id; + interface_addresses[populated].address = address; + populated++; + } + if (out_count != NULL) + (*out_count)++; + } } - if (interface_addresses != NULL && populated < capacity) +#if TCS_HAS_AF_PACKET + // AF_PACKET addresses via SIOCGIFHWADDR (Ethernet only) + if (address_family_filter == TCS_AF_ANY || address_family_filter == TCS_AF_PACKET) { - strncpy(interface_addresses[populated].iface.name, ifr->ifr_name, TCS_INTERFACE_NAME_SIZE - 1); - interface_addresses[populated].iface.name[TCS_INTERFACE_NAME_SIZE - 1] = '\0'; - interface_addresses[populated].iface.id = iface_index; - interface_addresses[populated].address = address; - populated++; - } + struct ifreq hw_req; + memset(&hw_req, 0, sizeof(hw_req)); + strncpy(hw_req.ifr_name, ifr->ifr_name, IFNAMSIZ - 1); - if (out_count != NULL) - (*out_count)++; + if (ioctl(fd, SIOCGIFHWADDR, &hw_req) == 0 && hw_req.ifr_hwaddr.sa_family == ARPHRD_ETHER) + { + if (interface_addresses != NULL && populated < capacity) + { + strncpy(interface_addresses[populated].iface.name, ifr->ifr_name, TCS_INTERFACE_NAME_SIZE - 1); + interface_addresses[populated].iface.name[TCS_INTERFACE_NAME_SIZE - 1] = '\0'; + interface_addresses[populated].iface.id = iface_id; + interface_addresses[populated].address.family = TCS_AF_PACKET; + interface_addresses[populated].address.data.packet.interface_id = iface_id; + memcpy(interface_addresses[populated].address.data.packet.mac, hw_req.ifr_hwaddr.sa_data, 6); + populated++; + } + if (out_count != NULL) + (*out_count)++; + } + } +#endif offset += entry_len; } diff --git a/src/tinycsocket_internal.h b/src/tinycsocket_internal.h index a22ffce..96d63e7 100644 --- a/src/tinycsocket_internal.h +++ b/src/tinycsocket_internal.h @@ -194,7 +194,7 @@ typedef unsigned int TcsInterfaceId; // TODO: GUID is used for in vista at newer #if defined(__sun) && !defined(__EXTENSIONS__) #pragma message( \ "tinycsocket: illumos/Solaris detected without __EXTENSIONS__. " \ - "Define __EXTENSIONS__ before including this header for full POSIX support.") + "Define __EXTENSIONS__ and _XOPEN_SOURCE=500 before including this header.") #endif #endif diff --git a/src/tinycsocket_posix.c b/src/tinycsocket_posix.c index 3397a32..aeec219 100644 --- a/src/tinycsocket_posix.c +++ b/src/tinycsocket_posix.c @@ -1664,14 +1664,9 @@ TcsResult tcs_address_list(unsigned int interface_id_filter, interface_addresses[populated].iface.id = interface_id; interface_addresses[populated].address = address; populated++; - - if (out_count != NULL) - (*out_count)++; } - else if (interface_addresses == NULL && out_count != NULL) - { + if (out_count != NULL) (*out_count)++; - } } freeifaddrs(ifap); @@ -1694,8 +1689,13 @@ TcsResult tcs_address_list(unsigned int interface_id_filter, if (out_count != NULL) *out_count = 0; - // ioctl SIOCGIFCONF only supports IPv4 - if (address_family_filter != TCS_AF_ANY && address_family_filter != TCS_AF_IP4) + // Check if we support the requested address family in the ioctl fallback. + // IPv4 is always available. AF_PACKET is available on Linux via SIOCGIFHWADDR. + bool supported = (address_family_filter == TCS_AF_ANY || address_family_filter == TCS_AF_IP4); +#if TCS_HAS_AF_PACKET + supported = supported || (address_family_filter == TCS_AF_PACKET); +#endif + if (!supported) return TCS_SUCCESS; int fd = socket(AF_INET, SOCK_DGRAM, 0); @@ -1721,7 +1721,6 @@ TcsResult tcs_address_list(unsigned int interface_id_filter, } if (ifc.ifc_len < buf_len) break; - // If caller only wants N results (not counting), don't grow beyond what's needed if (interface_addresses != NULL && out_count == NULL && (size_t)ifc.ifc_len / sizeof(struct ifreq) >= capacity) break; buf_len *= 2; @@ -1737,45 +1736,69 @@ TcsResult tcs_address_list(unsigned int interface_id_filter, size_t populated = 0; int offset = 0; - unsigned int iface_index = 0; while (offset < ifc.ifc_len) { struct ifreq* ifr = (struct ifreq*)(buf + offset); int entry_len = (int)sizeof(struct ifreq); - iface_index++; - if (interface_id_filter != 0 && iface_index != interface_id_filter) + unsigned int iface_id = if_nametoindex(ifr->ifr_name); + if (iface_id == 0) { offset += entry_len; continue; } - struct TcsAddress address = TCS_ADDRESS_NONE; - TcsResult convert_status = native2sockaddr((struct sockaddr*)&ifr->ifr_addr, &address); - if (convert_status == TCS_ERROR_NOT_IMPLEMENTED) + if (interface_id_filter != 0 && iface_id != interface_id_filter) { offset += entry_len; continue; } - if (convert_status != TCS_SUCCESS) + + // IPv4 addresses + if (address_family_filter == TCS_AF_ANY || address_family_filter == TCS_AF_IP4) { - if (buf != stack_buf) - free(buf); - close(fd); - return convert_status; + struct TcsAddress address = TCS_ADDRESS_NONE; + TcsResult convert_status = native2sockaddr((struct sockaddr*)&ifr->ifr_addr, &address); + if (convert_status == TCS_SUCCESS) + { + if (interface_addresses != NULL && populated < capacity) + { + strncpy(interface_addresses[populated].iface.name, ifr->ifr_name, TCS_INTERFACE_NAME_SIZE - 1); + interface_addresses[populated].iface.name[TCS_INTERFACE_NAME_SIZE - 1] = '\0'; + interface_addresses[populated].iface.id = iface_id; + interface_addresses[populated].address = address; + populated++; + } + if (out_count != NULL) + (*out_count)++; + } } - if (interface_addresses != NULL && populated < capacity) +#if TCS_HAS_AF_PACKET + // AF_PACKET addresses via SIOCGIFHWADDR (Ethernet only) + if (address_family_filter == TCS_AF_ANY || address_family_filter == TCS_AF_PACKET) { - strncpy(interface_addresses[populated].iface.name, ifr->ifr_name, TCS_INTERFACE_NAME_SIZE - 1); - interface_addresses[populated].iface.name[TCS_INTERFACE_NAME_SIZE - 1] = '\0'; - interface_addresses[populated].iface.id = iface_index; - interface_addresses[populated].address = address; - populated++; - } + struct ifreq hw_req; + memset(&hw_req, 0, sizeof(hw_req)); + strncpy(hw_req.ifr_name, ifr->ifr_name, IFNAMSIZ - 1); - if (out_count != NULL) - (*out_count)++; + if (ioctl(fd, SIOCGIFHWADDR, &hw_req) == 0 && hw_req.ifr_hwaddr.sa_family == ARPHRD_ETHER) + { + if (interface_addresses != NULL && populated < capacity) + { + strncpy(interface_addresses[populated].iface.name, ifr->ifr_name, TCS_INTERFACE_NAME_SIZE - 1); + interface_addresses[populated].iface.name[TCS_INTERFACE_NAME_SIZE - 1] = '\0'; + interface_addresses[populated].iface.id = iface_id; + interface_addresses[populated].address.family = TCS_AF_PACKET; + interface_addresses[populated].address.data.packet.interface_id = iface_id; + memcpy(interface_addresses[populated].address.data.packet.mac, hw_req.ifr_hwaddr.sa_data, 6); + populated++; + } + if (out_count != NULL) + (*out_count)++; + } + } +#endif offset += entry_len; } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 0ec21b7..d2a9b27 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -40,6 +40,7 @@ endif() if(CMAKE_SYSTEM_NAME STREQUAL "SunOS") target_compile_definitions(test_translation_units PRIVATE __EXTENSIONS__ _XOPEN_SOURCE=500) endif() + set_target_properties( test_translation_units PROPERTIES FOLDER tinycsocket/tests/header_only From 4b2d43a659975041647e0dbaed1cb096082f8e77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20Lindel=C3=B6w?= Date: Mon, 13 Apr 2026 16:46:12 +0200 Subject: [PATCH 4/4] ci: Fix s390x test exit code swallowed by tee pipe --- .github/workflows/ci.yml | 55 +++++++++++++++++++++++++++++----------- 1 file changed, 40 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 026b23f..6124b05 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -540,6 +540,7 @@ jobs: - name: Run tests shell: bash run: | + set -o pipefail echo '```' >> $GITHUB_STEP_SUMMARY ./build/tests_msvc_debug_x86.exe 2>&1 | tee -a $GITHUB_STEP_SUMMARY ./build/tests_msvc_release_x86.exe 2>&1 | tee -a $GITHUB_STEP_SUMMARY @@ -583,6 +584,7 @@ jobs: path: build/ - name: Run wine tests run: | + set -o pipefail echo '```' >> $GITHUB_STEP_SUMMARY docker run --rm \ -v ${{ github.workspace }}:/workspace \ @@ -628,6 +630,7 @@ jobs: path: build/ - name: Run wine tests run: | + set -o pipefail echo '```' >> $GITHUB_STEP_SUMMARY docker run --rm \ -v ${{ github.workspace }}:/workspace \ @@ -659,10 +662,11 @@ jobs: path: build/ - name: Run tests run: | + set -o pipefail echo '```' >> $GITHUB_STEP_SUMMARY chmod +x ./build/tests_linux_musl_x86_64 ./build/tests_header_only_linux_musl_x86_64 - ./build/tests_linux_musl_x86_64 2>&1 | tee -a $GITHUB_STEP_SUMMARY - ./build/tests_header_only_linux_musl_x86_64 2>&1 | tee -a $GITHUB_STEP_SUMMARY + sudo capsh --caps='cap_net_raw+eip' -- -c ./build/tests_linux_musl_x86_64 2>&1 | tee -a $GITHUB_STEP_SUMMARY + sudo capsh --caps='cap_net_raw+eip' -- -c ./build/tests_header_only_linux_musl_x86_64 2>&1 | tee -a $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY linux-test-musl-x86: @@ -676,10 +680,11 @@ jobs: path: build/ - name: Run tests run: | + set -o pipefail echo '```' >> $GITHUB_STEP_SUMMARY chmod +x ./build/tests_linux_musl_x86 ./build/tests_header_only_linux_musl_x86 - ./build/tests_linux_musl_x86 2>&1 | tee -a $GITHUB_STEP_SUMMARY - ./build/tests_header_only_linux_musl_x86 2>&1 | tee -a $GITHUB_STEP_SUMMARY + sudo capsh --caps='cap_net_raw+eip' -- -c ./build/tests_linux_musl_x86 2>&1 | tee -a $GITHUB_STEP_SUMMARY + sudo capsh --caps='cap_net_raw+eip' -- -c ./build/tests_header_only_linux_musl_x86 2>&1 | tee -a $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY linux-test-glibc-x86_64: @@ -693,10 +698,11 @@ jobs: path: build/ - name: Run tests run: | + set -o pipefail echo '```' >> $GITHUB_STEP_SUMMARY chmod +x ./build/tests_linux_glibc_x86_64 ./build/tests_header_only_linux_glibc_x86_64 - ./build/tests_linux_glibc_x86_64 2>&1 | tee -a $GITHUB_STEP_SUMMARY - ./build/tests_header_only_linux_glibc_x86_64 2>&1 | tee -a $GITHUB_STEP_SUMMARY + sudo capsh --caps='cap_net_raw+eip' -- -c ./build/tests_linux_glibc_x86_64 2>&1 | tee -a $GITHUB_STEP_SUMMARY + sudo capsh --caps='cap_net_raw+eip' -- -c ./build/tests_header_only_linux_glibc_x86_64 2>&1 | tee -a $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY linux-test-glibc-x86: @@ -710,8 +716,9 @@ jobs: path: build/ - name: Run tests run: | + set -o pipefail echo '```' >> $GITHUB_STEP_SUMMARY - docker run --rm \ + docker run --rm --cap-add=NET_RAW \ -v ${{ github.workspace }}:/workspace \ -w /workspace \ i386/debian:bookworm sh -c ' @@ -732,6 +739,7 @@ jobs: path: build/ - name: Run tests run: | + set -o pipefail echo '```' >> $GITHUB_STEP_SUMMARY chmod +x ./build/tests_macos_arm64 ./build/tests_header_only_macos_arm64 ./build/tests_macos_arm64 2>&1 | tee -a $GITHUB_STEP_SUMMARY @@ -752,8 +760,12 @@ jobs: usesh: true run: | chmod +x ./build/tests_freebsd_x86_64 ./build/tests_header_only_freebsd_x86_64 - ./build/tests_freebsd_x86_64 2>&1 | tee ./test_output.txt - ./build/tests_header_only_freebsd_x86_64 2>&1 | tee -a ./test_output.txt + ./build/tests_freebsd_x86_64 > /tmp/out1.txt 2>&1; s1=$? + cat /tmp/out1.txt + ./build/tests_header_only_freebsd_x86_64 > /tmp/out2.txt 2>&1; s2=$? + cat /tmp/out2.txt + cat /tmp/out1.txt /tmp/out2.txt > ./test_output.txt + [ $s1 -eq 0 ] && [ $s2 -eq 0 ] || exit 1 - name: Write step summary if: always() run: | @@ -775,8 +787,12 @@ jobs: usesh: true run: | chmod +x ./build/tests_openbsd_x86_64 ./build/tests_header_only_openbsd_x86_64 - ./build/tests_openbsd_x86_64 2>&1 | tee ./test_output.txt - ./build/tests_header_only_openbsd_x86_64 2>&1 | tee -a ./test_output.txt + ./build/tests_openbsd_x86_64 > /tmp/out1.txt 2>&1; s1=$? + cat /tmp/out1.txt + ./build/tests_header_only_openbsd_x86_64 > /tmp/out2.txt 2>&1; s2=$? + cat /tmp/out2.txt + cat /tmp/out1.txt /tmp/out2.txt > ./test_output.txt + [ $s1 -eq 0 ] && [ $s2 -eq 0 ] || exit 1 - name: Write step summary if: always() run: | @@ -798,8 +814,12 @@ jobs: usesh: true run: | chmod +x ./build/tests_omnios_x86_64 ./build/tests_header_only_omnios_x86_64 - ./build/tests_omnios_x86_64 2>&1 | tee ./test_output.txt - ./build/tests_header_only_omnios_x86_64 2>&1 | tee -a ./test_output.txt + ./build/tests_omnios_x86_64 > /tmp/out1.txt 2>&1; s1=$? + cat /tmp/out1.txt + ./build/tests_header_only_omnios_x86_64 > /tmp/out2.txt 2>&1; s2=$? + cat /tmp/out2.txt + cat /tmp/out1.txt /tmp/out2.txt > ./test_output.txt + [ $s1 -eq 0 ] && [ $s2 -eq 0 ] || exit 1 - name: Write step summary if: always() run: | @@ -821,8 +841,12 @@ jobs: usesh: true run: | chmod +x ./build/tests_omnios_x86_64_noifaddrs ./build/tests_header_only_omnios_x86_64_noifaddrs - ./build/tests_omnios_x86_64_noifaddrs 2>&1 | tee ./test_output.txt - ./build/tests_header_only_omnios_x86_64_noifaddrs 2>&1 | tee -a ./test_output.txt + ./build/tests_omnios_x86_64_noifaddrs > /tmp/out1.txt 2>&1; s1=$? + cat /tmp/out1.txt + ./build/tests_header_only_omnios_x86_64_noifaddrs > /tmp/out2.txt 2>&1; s2=$? + cat /tmp/out2.txt + cat /tmp/out1.txt /tmp/out2.txt > ./test_output.txt + [ $s1 -eq 0 ] && [ $s2 -eq 0 ] || exit 1 - name: Write step summary if: always() run: | @@ -841,6 +865,7 @@ jobs: path: build/ - name: Run tests run: | + set -o pipefail sudo apt-get update && sudo apt-get install -y qemu-user-static gcc-s390x-linux-gnu echo '```' >> $GITHUB_STEP_SUMMARY chmod +x ./build/tests_linux_s390x ./build/tests_header_only_linux_s390x