Folders and files
| Name | Name | Last commit date | ||
|---|---|---|---|---|
Repository files navigation
Samurai
=======
A portable C++ platform abstraction library: buffers, files and directories,
unicode, compression, sockets (TCP/UDP/multicast) with four socket-monitor
backends, a DNS resolver, an XML reader, an HTTP/1.1 client, UPnP gateway port
mapping, and Tiger/TigerTree hashing.
Building
--------
Samurai builds with CMake (3.16 or newer):
git clone --recurse-submodules https://github.com/janvidar/samurai.git
cmake -S . -B build
cmake --build build -j
ctest --test-dir build
The submodule is the exotic test generator, which the automated suite is built
through. In a clone made without it, run
git submodule update --init
The library itself does not need it; without it the suite is skipped, with a
notice at configure time.
Requirements: a C++20 compiler, zlib, bzip2, and either OpenSSL 3.0 or newer or
LibreSSL 3.5 or newer. TLS is mandatory, and the two are used through the same
API. Connections require TLS 1.2 at a minimum and use TLS 1.3 whenever the peer
supports it.
Certificates are verified by default. A client checks the server's chain against
the system trust store and its name against the certificate, and refuses the
connection if either fails, so the peer name has to be set before the handshake.
A server does not ask its clients for a certificate unless it is told to.
Both settings belong to the connection, not to the process, so reaching one peer
that cannot be verified does not stop verifying any of the others. Call
setAllowUntrusted(true) or setRequireClientCertificate(true) on the TlsFactory
before initialize(); afterwards they change nothing. A connection starts from the
process-wide defaults, which TlsFactory::setDefaultAllowUntrusted() and
TlsFactory::setDefaultRequireClientCertificate() move for connections made after
the call.
Both are found through find_package(OpenSSL); pass -DOPENSSL_ROOT_DIR=... to
pick a particular installation. The configure summary names the one that was
found, which is worth checking: LibreSSL reports itself as "OpenSSL 2.0.0" to
everything that reads OPENSSL_VERSION_NUMBER.
Proxies
-------
An outbound TCP connection can be made through a SOCKS5 proxy, Tor's SocksPort
included:
ProxySettings::setDefault(ProxySettings::tor()); // 127.0.0.1:9050
// or per connection, before connect()
auto socket = Socket::create(&handler, "example.com", 443);
socket->setProxy(ProxySettings("127.0.0.1", 1080));
socket->connect();
A socket takes the process default when it is constructed, so moving the default
never disturbs a connection already under way, and setProxy() overrides it for
one connection. ProxySettings::fromString() parses "host:port" and
"socks5://user:password@host:port", which is what a command line hands over.
Username and password are RFC 1929; Tor reads a distinct pair as a stream
isolation key when IsolateSOCKSAuth is on, which is how two connections are kept
off one circuit.
A proxied socket never resolves its peer. The name travels to the proxy inside
the CONNECT request and is resolved on the far side, because a name handed to the
system resolver is a plaintext question to whatever resolv.conf names, and that
question names the peer whether or not the connection is tunnelled. So there is
no equivalent of the distinction curl draws between socks5:// and socks5h://:
this is always the latter. An .onion name needs nothing special - it is not an
address literal, so it goes out as a domain name like any other. Socket::lookup()
refuses on a proxied socket rather than resolving anyway; connect() needs no
address.
Nothing goes through the proxy as a datagram. CONNECT is the only command that
can be sent: BIND and UDP ASSOCIATE are not implemented and cannot be asked for,
so there is no way to arrange anything through a proxy that is not a stream. Tor
carries no datagrams in any case.
A peer on the local network is refused before the request is sent - 10.0.0.0/8,
127.0.0.0/8, 169.254.0.0/16, fc00::/7, fe80::/10, and the names "localhost" and
anything under .local. Through Tor such a request cannot succeed, no exit relay
routing those; through any other proxy it can, which is worse, since an address
the program did not choose for itself turns the proxy into a way to reach
machines its user cannot see. ProxySettings::setAllowPrivateTargets(true) is for
a proxy that exists precisely to reach a network segment.
TLS runs inside the tunnel. EventConnected is not delivered until the proxy has
confirmed the peer, so a handler that starts the handshake there is already in the
right order, and the certificate is verified against the name that was asked for
rather than against the proxy. TLSInitialize() refuses while the proxy handshake
is still running.
Tor extends SOCKS5 with two commands that return an answer instead of opening a
stream, RESOLVE and RESOLVE_PTR. With ProxySettings::setTorExtensions(true) the
DNS resolver uses them, so a program that has to resolve a name for its own sake
can do so without asking the system resolver. It is off by default: a proxy that
is not Tor answers an unknown command with command-not-supported, which costs a
round trip and then fails a lookup that would have worked. The proxy has to be
given as an address literal for this, since resolving a proxy's own name is a
question only the system resolver can answer.
What is NOT proxied, and will not be:
DatagramSocket, MulticastSocket no datagram is proxied, see above
UPnP gateway port mapping SSDP over multicast, then HTTP to an
address on the local network
ServerSocket an inbound connection arrives directly
DNS::Resolver unless Tor extensions are on
These keep working exactly as they did, and bypass the proxy. A program that
needs every packet to go through one has to leave them alone; the library does
not refuse them on its behalf.
netcat and wget take --socks5 host:port and --tor, which is how this is exercised
against a real proxy:
./build/test/netcat --tor duckduckgogg42xjoc72x3sjasowoarfbgcmvfimaftt6twagswzczad.onion 80
./build/test/wget --socks5 127.0.0.1:9050 https://check.torproject.org/api/ip
Configuration
-------------
-DCMAKE_BUILD_TYPE=Debug|Release Debug (the default) enables the
QDBG/QERR tracing macros.
-DSAMURAI_BUILD_SHARED=ON|OFF Build libsamurai.so. Default ON.
-DSAMURAI_BUILD_STATIC=ON|OFF Build libsamurai.a. Default ON.
Both flavours share one compile pass.
-DSAMURAI_BUILD_TESTS=ON|OFF Build the test programs. Default ON.
-DSAMURAI_SANITIZE=ON|OFF Build with AddressSanitizer and
UndefinedBehaviorSanitizer. Default
OFF. Needs GCC or Clang. The flags
are a usage requirement, so anything
linking the library is instrumented
too.
-DSAMURAI_SANITIZE_THREAD=ON|OFF Build with ThreadSanitizer. Default
OFF. Cannot be combined with
SAMURAI_SANITIZE, since the two
cannot instrument the same build;
use a second build directory.
-DSAMURAI_SANITIZE_LEAKS=ON|OFF Also enable LeakSanitizer, which
requires SAMURAI_SANITIZE. Default
ON, except on macOS, which has no
LeakSanitizer at all: asking for it
there aborts the run before the first
test. Note that LeakSanitizer stops
the world through ptrace, so it
stalls for minutes on hosts where
/proc/sys/kernel/yama/ptrace_scope is
not 0.
For example, an optimised static-only build:
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release \
-DSAMURAI_BUILD_SHARED=OFF
Or an instrumented build for running the tests under the sanitizers:
cmake -S . -B build-asan -DCMAKE_BUILD_TYPE=Debug -DSAMURAI_SANITIZE=ON
cmake --build build-asan -j
ctest --test-dir build-asan --output-on-failure
Tests
-----
The programs under test/applications are interactive tools (netcat, wget,
dnslookup, upnplist, portmap, ...). They are built, but not run by ctest.
upnplist and portmap are the two that need a UPnP gateway on the network, which
no automated test can assume. Both take --url to skip the search and read a
description directly, which is also how they are exercised against a stub:
./build/test/upnplist
./build/test/portmap external
./build/test/portmap --lease 1200 add 8080
./build/test/portmap list
./build/test/portmap del 8080
./build/test/portmap --url http://192.168.1.1:5000/rootDesc.xml add 8080
The automated suite lives in test/exotic and is written for the "exotic" test
framework, pinned as a submodule in third_party/exotic and built as part of the
project. Each .tcc file is compiled as its own translation unit and the tests
register themselves, so nothing is generated and Perl is not needed. An
installed exotic is used instead when the submodule is absent.
ctest runs the suite as a single test named autotest. The binary can also be run
directly, which is what to do when a name is wanted rather than a pass or fail:
./build/test/autotest --help
./build/test/autotest --list-tests
./build/test/autotest 'sockets_*'
Continuous integration
----------------------
.github/workflows/ci.yml builds and runs the suite on every push to master and
every pull request:
Linux gcc and clang, Debug and Release plus an install into a temporary
prefix, which is what catches an
install rule naming something that
moved
Linux ASan/UBSan Debug, with LeakSanitizer
macOS clang Debug, and the only job that runs
the kqueue socket monitor
Windows is not covered: nothing compiles or runs this on Windows yet.
Installing
----------
cmake --install build --prefix /usr/local
Consumers can then use it from CMake:
find_package(samurai REQUIRED)
target_link_libraries(myapp PRIVATE samurai::samurai)
samurai::samurai is the shared library when one was installed and the static
library otherwise. To insist on the static library, link samurai::samurai_static.
Notes
-----
src/media (audio and video) is not part of the build and does not currently
compile.