Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

223 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

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.

About

A portable C++ library for writing networked applications, focus on network, SSL, threads, and crypto.

Resources

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages