mirror of
https://github.com/shadowsocks/shadowsocks-libev.git
synced 2026-10-28 10:44:43 +08:00
Fix findings from the security audit (#3083)
* Fix findings from the security audit Remote: - TCP: handshake/connect deadlines follow --timeout instead of a 24h minimum, so idle sockets can no longer fill the connection pool. - ACL: IPv4-mapped IPv6 addresses are matched against the IPv4 lists. - UDP server now enforces the peer ACL and the outbound block list. - ACL loading fails closed on bad files or invalid entries. - TCP half-close is relayed instead of tearing the connection down. - AEAD-2022 UDP sessions are created only after timestamp/replay checks. ss-manager: - Owner-only conf files (deleted on remove), 0600 control socket, umask 022, safe working directory, loopback-only IP addresses, per-user sender check on Linux, plugins only over the UNIX socket. - Port canonicalisation, list() overflow, add/remove waits for the server, stale pid files can't signal unrelated processes. Also: plugins drop to --user, daemonize readiness/pid file handling, resolver cancellation, replay-filter fail-closed, key wiping, spec strictness in AEAD/2022 parsing, JSON surrogate validation, ss:// plugin name checks, embedding API state reset, Android protect_socket replies. Adds tests/test_server_acl.py and tests/test_half_close.py and extends the manager, ACL, AEAD-2022 and ssurl tests. Co-Authored-By: Claude <noreply@anthropic.com> * Fix CI on Ubuntu 26.04 runners; use string_view over raw string pointers - Define _GLIBCXX_NO_ASSERTIONS: libstdc++ 15 enables assertions at -O0, which pulls __glibcxx_assert_fail (C++ runtime) into the programs. - test_redir_qemu: boot the guest with -cpu max so the runner's glibc loader runs under TCG. - find_program: std::string_view PATH splitting into a fixed buffer, no heap and no null snprintf destination (GCC -Wformat-truncation). - manager: parse /proc/net/udp numbers with strtoul (cert-err34-c); walk /proc/<pid>/cmdline with std::string_view. - ssurl: sanitise the tag through std::span. Co-Authored-By: Claude <noreply@anthropic.com> * coverage: tolerate gcov 15 lambda inconsistencies in lcov 2 Co-Authored-By: Claude <noreply@anthropic.com> * resolver: replace init's cleanup lambda with a member; lcov --list ignores inconsistent gcov 15 marks the lambda as not hit while its lines are, which lcov 2 rejects. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
1 parent
db861130d7
commit
50f1ae54f7
47 files changed
+1601
-249
No files matched your search
@@ -182,7 +182,7 @@ jobs:
|
||||
{
|
||||
echo '### Coverage (src/, unit + stress tests)'
|
||||
echo '```'
|
||||
lcov --list build/coverage.info --ignore-errors unused,empty
|
||||
lcov --list build/coverage.info --ignore-errors unused,empty,inconsistent
|
||||
echo '```'
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
|
||||
+5
-1
@@ -127,6 +127,8 @@ endif()
|
||||
|
||||
# Coverage report (requires ENABLE_COVERAGE=ON, lcov and genhtml)
|
||||
# Usage: make coverage (after running ctest / integration tests)
|
||||
# gcov 15 can record a lambda's lines as hit but the lambda itself as not;
|
||||
# lcov 2 treats that as fatal ("inconsistent"), so it is only a warning here.
|
||||
if (ENABLE_COVERAGE)
|
||||
find_program(LCOV_EXECUTABLE lcov)
|
||||
find_program(GENHTML_EXECUTABLE genhtml)
|
||||
@@ -135,9 +137,11 @@ if (ENABLE_COVERAGE)
|
||||
COMMAND ${LCOV_EXECUTABLE} --capture --directory ${PROJECT_BINARY_DIR}
|
||||
--output-file ${PROJECT_BINARY_DIR}/coverage.info
|
||||
--include ${PROJECT_SOURCE_DIR}/src/*
|
||||
--ignore-errors mismatch,unused,empty,gcov
|
||||
--ignore-errors mismatch,unused,empty,gcov,inconsistent
|
||||
COMMAND ${LCOV_EXECUTABLE} --list ${PROJECT_BINARY_DIR}/coverage.info
|
||||
--ignore-errors inconsistent
|
||||
COMMAND ${GENHTML_EXECUTABLE} ${PROJECT_BINARY_DIR}/coverage.info
|
||||
--ignore-errors inconsistent
|
||||
--output-directory ${PROJECT_BINARY_DIR}/coverage-html
|
||||
COMMAND ${CMAKE_COMMAND} -E echo "Coverage report: ${PROJECT_BINARY_DIR}/coverage-html/index.html"
|
||||
WORKING_DIRECTORY ${PROJECT_BINARY_DIR}
|
||||
|
||||
@@ -31,6 +31,10 @@ function(ss_project_options target)
|
||||
# it when they are rewritten.
|
||||
target_compile_options(${target} PRIVATE
|
||||
$<$<COMPILE_LANGUAGE:CXX>:-fno-exceptions -fno-rtti -fno-threadsafe-statics>)
|
||||
# libstdc++ 15 enables its assertions at -O0, and they call
|
||||
# __glibcxx_assert_fail from the C++ runtime, which the programs and
|
||||
# libshadowsocks-c must not need (scripts/check_cxx_runtime.py).
|
||||
target_compile_definitions(${target} PRIVATE $<$<COMPILE_LANGUAGE:CXX>:_GLIBCXX_NO_ASSERTIONS>)
|
||||
# GCC's C++ -Wall adds -Wsign-compare and -Wclass-memaccess, which C's
|
||||
# -Wall does not have. Modules converted from C keep the warning set
|
||||
# they were written against; ss_strict_warnings() turns both back on.
|
||||
|
||||
@@ -62,6 +62,26 @@ stat: {"8001":11370}
|
||||
|
||||
There is no way to reset the traffic statistics, unless you remove the port and add it again
|
||||
|
||||
\section ss_manager_security SECURITY
|
||||
|
||||
The protocol carries no credentials, so ss-manager decides who may use it by
|
||||
where commands come from:
|
||||
|
||||
- A UNIX domain socket (recommended) is created with mode 0600, owned by the
|
||||
user ss-manager runs as (or by the `-a` user its servers run as).
|
||||
- An IP manager address must be a loopback one; any other is refused. On
|
||||
Linux a datagram is obeyed only if its source socket belongs to the same
|
||||
user, root, or the `-a` user; elsewhere any local user can reach a
|
||||
loopback address, so prefer the UNIX socket there.
|
||||
- A per-server `plugin` names a program to run and is accepted only over the
|
||||
UNIX socket.
|
||||
|
||||
The working directory (`-D`) holds each server's configuration, with its
|
||||
password, in owner-only files that are deleted when the port is removed; it
|
||||
must be owned by the user running ss-manager and writable by nobody else.
|
||||
`add` answers once the new ss-server is serving (or has failed), within ten
|
||||
seconds.
|
||||
|
||||
\section ss_manager_example EXAMPLE
|
||||
|
||||
To use ss-manager(1), First start it and specify necessary information.
|
||||
|
||||
+54
-25
@@ -50,6 +50,24 @@ static struct ss_ipset outbound_block_list_ipv4;
|
||||
static struct ss_ipset outbound_block_list_ipv6;
|
||||
static rule_list_t outbound_block_list_rules;
|
||||
|
||||
/*
|
||||
* Parse an address for ACL lookups. An IPv4-mapped IPv6 address
|
||||
* (::ffff:a.b.c.d) reaches the IPv4 host on a dual-stack socket, so it is
|
||||
* matched against the IPv4 lists.
|
||||
*/
|
||||
static int
|
||||
acl_ip_init(struct ss_ip *addr, const char *text)
|
||||
{
|
||||
static const uint8_t mapped_prefix[12] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff };
|
||||
int err = ss_ip_init(addr, text);
|
||||
if (!err && addr->version == 6 && memcmp(addr->bytes, mapped_prefix, sizeof(mapped_prefix)) == 0) {
|
||||
memmove(addr->bytes, addr->bytes + 12, 4);
|
||||
addr->version = 4;
|
||||
return 1;
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
static int
|
||||
parse_addr_cidr(const char *str, char *host, size_t host_len, int *cidr)
|
||||
{
|
||||
@@ -116,14 +134,8 @@ init_acl(const char *path)
|
||||
return -1;
|
||||
}
|
||||
|
||||
// initialize ipset
|
||||
|
||||
ss_ipset_clear(&white_list_ipv4);
|
||||
ss_ipset_clear(&white_list_ipv6);
|
||||
ss_ipset_clear(&black_list_ipv4);
|
||||
ss_ipset_clear(&black_list_ipv6);
|
||||
ss_ipset_clear(&outbound_block_list_ipv4);
|
||||
ss_ipset_clear(&outbound_block_list_ipv6);
|
||||
// Start from nothing: a reload must not keep the previous lists or mode.
|
||||
free_acl();
|
||||
|
||||
|
||||
struct ss_ipset *list_ipv4 = &black_list_ipv4;
|
||||
@@ -203,16 +215,33 @@ init_acl(const char *path)
|
||||
int cidr;
|
||||
if (parse_addr_cidr(line, host, sizeof(host), &cidr) == -1) {
|
||||
LOGE("invalid ACL entry: %s", line);
|
||||
continue;
|
||||
fclose(f);
|
||||
free_acl();
|
||||
return -1;
|
||||
}
|
||||
|
||||
struct ss_ip addr;
|
||||
int err = ss_ip_init(&addr, host);
|
||||
int err = acl_ip_init(&addr, host);
|
||||
if (err == 1) {
|
||||
// ::ffff:a.b.c.d/N is the IPv4 prefix a.b.c.d/(N-96)
|
||||
if (cidr >= 0 && cidr < 96) {
|
||||
LOGE("invalid ACL IPv4-mapped CIDR: %s", line);
|
||||
fclose(f);
|
||||
free_acl();
|
||||
return -1;
|
||||
}
|
||||
if (cidr >= 96) {
|
||||
cidr -= 96;
|
||||
}
|
||||
err = 0;
|
||||
}
|
||||
if (!err) {
|
||||
if (addr.version == 4) {
|
||||
if (cidr > 32 || cidr == -2) {
|
||||
LOGE("invalid ACL IPv4 CIDR: %s", line);
|
||||
continue;
|
||||
fclose(f);
|
||||
free_acl();
|
||||
return -1;
|
||||
}
|
||||
if (cidr >= 0) {
|
||||
ss_ipset_assign(list_ipv4, addr.bytes, 32, (unsigned)cidr, true);
|
||||
@@ -222,7 +251,9 @@ init_acl(const char *path)
|
||||
} else if (addr.version == 6) {
|
||||
if (cidr > 128 || cidr == -2) {
|
||||
LOGE("invalid ACL IPv6 CIDR: %s", line);
|
||||
continue;
|
||||
fclose(f);
|
||||
free_acl();
|
||||
return -1;
|
||||
}
|
||||
if (cidr >= 0) {
|
||||
ss_ipset_assign(list_ipv6, addr.bytes, 128, (unsigned)cidr, true);
|
||||
@@ -232,21 +263,17 @@ init_acl(const char *path)
|
||||
}
|
||||
} else {
|
||||
rule_t *rule = new_rule();
|
||||
if (rule == NULL) {
|
||||
continue;
|
||||
}
|
||||
int status = accept_rule_arg(rule, line);
|
||||
int status = rule != NULL ? accept_rule_arg(rule, line) : -1;
|
||||
if (status == 1) status = init_rule(rule);
|
||||
if (status < 0) {
|
||||
if (status != 1) {
|
||||
// A rule that cannot be used must not silently drop
|
||||
// out of a block list.
|
||||
LOGE("invalid ACL rule: %s", line);
|
||||
free_rule(rule);
|
||||
fclose(f);
|
||||
free_acl();
|
||||
return -1;
|
||||
}
|
||||
if (status != 1) {
|
||||
free_rule(rule);
|
||||
continue;
|
||||
}
|
||||
add_rule(rules, rule);
|
||||
}
|
||||
}
|
||||
@@ -281,6 +308,8 @@ free_acl(void)
|
||||
free_rules(&black_list_rules);
|
||||
free_rules(&white_list_rules);
|
||||
free_rules(&outbound_block_list_rules);
|
||||
|
||||
acl_mode = BLACK_LIST;
|
||||
}
|
||||
|
||||
int
|
||||
@@ -299,7 +328,7 @@ acl_match_host(const char *host)
|
||||
{
|
||||
struct ss_ip addr;
|
||||
int ret = 0;
|
||||
int err = ss_ip_init(&addr, host);
|
||||
int err = acl_ip_init(&addr, host) < 0 ? -1 : 0;
|
||||
|
||||
if (err) {
|
||||
int host_len = strlen(host);
|
||||
@@ -329,7 +358,7 @@ int
|
||||
acl_add_ip(const char *ip)
|
||||
{
|
||||
struct ss_ip addr;
|
||||
int err = ss_ip_init(&addr, ip);
|
||||
int err = acl_ip_init(&addr, ip) < 0 ? -1 : 0;
|
||||
if (err) {
|
||||
return -1;
|
||||
}
|
||||
@@ -347,7 +376,7 @@ int
|
||||
acl_remove_ip(const char *ip)
|
||||
{
|
||||
struct ss_ip addr;
|
||||
int err = ss_ip_init(&addr, ip);
|
||||
int err = acl_ip_init(&addr, ip) < 0 ? -1 : 0;
|
||||
if (err) {
|
||||
return -1;
|
||||
}
|
||||
@@ -370,7 +399,7 @@ outbound_block_match_host(const char *host)
|
||||
{
|
||||
struct ss_ip addr;
|
||||
int ret = 0;
|
||||
int err = ss_ip_init(&addr, host);
|
||||
int err = acl_ip_init(&addr, host) < 0 ? -1 : 0;
|
||||
|
||||
if (err) {
|
||||
int host_len = strlen(host);
|
||||
|
||||
+15
-5
@@ -80,16 +80,26 @@ protect_socket(int fd)
|
||||
return -1;
|
||||
}
|
||||
|
||||
char ret = 0;
|
||||
|
||||
if (recv(sock, &ret, 1, 0) == -1) {
|
||||
ERROR("[android] recv");
|
||||
// The VPN service answers one byte: 0 once the socket is protected.
|
||||
// Anything else, or no answer, leaves the socket routed into the VPN.
|
||||
char ret = 1;
|
||||
ssize_t n = recv(sock, &ret, 1, 0);
|
||||
if (n != 1) {
|
||||
if (n == -1) {
|
||||
ERROR("[android] recv");
|
||||
} else {
|
||||
LOGE("[android] protect_path closed without an answer");
|
||||
}
|
||||
close(sock);
|
||||
return -1;
|
||||
}
|
||||
|
||||
close(sock);
|
||||
return ret;
|
||||
if (ret != 0) {
|
||||
LOGE("[android] failed to protect socket %d", fd);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
extern char *stat_path;
|
||||
|
||||
@@ -39,6 +39,8 @@ public:
|
||||
|
||||
constexpr std::size_t capacity() const noexcept { return N; }
|
||||
constexpr std::size_t size() const noexcept { return len_; }
|
||||
// All of the storage, whatever is in use, for wiping secrets.
|
||||
Bytes storage() noexcept { return { data_.data(), N }; }
|
||||
constexpr bool empty() const noexcept { return len_ == 0; }
|
||||
constexpr bool full() const noexcept { return idx_ + len_ == N; }
|
||||
|
||||
|
||||
+11
-3
@@ -91,7 +91,8 @@ private:
|
||||
|
||||
bool ppbloom_seen(void *, std::span<const std::uint8_t> salt) noexcept
|
||||
{
|
||||
return ppbloom_check(salt.data(), static_cast<int>(salt.size())) == 1;
|
||||
// An unusable filter (-1) counts as seen: replay protection fails closed.
|
||||
return ppbloom_check(salt.data(), static_cast<int>(salt.size())) != 0;
|
||||
}
|
||||
|
||||
void ppbloom_remember(void *, std::span<const std::uint8_t> salt) noexcept
|
||||
@@ -177,6 +178,7 @@ Status hkdf_sha1(std::span<const std::uint8_t> salt, std::span<const std::uint8_
|
||||
expand.update({ &counter, 1 });
|
||||
if (!expand.finish(t)) {
|
||||
sodium_memzero(prk, sizeof(prk));
|
||||
sodium_memzero(t, sizeof(t));
|
||||
return fail(Errc::crypto);
|
||||
}
|
||||
t_len = kHash;
|
||||
@@ -290,6 +292,11 @@ Status AeadTcpEncryptor::init(const MasterKey &master, ReplayFilter filter, bool
|
||||
salt_sent_ = false;
|
||||
nonce_.fill(0);
|
||||
randombytes_buf(salt_.data(), master.spec.key_len);
|
||||
if (printable_salt && master.spec.key_len < 32) {
|
||||
// Up to 12 constrained bytes leave >= 238 bits of a 32-byte salt
|
||||
// but only ~111 of a 16-byte one: not offered there.
|
||||
return fail(Errc::config);
|
||||
}
|
||||
if (printable_salt) {
|
||||
// Vary the constrained span without adding a delimiter or marker.
|
||||
std::size_t prefix = 6 + randombytes_uniform(7);
|
||||
@@ -418,8 +425,9 @@ Result<AeadTcpDecryptor::Step> AeadTcpDecryptor::decrypt(std::span<const std::ui
|
||||
failed_ = true;
|
||||
return fail(Errc::crypto);
|
||||
}
|
||||
std::size_t payload = load16_be(len) & kMaxChunk;
|
||||
if (payload == 0) {
|
||||
// The two top bits are reserved (SIP004): reject, don't mask.
|
||||
std::size_t payload = load16_be(len);
|
||||
if (payload == 0 || payload > kMaxChunk) {
|
||||
failed_ = true;
|
||||
return fail(Errc::protocol);
|
||||
}
|
||||
|
||||
@@ -68,6 +68,12 @@ struct MasterKey {
|
||||
AeadSpec spec;
|
||||
std::array<std::uint8_t, kMaxKey> bytes{};
|
||||
|
||||
MasterKey() noexcept = default;
|
||||
MasterKey(const AeadSpec &s, const std::array<std::uint8_t, kMaxKey> &b) noexcept : spec(s), bytes(b) {}
|
||||
MasterKey(const MasterKey &) noexcept = default;
|
||||
MasterKey &operator=(const MasterKey &) noexcept = default;
|
||||
~MasterKey() { sodium_memzero(bytes.data(), bytes.size()); }
|
||||
|
||||
std::span<const std::uint8_t> key() const noexcept { return { bytes.data(), spec.key_len }; }
|
||||
|
||||
// EVP_BytesToKey(MD5) from a password, as the C implementation does.
|
||||
@@ -168,6 +174,12 @@ private:
|
||||
|
||||
class AeadTcpDecryptor {
|
||||
public:
|
||||
AeadTcpDecryptor() noexcept = default;
|
||||
AeadTcpDecryptor(const AeadTcpDecryptor &) = delete;
|
||||
AeadTcpDecryptor &operator=(const AeadTcpDecryptor &) = delete;
|
||||
// The reassembly buffer held decrypted payload.
|
||||
~AeadTcpDecryptor() { sodium_memzero(buffer_.storage().data(), buffer_.storage().size()); }
|
||||
|
||||
Status init(const MasterKey &master, ReplayFilter filter) noexcept;
|
||||
|
||||
struct Step {
|
||||
|
||||
+18
-4
@@ -83,7 +83,7 @@ std::size_t socks_addr_len(std::span<const std::uint8_t> buf) noexcept
|
||||
return 0;
|
||||
}
|
||||
std::size_t len = 0;
|
||||
switch (buf[0] & 0x0F) {
|
||||
switch (buf[0]) { // exactly 1, 3 or 4, as the SOCKS parser reads it
|
||||
case 1:
|
||||
len = 1 + 4;
|
||||
break;
|
||||
@@ -222,7 +222,9 @@ bool SaltPool::check_and_add(std::span<const std::uint8_t> salt, std::uint64_t n
|
||||
return false;
|
||||
}
|
||||
if (seen_.full()) {
|
||||
(void)seen_.pop_oldest();
|
||||
// Every remembered salt is still inside the replay window: forgetting
|
||||
// one would let its request be replayed, so refuse the new one.
|
||||
return false;
|
||||
}
|
||||
return seen_.emplace(*key, now).has_value();
|
||||
}
|
||||
@@ -672,12 +674,24 @@ Result<std::size_t> udp2022_accept(Role role, UdpSession2022 &session, const Udp
|
||||
}
|
||||
std::size_t pad = load16_be(body + o);
|
||||
o += kLenSize;
|
||||
if (o + pad > n) {
|
||||
if (pad > k2022MaxPad || o + pad > n) {
|
||||
return fail(Errc::protocol);
|
||||
}
|
||||
o += pad;
|
||||
|
||||
if (!session.rx_ready || session.rx_id != packet.session_id) {
|
||||
if (session.rx_ready && session.rx_id != packet.session_id) {
|
||||
// Keep the current window as the previous one, or bring the previous
|
||||
// one back for a late packet of that session.
|
||||
bool back = session.prev_ready && session.prev_id == packet.session_id;
|
||||
std::swap(session.rx_id, session.prev_id);
|
||||
std::swap(session.rx_highest, session.prev_highest);
|
||||
std::swap(session.rx_window, session.prev_window);
|
||||
std::swap(session.rx_ready, session.prev_ready);
|
||||
if (!back) {
|
||||
session.rx_ready = false;
|
||||
}
|
||||
}
|
||||
if (!session.rx_ready) {
|
||||
window_reset(session, packet.session_id, packet.packet_id);
|
||||
} else if (!window_check(session, packet.packet_id)) {
|
||||
return fail(Errc::crypto); // replayed
|
||||
|
||||
@@ -54,6 +54,12 @@ struct Psk {
|
||||
Aead2022Spec spec;
|
||||
std::array<std::uint8_t, kMaxKey> bytes{};
|
||||
|
||||
Psk() noexcept = default;
|
||||
Psk(const Aead2022Spec &s, const std::array<std::uint8_t, kMaxKey> &b) noexcept : spec(s), bytes(b) {}
|
||||
Psk(const Psk &) noexcept = default;
|
||||
Psk &operator=(const Psk &) noexcept = default;
|
||||
~Psk() { sodium_memzero(bytes.data(), bytes.size()); }
|
||||
|
||||
std::size_t size() const noexcept { return spec.aead.key_len; }
|
||||
std::span<const std::uint8_t> key() const noexcept { return { bytes.data(), size() }; }
|
||||
bool chacha() const noexcept { return spec.aead.kind == AeadKind::chacha20_poly1305; }
|
||||
@@ -112,6 +118,12 @@ private:
|
||||
|
||||
class Aead2022TcpDecryptor {
|
||||
public:
|
||||
Aead2022TcpDecryptor() noexcept = default;
|
||||
Aead2022TcpDecryptor(const Aead2022TcpDecryptor &) = delete;
|
||||
Aead2022TcpDecryptor &operator=(const Aead2022TcpDecryptor &) = delete;
|
||||
// Chunks are decrypted in place: the buffer held plaintext.
|
||||
~Aead2022TcpDecryptor() { sodium_memzero(buffer_.storage().data(), buffer_.storage().size()); }
|
||||
|
||||
// A client decryptor checks the response echoes `request_salt`; a server
|
||||
// decryptor rejects salts already in `pool` (which may be null in tests).
|
||||
Status init(const Psk &psk, Role role, std::span<const std::uint8_t> request_salt = {},
|
||||
@@ -166,6 +178,13 @@ struct UdpSession2022 {
|
||||
std::array<std::uint8_t, kUdpWindowBits / 8> rx_window{};
|
||||
bool rx_ready = false;
|
||||
|
||||
// The peer session heard from before rx_id, so a peer that starts a new
|
||||
// session cannot have its old packets replayed into a fresh window.
|
||||
std::array<std::uint8_t, kSessionIdLen> prev_id{};
|
||||
std::uint64_t prev_highest = 0;
|
||||
std::array<std::uint8_t, kUdpWindowBits / 8> prev_window{};
|
||||
bool prev_ready = false;
|
||||
|
||||
// Server side: the client session ID replies are addressed to.
|
||||
std::array<std::uint8_t, kSessionIdLen> peer_id{};
|
||||
bool peer_known = false;
|
||||
|
||||
+6
-1
@@ -54,9 +54,14 @@ bool parse_key(const char *base64, std::span<std::uint8_t> key, bool exact) noex
|
||||
// One spare byte tells an over-long key from an exact one; base64_decode
|
||||
// never writes past the size it is given.
|
||||
std::uint8_t out[kMaxKey + 1];
|
||||
int want = static_cast<int>(key.size()) + (exact ? 1 : 0);
|
||||
int want = static_cast<int>(key.size()) + 1;
|
||||
int n = base64_decode(out, base64, want);
|
||||
bool ok = exact ? n == static_cast<int>(key.size()) : n >= static_cast<int>(key.size());
|
||||
if (ok && n > static_cast<int>(key.size())) {
|
||||
// Kept for existing configs, but two different strings must not
|
||||
// silently be the same key.
|
||||
LOGE("the key is longer than %zu bytes; only the first %zu are used", key.size(), key.size());
|
||||
}
|
||||
if (ok) {
|
||||
std::memcpy(key.data(), out, key.size());
|
||||
}
|
||||
|
||||
@@ -60,9 +60,13 @@ public:
|
||||
if (signal_count_ == signals_.size()) {
|
||||
return fail(Errc::exhausted);
|
||||
}
|
||||
SignalWatch &w = signals_[signal_count_++];
|
||||
SignalWatch &w = signals_[signal_count_];
|
||||
w.program = this;
|
||||
(void)w.signal.open(loop_, w, signum);
|
||||
Status opened = w.signal.open(loop_, w, signum);
|
||||
if (w.signal.is_open()) {
|
||||
++signal_count_; // close() releases it even if starting failed
|
||||
}
|
||||
SS_TRY(opened);
|
||||
}
|
||||
(void)usr1;
|
||||
return {};
|
||||
|
||||
+16
-13
@@ -382,13 +382,14 @@ read_jconf(const char *file)
|
||||
json_value *value = obj->u.object.values[i].value;
|
||||
if (strcmp(name, "server") == 0) {
|
||||
if (value->type == json_array) {
|
||||
for (j = 0; j < value->u.array.length; j++) {
|
||||
if (j >= MAX_REMOTE_NUM) {
|
||||
break;
|
||||
conf.remote_num = 0;
|
||||
for (j = 0; j < value->u.array.length && conf.remote_num < MAX_REMOTE_NUM; j++) {
|
||||
char *host = to_string(value->u.array.values[j]);
|
||||
if (host == NULL) {
|
||||
continue; // a null entry is not an address
|
||||
}
|
||||
json_value *v = value->u.array.values[j];
|
||||
parse_conf_addr(to_string(v), conf.remote_addr + j);
|
||||
conf.remote_num = j + 1;
|
||||
parse_conf_addr(host, conf.remote_addr + conf.remote_num);
|
||||
conf.remote_num++;
|
||||
}
|
||||
} else if (value->type == json_string) {
|
||||
parse_conf_addr(to_string(value), conf.remote_addr);
|
||||
@@ -396,16 +397,18 @@ read_jconf(const char *file)
|
||||
}
|
||||
} else if (strcmp(name, "port_password") == 0) {
|
||||
if (value->type == json_object) {
|
||||
for (j = 0; j < value->u.object.length; j++) {
|
||||
if (j >= MAX_PORT_NUM) {
|
||||
break;
|
||||
}
|
||||
// Entries are packed: a skipped one leaves no hole.
|
||||
conf.port_password_num = 0;
|
||||
for (j = 0; j < value->u.object.length && conf.port_password_num < MAX_PORT_NUM; j++) {
|
||||
json_value *v = value->u.object.values[j].value;
|
||||
if (v->type == json_string) {
|
||||
conf.port_password[j].port = conf_strndup(
|
||||
int k = conf.port_password_num++;
|
||||
conf.port_password[k].port = conf_strndup(
|
||||
std::string_view(value->u.object.values[j].name, value->u.object.values[j].name_length));
|
||||
conf.port_password[j].password = to_string(v);
|
||||
conf.port_password_num = j + 1;
|
||||
conf.port_password[k].password = to_string(v);
|
||||
} else {
|
||||
LOGE("ignoring the port_password entry for %s: the password must be a string",
|
||||
value->u.object.values[j].name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+20
-3
@@ -327,7 +327,19 @@ json_value * json_parse_ex (json_settings * settings,
|
||||
uc_b2 = (uc_b3 << 4) | uc_b4;
|
||||
uchar = (uc_b1 << 8) | uc_b2;
|
||||
|
||||
if ((uchar & 0xF800) == 0xD800) {
|
||||
if (uchar == 0) {
|
||||
// Every string ends up as a C string: a NUL would
|
||||
// silently cut it short.
|
||||
sprintf (error, "Invalid escape \\u0000 (at %d:%d)", line_and_col);
|
||||
goto e_failed;
|
||||
}
|
||||
|
||||
if ((uchar & 0xFC00) == 0xDC00) {
|
||||
sprintf (error, "Unpaired low surrogate (at %d:%d)", line_and_col);
|
||||
goto e_failed;
|
||||
}
|
||||
|
||||
if ((uchar & 0xFC00) == 0xD800) {
|
||||
json_uchar uchar2;
|
||||
|
||||
if (end - state.ptr <= 6 || (*++ state.ptr) != '\\' || (*++ state.ptr) != 'u' ||
|
||||
@@ -343,8 +355,13 @@ json_value * json_parse_ex (json_settings * settings,
|
||||
uc_b1 = (uc_b1 << 4) | uc_b2;
|
||||
uc_b2 = (uc_b3 << 4) | uc_b4;
|
||||
uchar2 = (uc_b1 << 8) | uc_b2;
|
||||
|
||||
uchar = 0x010000 | ((uchar & 0x3FF) << 10) | (uchar2 & 0x3FF);
|
||||
|
||||
if ((uchar2 & 0xFC00) != 0xDC00) {
|
||||
sprintf (error, "Invalid surrogate pair (at %d:%d)", line_and_col);
|
||||
goto e_failed;
|
||||
}
|
||||
|
||||
uchar = 0x10000 + ((uchar & 0x3FF) << 10) + (uchar2 & 0x3FF);
|
||||
}
|
||||
|
||||
if (sizeof (json_char) >= sizeof (json_uchar) || (uchar <= 0x7F))
|
||||
|
||||
+77
-23
@@ -301,11 +301,11 @@ struct Relays {
|
||||
}
|
||||
resolving = true;
|
||||
}
|
||||
if (!tcp.init(*loop, method, config, resolving ? &resolver : NULL) || !tcp.listen(listenfd)) {
|
||||
if (!tcp.init(*loop, method, config, resolving ? &resolver : NULL)) {
|
||||
return false;
|
||||
}
|
||||
tcp_on = true;
|
||||
return true;
|
||||
tcp_on = true; // stop() shuts it down, closing listenfd once it is taken
|
||||
return (bool)tcp.listen(listenfd);
|
||||
}
|
||||
|
||||
// Returns the UDP socket, or -1.
|
||||
@@ -674,7 +674,11 @@ Same behavior as `-S`; see that option for details. Short alias: `-S`.
|
||||
break;
|
||||
case GETOPT_VAL_ACL:
|
||||
LOGI("initializing acl...");
|
||||
acl = !init_acl(optarg);
|
||||
if (init_acl(optarg) != 0) {
|
||||
LOGE("failed to load the ACL file %s", optarg);
|
||||
FATAL("invalid ACL");
|
||||
}
|
||||
acl = 1;
|
||||
break;
|
||||
case GETOPT_VAL_MTU:
|
||||
if (ss_parse_int(optarg, 0, INT_MAX, &mtu) == -1) {
|
||||
@@ -968,7 +972,11 @@ Same behavior as `-S`; see that option for details. Short alias: `-S`.
|
||||
}
|
||||
if (acl == 0 && conf->acl != NULL) {
|
||||
LOGI("initializing acl...");
|
||||
acl = !init_acl(conf->acl);
|
||||
if (init_acl(conf->acl) != 0) {
|
||||
LOGE("failed to load the ACL file %s", conf->acl);
|
||||
FATAL("invalid ACL");
|
||||
}
|
||||
acl = 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1163,6 +1171,7 @@ Same behavior as `-S`; see that option for details. Short alias: `-S`.
|
||||
snprintf(remote_str + len, buf_size - len, "|%s", remote_addr[i].host);
|
||||
len = strlen(remote_str);
|
||||
}
|
||||
set_plugin_user(user); // it drops root along with us
|
||||
int err = start_plugin(plugin, plugin_opts, remote_str,
|
||||
remote_port, plugin_host, plugin_port,
|
||||
#ifdef __MINGW32__
|
||||
@@ -1271,6 +1280,7 @@ Same behavior as `-S`; see that option for details. Short alias: `-S`.
|
||||
}
|
||||
#endif
|
||||
|
||||
daemon_ready();
|
||||
// Enter the loop
|
||||
ss::heap_seal();
|
||||
{
|
||||
@@ -1306,13 +1316,14 @@ Same behavior as `-S`; see that option for details. Short alias: `-S`.
|
||||
|
||||
// ------------------------------------------------------------ library API
|
||||
|
||||
int _start_ss_local_server(profile_t profile, ss_local_callback callback, void *udata);
|
||||
int _start_ss_local_server(profile_t profile, ss_local_callback callback, void *udata, bool embedded);
|
||||
|
||||
struct ss_local_handle {
|
||||
profile_t profile;
|
||||
ss_local_callback callback;
|
||||
void *udata;
|
||||
uv_thread_t thread;
|
||||
uv_mutex_t join_lock; // serialises ss_local_wait() callers
|
||||
int result;
|
||||
bool waited;
|
||||
bool stop_requested; // guarded by control_lock
|
||||
@@ -1348,7 +1359,7 @@ void free_profile(profile_t *p)
|
||||
void server_thread(void *arg)
|
||||
{
|
||||
ss_local_handle *h = (ss_local_handle *)arg;
|
||||
h->result = _start_ss_local_server(h->profile, h->callback, h->udata);
|
||||
h->result = _start_ss_local_server(h->profile, h->callback, h->udata, true);
|
||||
uv_mutex_lock(&control_lock);
|
||||
running_handle = NULL;
|
||||
server_busy = false;
|
||||
@@ -1384,6 +1395,12 @@ ss_local_start(const profile_t *profile, ss_local_callback callback, void *udata
|
||||
h->profile.log = copy_string(profile->log);
|
||||
h->callback = callback;
|
||||
h->udata = udata;
|
||||
if (uv_mutex_init(&h->join_lock) != 0) {
|
||||
uv_mutex_unlock(&control_lock);
|
||||
free_profile(&h->profile);
|
||||
free(h); // heap-ok: library API, startup
|
||||
return ENOMEM;
|
||||
}
|
||||
server_busy = true;
|
||||
running_handle = h;
|
||||
uv_mutex_unlock(&control_lock);
|
||||
@@ -1392,6 +1409,7 @@ ss_local_start(const profile_t *profile, ss_local_callback callback, void *udata
|
||||
server_busy = false;
|
||||
running_handle = NULL;
|
||||
uv_mutex_unlock(&control_lock);
|
||||
uv_mutex_destroy(&h->join_lock);
|
||||
free_profile(&h->profile);
|
||||
free(h); // heap-ok: library API, startup
|
||||
return ENOMEM;
|
||||
@@ -1421,11 +1439,14 @@ ss_local_wait(ss_local_handle *handle)
|
||||
if (handle == NULL) {
|
||||
return EINVAL;
|
||||
}
|
||||
uv_mutex_lock(&handle->join_lock);
|
||||
if (!handle->waited) {
|
||||
uv_thread_join(&handle->thread);
|
||||
handle->waited = true;
|
||||
}
|
||||
return handle->result;
|
||||
int result = handle->result;
|
||||
uv_mutex_unlock(&handle->join_lock);
|
||||
return result;
|
||||
}
|
||||
|
||||
extern "C" void
|
||||
@@ -1436,12 +1457,13 @@ ss_local_destroy(ss_local_handle *handle)
|
||||
}
|
||||
ss_local_stop(handle);
|
||||
(void)ss_local_wait(handle);
|
||||
uv_mutex_destroy(&handle->join_lock);
|
||||
free_profile(&handle->profile);
|
||||
free(handle); // heap-ok: library API, startup
|
||||
}
|
||||
|
||||
int
|
||||
_start_ss_local_server(profile_t profile, ss_local_callback callback, void *udata)
|
||||
_start_ss_local_server(profile_t profile, ss_local_callback callback, void *udata, bool embedded)
|
||||
{
|
||||
char *remote_host = profile.remote_host;
|
||||
char *local_addr = profile.local_addr;
|
||||
@@ -1471,9 +1493,18 @@ _start_ss_local_server(profile_t profile, ss_local_callback callback, void *udat
|
||||
|
||||
USE_LOGFILE(log);
|
||||
|
||||
// Nothing carries over from an earlier run in this process.
|
||||
ret_val = 0;
|
||||
acl = 0;
|
||||
udp_fd = -1;
|
||||
free_acl();
|
||||
if (profile.acl != NULL) {
|
||||
LOGI("initializing acl...");
|
||||
acl = !init_acl(profile.acl);
|
||||
if (init_acl(profile.acl) != 0) {
|
||||
LOGE("failed to load the ACL file %s", profile.acl);
|
||||
return -1;
|
||||
}
|
||||
acl = 1;
|
||||
}
|
||||
|
||||
if (local_addr == NULL) {
|
||||
@@ -1483,22 +1514,43 @@ _start_ss_local_server(profile_t profile, ss_local_callback callback, void *udat
|
||||
#ifndef __MINGW32__
|
||||
// ignore SIGPIPE
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
signal(SIGABRT, SIG_IGN);
|
||||
if (!embedded) {
|
||||
signal(SIGABRT, SIG_IGN);
|
||||
}
|
||||
#endif
|
||||
|
||||
// The ss_local_start() API runs in a host process whose signals are its
|
||||
// own: it stops through ss_local_stop() instead.
|
||||
ss::event::Program program;
|
||||
if (!program.init() || !program.handle_signals(true) || !program.allow_remote_stop())
|
||||
FATAL("failed to set up the event loop");
|
||||
Relays relays;
|
||||
auto abort_start = [&](int listenfd) {
|
||||
if (listenfd >= 0 && relays.tcp.listeners() == 0) {
|
||||
ss_socket_close(listenfd);
|
||||
}
|
||||
relays.stop();
|
||||
program.close();
|
||||
free_acl();
|
||||
acl = 0;
|
||||
#ifdef __MINGW32__
|
||||
winsock_cleanup();
|
||||
#endif
|
||||
return -1;
|
||||
};
|
||||
if (!program.init() || (!embedded && !program.handle_signals(true)) || !program.allow_remote_stop()) {
|
||||
LOGE("failed to set up the event loop");
|
||||
return abort_start(-1);
|
||||
}
|
||||
|
||||
// Setup keys
|
||||
Relays relays;
|
||||
if (!relays.init(program.loop(), method, password, NULL))
|
||||
FATAL("failed to init ciphers");
|
||||
if (!relays.init(program.loop(), method, password, NULL)) {
|
||||
LOGE("failed to init ciphers");
|
||||
return abort_start(-1);
|
||||
}
|
||||
|
||||
struct sockaddr_storage storage;
|
||||
memset(&storage, 0, sizeof(struct sockaddr_storage));
|
||||
if (get_sockaddr(remote_host, remote_port_str, &storage, 1, ipv6first) == -1) {
|
||||
return -1;
|
||||
return abort_start(-1);
|
||||
}
|
||||
|
||||
// Setup proxy context
|
||||
@@ -1517,15 +1569,15 @@ _start_ss_local_server(profile_t profile, ss_local_callback callback, void *udat
|
||||
listenfd = create_and_bind(local_addr, local_port_str);
|
||||
if (listenfd == -1) {
|
||||
ERROR("bind()");
|
||||
return -1;
|
||||
return abort_start(-1);
|
||||
}
|
||||
if (listen(listenfd, SOMAXCONN) == -1) {
|
||||
ERROR("listen()");
|
||||
return -1;
|
||||
return abort_start(listenfd);
|
||||
}
|
||||
if (!relays.start_tcp(tcp_config, listenfd)) {
|
||||
LOGE("failed to set up the TCP relay");
|
||||
return -1;
|
||||
return abort_start(listenfd);
|
||||
}
|
||||
listen_fd = listenfd;
|
||||
}
|
||||
@@ -1535,7 +1587,7 @@ _start_ss_local_server(profile_t profile, ss_local_callback callback, void *udat
|
||||
LOGI("udprelay enabled");
|
||||
udp_fd = relays.start_udp(local_addr, local_port_str, &storage, mtu, timeout, NULL);
|
||||
if (udp_fd == -1) {
|
||||
return -1;
|
||||
return abort_start(-1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1568,6 +1620,8 @@ _start_ss_local_server(profile_t profile, ss_local_callback callback, void *udat
|
||||
// Clean up
|
||||
relays.stop();
|
||||
program.close();
|
||||
free_acl();
|
||||
acl = 0;
|
||||
|
||||
#ifdef __MINGW32__
|
||||
winsock_cleanup();
|
||||
@@ -1579,13 +1633,13 @@ _start_ss_local_server(profile_t profile, ss_local_callback callback, void *udat
|
||||
int
|
||||
start_ss_local_server(profile_t profile)
|
||||
{
|
||||
return _start_ss_local_server(profile, NULL, NULL);
|
||||
return _start_ss_local_server(profile, NULL, NULL, false);
|
||||
}
|
||||
|
||||
int
|
||||
start_ss_local_server_with_callback(profile_t profile, ss_local_callback callback, void *udata)
|
||||
{
|
||||
return _start_ss_local_server(profile, callback, udata);
|
||||
return _start_ss_local_server(profile, callback, udata, false);
|
||||
}
|
||||
|
||||
#endif
|
||||
+301
-18
@@ -44,6 +44,7 @@
|
||||
#include <netdb.h>
|
||||
#include <errno.h>
|
||||
#include <new>
|
||||
#include <string_view>
|
||||
#include <arpa/inet.h>
|
||||
#include <netdb.h>
|
||||
#include <netinet/in.h>
|
||||
@@ -67,6 +68,8 @@
|
||||
|
||||
#include "event/program.hpp"
|
||||
|
||||
#define SERVER_START_TIMEOUT_MS 10000
|
||||
|
||||
#ifndef BUF_SIZE
|
||||
#define BUF_SIZE 65535
|
||||
#endif
|
||||
@@ -88,6 +91,106 @@ find_server(const char *port)
|
||||
return server_table.find_if([port](struct server &server) { return strcmp(server.port, port) == 0; });
|
||||
}
|
||||
|
||||
// How the control channel is reached, set once at startup.
|
||||
static bool control_is_unix = false;
|
||||
static uid_t server_uid = (uid_t)-1; // the -a user managed servers run as
|
||||
|
||||
#ifdef __linux__
|
||||
/*
|
||||
* The uid owning the UDP socket a datagram came from, found in
|
||||
* /proc/net/udp{,6} like identd would. Only meaningful for loopback
|
||||
* senders, which is all the control channel accepts.
|
||||
*/
|
||||
static bool
|
||||
udp_sender_uid(const struct sockaddr_storage *from, uid_t *uid)
|
||||
{
|
||||
bool v6 = from->ss_family == AF_INET6;
|
||||
unsigned want_port = ntohs(v6 ? ((const struct sockaddr_in6 *)from)->sin6_port
|
||||
: ((const struct sockaddr_in *)from)->sin_port);
|
||||
FILE *f = fopen(v6 ? "/proc/net/udp6" : "/proc/net/udp", "r");
|
||||
if (f == NULL) {
|
||||
return false;
|
||||
}
|
||||
char line[512];
|
||||
bool found = false;
|
||||
if (fgets(line, sizeof(line), f) != NULL) { // header
|
||||
while (!found && fgets(line, sizeof(line), f) != NULL) {
|
||||
char local[64];
|
||||
char port_hex[9];
|
||||
char owner_dec[21];
|
||||
// sl local_address rem_address st tx:rx tr:when retrnsmt uid
|
||||
if (sscanf(line, " %*s %63[0-9A-Fa-f]:%8[0-9A-Fa-f] %*s %*s %*s %*s %*s %20[0-9]",
|
||||
local, port_hex, owner_dec) != 3
|
||||
|| strtoul(port_hex, NULL, 16) != want_port) {
|
||||
continue;
|
||||
}
|
||||
unsigned long owner = strtoul(owner_dec, NULL, 10);
|
||||
// The kernel prints each 32-bit word of the address as a host
|
||||
// order integer; zeros mean the socket is unbound (wildcard).
|
||||
uint8_t bytes[16];
|
||||
size_t words = v6 ? 4 : 1;
|
||||
if (strlen(local) != words * 8) {
|
||||
continue;
|
||||
}
|
||||
bool wildcard = true;
|
||||
for (size_t w = 0; w < words; w++) {
|
||||
char hex[9];
|
||||
memcpy(hex, local + w * 8, 8);
|
||||
hex[8] = '\0';
|
||||
uint32_t value = (uint32_t)strtoul(hex, NULL, 16);
|
||||
memcpy(bytes + w * 4, &value, 4);
|
||||
wildcard = wildcard && value == 0;
|
||||
}
|
||||
const void *addr = v6 ? (const void *)&((const struct sockaddr_in6 *)from)->sin6_addr
|
||||
: (const void *)&((const struct sockaddr_in *)from)->sin_addr;
|
||||
if (wildcard || memcmp(bytes, addr, words * 4) == 0) {
|
||||
*uid = (uid_t)owner;
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
fclose(f);
|
||||
return found;
|
||||
}
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Whether a datagram on the control channel may be obeyed. A unix socket
|
||||
* is guarded by its file mode; a (loopback) UDP sender must, where the
|
||||
* system can tell, be this user, root or the user servers run as.
|
||||
*/
|
||||
static bool
|
||||
control_sender_allowed(const struct sockaddr_storage *from)
|
||||
{
|
||||
if (control_is_unix) {
|
||||
return true;
|
||||
}
|
||||
#ifdef __linux__
|
||||
uid_t uid;
|
||||
if (!udp_sender_uid(from, &uid)) {
|
||||
return false;
|
||||
}
|
||||
return uid == geteuid() || uid == 0 || (server_uid != (uid_t)-1 && uid == server_uid);
|
||||
#else
|
||||
(void)from;
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
static bool
|
||||
is_loopback_host(const char *host)
|
||||
{
|
||||
struct ss_ip ip;
|
||||
if (ss_ip_init(&ip, host) != 0) {
|
||||
return strcmp(host, "localhost") == 0;
|
||||
}
|
||||
if (ip.version == 4) {
|
||||
return ip.bytes[0] == 127;
|
||||
}
|
||||
static const uint8_t v6_loopback[16] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 };
|
||||
return memcmp(ip.bytes, v6_loopback, sizeof(v6_loopback)) == 0;
|
||||
}
|
||||
|
||||
static int
|
||||
copy_port(char *dst, size_t dst_len, const char *src, size_t src_len)
|
||||
{
|
||||
@@ -109,6 +212,9 @@ copy_port(char *dst, size_t dst_len, const char *src, size_t src_len)
|
||||
dst[0] = '\0';
|
||||
return -1;
|
||||
}
|
||||
// Canonical form ("0080" is "80"): the table, the config file and the
|
||||
// pid file ss-server writes must all use the same name.
|
||||
snprintf(dst, dst_len, "%u", (unsigned)port);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -327,7 +433,23 @@ build_config(char *prefix, struct manager_ctx *manager, struct server *server)
|
||||
|
||||
path = (char *)ss_malloc(path_size);
|
||||
snprintf(path, path_size, "%s/.shadowsocks_%s.conf", prefix, server->port);
|
||||
FILE *f = fopen(path, "w+");
|
||||
// It holds the password: owner-only, and never written through a link.
|
||||
int flags = O_WRONLY | O_CREAT | O_TRUNC;
|
||||
#ifdef O_NOFOLLOW
|
||||
flags |= O_NOFOLLOW;
|
||||
#endif
|
||||
#ifdef O_CLOEXEC
|
||||
flags |= O_CLOEXEC;
|
||||
#endif
|
||||
int fd = open(path, flags, 0600);
|
||||
FILE *f = NULL;
|
||||
if (fd != -1) {
|
||||
(void)fchmod(fd, 0600); // an older file may have been more open
|
||||
f = fdopen(fd, "w");
|
||||
if (f == NULL) {
|
||||
close(fd);
|
||||
}
|
||||
}
|
||||
if (f == NULL) {
|
||||
if (verbose) {
|
||||
LOGE("unable to open config file");
|
||||
@@ -386,6 +508,8 @@ add_server_arg(char **argv, int *argc, int max_argc, char *arg)
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void kill_pid_from_file(FILE *f);
|
||||
|
||||
static int
|
||||
start_server_process(struct manager_ctx *manager, struct server *server)
|
||||
{
|
||||
@@ -538,8 +662,29 @@ start_server_process(struct manager_ctx *manager, struct server *server)
|
||||
_exit(127);
|
||||
}
|
||||
|
||||
while (waitpid(pid, &status, 0) == -1) {
|
||||
if (errno != EINTR) {
|
||||
// ss-server -f returns once it is serving (or has failed). Wait for
|
||||
// that, but not forever: a server stuck starting must not stall the
|
||||
// control channel.
|
||||
{
|
||||
int waited = 0;
|
||||
pid_t done;
|
||||
while ((done = waitpid(pid, &status, WNOHANG)) == 0 && waited < SERVER_START_TIMEOUT_MS) {
|
||||
struct timespec delay = { 0, 10 * 1000 * 1000 };
|
||||
nanosleep(&delay, NULL);
|
||||
waited += 10;
|
||||
}
|
||||
if (done == 0) {
|
||||
LOGE("server on port %d did not start within %d ms", port, SERVER_START_TIMEOUT_MS);
|
||||
kill(pid, SIGKILL);
|
||||
while (waitpid(pid, &status, 0) == -1 && errno == EINTR) {}
|
||||
FILE *f = fopen(pid_path, "r"); // the daemon it may have forked already
|
||||
if (f != NULL) {
|
||||
kill_pid_from_file(f);
|
||||
fclose(f);
|
||||
}
|
||||
goto ERROR;
|
||||
}
|
||||
if (done == -1) {
|
||||
ERROR("waitpid");
|
||||
goto ERROR;
|
||||
}
|
||||
@@ -575,7 +720,9 @@ get_data(char *buf, int len)
|
||||
if (pos == len) {
|
||||
return NULL;
|
||||
}
|
||||
data = buf + pos - 1;
|
||||
// The JSON object itself (one byte earlier was the ':' that
|
||||
// get_action() has already cut, or buf[-1] for a bare "{").
|
||||
data = buf + pos;
|
||||
|
||||
return data;
|
||||
}
|
||||
@@ -666,6 +813,13 @@ get_server(char *buf, int len)
|
||||
strncpy(server->no_delay, (value->u.boolean ? "true" : "false"), 8);
|
||||
}
|
||||
} else if (strcmp(name, "plugin") == 0) {
|
||||
if (!control_is_unix) {
|
||||
// It names a program to run: only over the unix socket,
|
||||
// whose file mode says who may send.
|
||||
LOGE("a per-server plugin is accepted only over a unix socket manager address");
|
||||
valid = 0;
|
||||
break;
|
||||
}
|
||||
if (value->type == json_string) {
|
||||
server->plugin = strdup(value->u.string.ptr);
|
||||
}
|
||||
@@ -678,7 +832,7 @@ get_server(char *buf, int len)
|
||||
server->mode = strdup(value->u.string.ptr);
|
||||
}
|
||||
} else {
|
||||
LOGE("invalid data: %s", data);
|
||||
LOGE("invalid data: unknown field \"%s\"", name);
|
||||
valid = 0;
|
||||
break;
|
||||
}
|
||||
@@ -839,6 +993,7 @@ check_port(struct manager_ctx *manager, struct server *server)
|
||||
udp_ret = create_and_bind(manager->hosts[i], server->port, IPPROTO_UDP);
|
||||
}
|
||||
|
||||
// create_and_bind() has already closed the probe socket.
|
||||
if (tcp_udp_ret == -1 || udp_ret == -1) {
|
||||
bind_err = -1;
|
||||
break;
|
||||
@@ -871,6 +1026,66 @@ add_server(struct manager_ctx *manager, struct server *server)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* A stale pid file may name a pid that has been reused since: signal only
|
||||
* a process that is one of our ss-server instances, where the system can
|
||||
* tell (each one is started with --manager-address).
|
||||
*/
|
||||
static bool
|
||||
is_managed_server(int pid)
|
||||
{
|
||||
#ifdef __linux__
|
||||
char path[64];
|
||||
snprintf(path, sizeof(path), "/proc/%d/cmdline", pid);
|
||||
FILE *f = fopen(path, "r");
|
||||
if (f == NULL) {
|
||||
return errno != ENOENT; // gone: nothing to signal; unreadable: assume ours
|
||||
}
|
||||
char cmdline[4096];
|
||||
size_t n = fread(cmdline, 1, sizeof(cmdline), f);
|
||||
fclose(f);
|
||||
// NUL-separated arguments; the last one may be cut short.
|
||||
std::string_view rest(cmdline, n);
|
||||
while (!rest.empty()) {
|
||||
std::string_view::size_type nul = rest.find('\0');
|
||||
std::string_view arg(rest.data(), nul != std::string_view::npos ? nul : rest.size());
|
||||
if (arg == "--manager-address") {
|
||||
return true;
|
||||
}
|
||||
rest.remove_prefix(nul != std::string_view::npos ? nul + 1 : rest.size());
|
||||
}
|
||||
return false;
|
||||
#else
|
||||
(void)pid;
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
/*
|
||||
* SIGTERM, then wait (briefly) for the server to go, so its port is free
|
||||
* for a server added in its place; SIGKILL if it lingers.
|
||||
*/
|
||||
static void
|
||||
stop_pid(int pid)
|
||||
{
|
||||
if (!is_managed_server(pid)) {
|
||||
LOGE("pid %d is not a managed ss-server; not signaling it", pid);
|
||||
return;
|
||||
}
|
||||
if (kill(pid, SIGTERM) != 0) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < 200; i++) {
|
||||
if (kill(pid, 0) != 0 && errno == ESRCH) {
|
||||
return;
|
||||
}
|
||||
struct timespec delay = { 0, 10 * 1000 * 1000 };
|
||||
nanosleep(&delay, NULL);
|
||||
}
|
||||
LOGE("server %d ignored SIGTERM; killing it", pid);
|
||||
kill(pid, SIGKILL);
|
||||
}
|
||||
|
||||
static void
|
||||
kill_pid_from_file(FILE *f)
|
||||
{
|
||||
@@ -889,7 +1104,7 @@ kill_pid_from_file(FILE *f)
|
||||
}
|
||||
// Reject malformed pid file content instead of signaling a garbage pid
|
||||
if (ss_parse_int(buf, 1, INT_MAX, &pid) == 0) {
|
||||
kill(pid, SIGTERM);
|
||||
stop_pid(pid);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -931,6 +1146,10 @@ stop_server(char *prefix, char *port)
|
||||
}
|
||||
kill_pid_from_file(f);
|
||||
fclose(f);
|
||||
remove(path);
|
||||
// The config holds the password of a server that no longer runs.
|
||||
snprintf(path, path_size, "%s/.shadowsocks_%s.conf", prefix, port);
|
||||
remove(path);
|
||||
ss_free(path);
|
||||
}
|
||||
|
||||
@@ -967,17 +1186,27 @@ manager_recv_cb(void *ctx) noexcept
|
||||
struct manager_ctx *manager = (struct manager_ctx *)ctx;
|
||||
socklen_t len;
|
||||
ssize_t r;
|
||||
struct sockaddr_un claddr;
|
||||
// Big enough for a unix, IPv4 or IPv6 sender.
|
||||
union {
|
||||
struct sockaddr_un un;
|
||||
struct sockaddr_storage storage;
|
||||
} claddr_buf;
|
||||
memset(&claddr_buf, 0, sizeof(claddr_buf));
|
||||
struct sockaddr_un &claddr = claddr_buf.un;
|
||||
char buf[BUF_SIZE];
|
||||
|
||||
memset(buf, 0, BUF_SIZE);
|
||||
|
||||
len = sizeof(struct sockaddr_un);
|
||||
len = sizeof(claddr_buf);
|
||||
r = recvfrom(manager->fd, buf, BUF_SIZE, 0, (struct sockaddr *)&claddr, &len);
|
||||
if (r == -1) {
|
||||
ERROR("manager_recvfrom");
|
||||
return;
|
||||
}
|
||||
if (!control_sender_allowed((const struct sockaddr_storage *)&claddr)) {
|
||||
LOGE("ignoring a control datagram from another user");
|
||||
return;
|
||||
}
|
||||
|
||||
if (r > BUF_SIZE / 2) {
|
||||
LOGE("too large request: %d", (int)r);
|
||||
@@ -996,7 +1225,7 @@ manager_recv_cb(void *ctx) noexcept
|
||||
struct server *server = get_server(buf, r);
|
||||
|
||||
if (server == NULL || server->port[0] == 0 || server->password[0] == 0) {
|
||||
LOGE("invalid command: %s:%s", buf, get_data(buf, r));
|
||||
LOGE("invalid command: %s", buf);
|
||||
if (server != NULL) {
|
||||
free_server(server);
|
||||
}
|
||||
@@ -1033,17 +1262,25 @@ manager_recv_cb(void *ctx) noexcept
|
||||
size_t entry_pos = 0;
|
||||
size_t pos = strlen(buf);
|
||||
|
||||
if (append_list_entry(entry_buf, sizeof(entry_buf), &entry_pos, server, method) == -1) {
|
||||
// Leaves room for "[" and "\n]\0" around it in buf.
|
||||
if (append_list_entry(entry_buf, sizeof(entry_buf) - 4, &entry_pos, server, method) == -1) {
|
||||
LOGE("list entry is too large");
|
||||
continue;
|
||||
}
|
||||
if (pos >= BUF_SIZE - entry_pos) {
|
||||
if (sendto(manager->fd, buf, pos, 0, (struct sockaddr *)&claddr, len)
|
||||
!= (ssize_t)pos) {
|
||||
ERROR("list_sendto");
|
||||
// Room for the entry and the closing "\n]" with its NUL; a full
|
||||
// buffer goes out as a complete array of its own.
|
||||
if (pos + entry_pos + 3 > BUF_SIZE) {
|
||||
if (pos > 1) {
|
||||
strcpy(buf + pos - 1, "\n]"); // replaces the trailing ","
|
||||
pos = strlen(buf);
|
||||
if (sendto(manager->fd, buf, pos, 0, (struct sockaddr *)&claddr, len)
|
||||
!= (ssize_t)pos) {
|
||||
ERROR("list_sendto");
|
||||
}
|
||||
}
|
||||
memset(buf, 0, BUF_SIZE);
|
||||
pos = 0;
|
||||
strcpy(buf, "[");
|
||||
pos = 1;
|
||||
}
|
||||
memcpy(buf + pos, entry_buf, entry_pos + 1);
|
||||
}
|
||||
@@ -1059,7 +1296,7 @@ manager_recv_cb(void *ctx) noexcept
|
||||
struct server *server = get_server(buf, r);
|
||||
|
||||
if (server == NULL || server->port[0] == 0) {
|
||||
LOGE("invalid command: %s:%s", buf, get_data(buf, r));
|
||||
LOGE("invalid command: %s", buf);
|
||||
if (server != NULL) {
|
||||
free_server(server);
|
||||
}
|
||||
@@ -1078,7 +1315,7 @@ manager_recv_cb(void *ctx) noexcept
|
||||
uint64_t traffic = 0;
|
||||
|
||||
if (parse_traffic(buf, r, port, &traffic) == -1) {
|
||||
LOGE("invalid command: %s:%s", buf, get_data(buf, r));
|
||||
LOGE("invalid command: %s", buf);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1688,12 +1925,23 @@ Same behavior as `-d`; see that option for details. Short alias: `-d`.
|
||||
}
|
||||
LOGI("working directory points to %s", working_dir);
|
||||
|
||||
int err = mkdir(working_dir, S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH);
|
||||
// It holds every server's config (with its password) and pid file.
|
||||
int err = mkdir(working_dir, S_IRWXU);
|
||||
if (err != 0 && errno != EEXIST) {
|
||||
ERROR("mkdir");
|
||||
ss_free(working_dir);
|
||||
FATAL("unable to create working directory");
|
||||
}
|
||||
// An existing directory must be ours alone: in a shared place such as
|
||||
// /tmp, someone else could have made it to plant links and pid files.
|
||||
struct stat st;
|
||||
if (stat(working_dir, &st) != 0 || !S_ISDIR(st.st_mode)
|
||||
|| (st.st_uid != geteuid() && st.st_uid != 0) || (st.st_mode & (S_IWGRP | S_IWOTH)) != 0) {
|
||||
LOGE("working directory %s must be a directory owned by this user and writable by it alone",
|
||||
working_dir);
|
||||
ss_free(working_dir);
|
||||
FATAL("unsafe working directory");
|
||||
}
|
||||
|
||||
if (manager_address == NULL) {
|
||||
size_t manager_address_size = strlen(workdir) + 20;
|
||||
@@ -1725,6 +1973,18 @@ Same behavior as `-d`; see that option for details. Short alias: `-d`.
|
||||
manager.iface = iface;
|
||||
manager.acl = acl;
|
||||
manager.user = user;
|
||||
if (user != NULL) {
|
||||
// Servers started with -a report from this uid.
|
||||
char *end = NULL;
|
||||
long numeric = strtol(user, &end, 10);
|
||||
struct passwd *pw = (*user != '\0' && *end == '\0' && numeric >= 0) ? getpwuid((uid_t)numeric)
|
||||
: getpwnam(user);
|
||||
if (pw == NULL) {
|
||||
LOGE("unknown user %s", user);
|
||||
FATAL("invalid user");
|
||||
}
|
||||
server_uid = pw->pw_uid;
|
||||
}
|
||||
manager.manager_address = manager_address;
|
||||
manager.hosts = server_host;
|
||||
manager.host_num = server_num;
|
||||
@@ -1812,7 +2072,29 @@ Same behavior as `-d`; see that option for details. Short alias: `-d`.
|
||||
ss_free(working_dir);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
// The protocol has no authentication of its own: only this user
|
||||
// (and the -a user the servers report as) may send, whatever the
|
||||
// umask.
|
||||
if (chmod(manager_address, S_IRUSR | S_IWUSR) == -1) {
|
||||
ERROR("chmod");
|
||||
ss_free(working_dir);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
if (server_uid != (uid_t)-1 && geteuid() == 0 && chown(manager_address, server_uid, (gid_t)-1) == -1) {
|
||||
ERROR("chown");
|
||||
ss_free(working_dir);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
control_is_unix = true;
|
||||
} else {
|
||||
// Over IP anyone who can reach the port could list passwords and
|
||||
// start servers: loopback only.
|
||||
if (ip_addr.host == NULL || !is_loopback_host(ip_addr.host)) {
|
||||
LOGE("the manager address %s:%s is not a loopback address; use a unix socket or 127.0.0.1",
|
||||
ip_addr.host != NULL ? ip_addr.host : "*", ip_addr.port);
|
||||
ss_free(working_dir);
|
||||
FATAL("unsafe manager address");
|
||||
}
|
||||
sfd = create_server_socket(ip_addr.host, ip_addr.port);
|
||||
if (sfd == -1) {
|
||||
ss_free(working_dir);
|
||||
@@ -1824,6 +2106,7 @@ Same behavior as `-d`; see that option for details. Short alias: `-d`.
|
||||
if (!program.watch(manager.fd, manager_recv_cb, &manager))
|
||||
FATAL("failed to watch the control socket");
|
||||
|
||||
daemon_ready();
|
||||
// start ev loop
|
||||
(void)program.run();
|
||||
|
||||
|
||||
+8
-4
@@ -175,6 +175,8 @@ get_sockaddr(char *host, char *port,
|
||||
if (port != NULL) {
|
||||
addr->sin6_port = htons(numeric_port);
|
||||
}
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
} else {
|
||||
@@ -221,12 +223,12 @@ get_sockaddr(char *host, char *port,
|
||||
}
|
||||
}
|
||||
|
||||
if (rp == NULL) {
|
||||
int found = rp != NULL;
|
||||
freeaddrinfo(result);
|
||||
if (!found) {
|
||||
LOGE("failed to resolve remote addr");
|
||||
return -1;
|
||||
}
|
||||
|
||||
freeaddrinfo(result);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -332,7 +334,9 @@ validate_hostname(const char *hostname, const int hostname_len)
|
||||
}
|
||||
}
|
||||
|
||||
label += label_len + 1;
|
||||
if (next_dot == NULL)
|
||||
break;
|
||||
label = next_dot + 1; // a trailing dot ends the loop here
|
||||
}
|
||||
|
||||
return 1;
|
||||
|
||||
@@ -178,6 +178,14 @@ start_obfsproxy(const char *plugin,
|
||||
return ret;
|
||||
}
|
||||
|
||||
static const char *plugin_user = NULL;
|
||||
|
||||
void
|
||||
set_plugin_user(const char *user)
|
||||
{
|
||||
plugin_user = user;
|
||||
}
|
||||
|
||||
int
|
||||
start_plugin(const char *plugin,
|
||||
const char *plugin_opts,
|
||||
@@ -200,6 +208,10 @@ start_plugin(const char *plugin,
|
||||
|
||||
stop_plugin();
|
||||
sub = ss_process_new(plugin);
|
||||
if (ss_process_user(sub, plugin_user) != 0) {
|
||||
stop_plugin();
|
||||
return -1;
|
||||
}
|
||||
#ifdef __MINGW32__
|
||||
sub_control_port = control_port;
|
||||
#endif
|
||||
|
||||
@@ -77,6 +77,9 @@ int start_plugin(const char *plugin,
|
||||
uint16_t control_port,
|
||||
#endif
|
||||
enum plugin_mode mode);
|
||||
/* The user the plugin runs as once started as root (see --user); NULL
|
||||
* keeps the starting user. Set before start_plugin(). */
|
||||
void set_plugin_user(const char *user);
|
||||
uint16_t get_local_port();
|
||||
void stop_plugin();
|
||||
int is_plugin_running();
|
||||
|
||||
@@ -19,5 +19,6 @@ start_plugin(const char *plugin, const char *options, const char *remote_host,
|
||||
return -1;
|
||||
}
|
||||
uint16_t get_local_port(void) { FATAL("Plugin support is disabled in this build"); return 0; }
|
||||
void set_plugin_user(const char *user) { (void)user; }
|
||||
void stop_plugin(void) {}
|
||||
int is_plugin_running(void) { return 0; }
|
||||
+203
@@ -4,18 +4,39 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "utils.h"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <string_view>
|
||||
#ifdef _WIN32
|
||||
#include <winsock2.h>
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <fcntl.h>
|
||||
#include <grp.h>
|
||||
#include <limits.h>
|
||||
#include <pwd.h>
|
||||
#include <signal.h>
|
||||
#include <spawn.h>
|
||||
#include <sys/wait.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
#ifdef __linux__
|
||||
#include <linux/capability.h>
|
||||
#include <sys/prctl.h>
|
||||
#include <sys/syscall.h>
|
||||
#endif
|
||||
extern char **environ;
|
||||
#endif
|
||||
|
||||
#ifndef _WIN32
|
||||
#ifdef __APPLE__
|
||||
typedef int ss_group_t; /* getgrouplist() takes int on macOS */
|
||||
#else
|
||||
typedef gid_t ss_group_t;
|
||||
#endif
|
||||
enum { SS_MAX_GROUPS = 64 };
|
||||
#endif
|
||||
|
||||
struct ss_process {
|
||||
char *program;
|
||||
char **argv, **env;
|
||||
@@ -25,6 +46,11 @@ struct ss_process {
|
||||
uint16_t control_port;
|
||||
#else
|
||||
pid_t pid;
|
||||
bool drop; /* switch to uid/gid/groups in the child */
|
||||
uid_t uid;
|
||||
gid_t gid;
|
||||
ss_group_t groups[SS_MAX_GROUPS];
|
||||
int ngroups;
|
||||
#endif
|
||||
};
|
||||
|
||||
@@ -66,6 +92,165 @@ ss_process_env(struct ss_process *process, const char *name, const char *value)
|
||||
}
|
||||
|
||||
#ifndef _WIN32
|
||||
int
|
||||
ss_process_user(struct ss_process *process, const char *user)
|
||||
{
|
||||
if (user == NULL || geteuid() != 0) {
|
||||
return 0; /* nothing to drop */
|
||||
}
|
||||
char buf[16 * 1024];
|
||||
struct passwd pwdbuf, *pwd = NULL;
|
||||
char *end = NULL;
|
||||
long uid = strtol(user, &end, 10);
|
||||
int err = (*user != '\0' && *end == '\0' && uid >= 0)
|
||||
? getpwuid_r((uid_t)uid, &pwdbuf, buf, sizeof(buf), &pwd)
|
||||
: getpwnam_r(user, &pwdbuf, buf, sizeof(buf), &pwd);
|
||||
if (err != 0 || pwd == NULL) {
|
||||
LOGE("plugin: unknown user %s", user);
|
||||
return -1;
|
||||
}
|
||||
process->ngroups = SS_MAX_GROUPS;
|
||||
if (getgrouplist(pwd->pw_name, (ss_group_t)pwd->pw_gid, process->groups, &process->ngroups) == -1) {
|
||||
process->groups[0] = (ss_group_t)pwd->pw_gid; /* more than SS_MAX_GROUPS: keep the primary one */
|
||||
process->ngroups = 1;
|
||||
}
|
||||
process->uid = pwd->pw_uid;
|
||||
process->gid = pwd->pw_gid;
|
||||
process->drop = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* The executable execve() runs: `program` itself if it names a path, or the
|
||||
* first match on PATH, as posix_spawnp() would find it. */
|
||||
using ProgramPath = std::array<char, PATH_MAX>;
|
||||
|
||||
static bool
|
||||
set_path(ProgramPath &out, std::string_view dir, std::string_view program)
|
||||
{
|
||||
std::string_view slash = dir.empty() ? "" : "/";
|
||||
if (dir.size() + slash.size() + program.size() >= out.size()) {
|
||||
return false;
|
||||
}
|
||||
auto end = std::copy(dir.begin(), dir.end(), out.begin());
|
||||
end = std::copy(slash.begin(), slash.end(), end);
|
||||
end = std::copy(program.begin(), program.end(), end);
|
||||
*end = '\0';
|
||||
return true;
|
||||
}
|
||||
|
||||
static std::string_view
|
||||
env_value(const char *name)
|
||||
{
|
||||
const char *value = getenv(name);
|
||||
return value != NULL ? std::string_view(value) : std::string_view();
|
||||
}
|
||||
|
||||
static bool
|
||||
find_program(std::string_view program, ProgramPath &out)
|
||||
{
|
||||
if (program.find('/') != std::string_view::npos) {
|
||||
return set_path(out, {}, program);
|
||||
}
|
||||
std::string_view path = env_value("PATH");
|
||||
if (path.empty()) {
|
||||
path = "/usr/local/bin:/usr/bin:/bin";
|
||||
}
|
||||
for (;;) {
|
||||
// Not substr(): it can throw, which would pull in the C++ runtime.
|
||||
std::string_view::size_type colon = path.find(':');
|
||||
std::string_view dir(path.data(), colon != std::string_view::npos ? colon : path.size());
|
||||
if (set_path(out, dir, program) && access(out.data(), X_OK) == 0) {
|
||||
return true;
|
||||
}
|
||||
if (colon == std::string_view::npos) {
|
||||
return false;
|
||||
}
|
||||
path.remove_prefix(colon + 1);
|
||||
}
|
||||
}
|
||||
|
||||
/* In the forked child: lose root, but keep the right to bind ports below
|
||||
* 1024 (on Linux), which a server-side plugin listening on 443 needs. Only
|
||||
* system calls from here on. */
|
||||
static int
|
||||
drop_in_child(const struct ss_process *process)
|
||||
{
|
||||
#ifdef __linux__
|
||||
(void)prctl(PR_SET_KEEPCAPS, 1, 0, 0, 0);
|
||||
#endif
|
||||
if (setgroups((size_t)process->ngroups, (const gid_t *)process->groups) != 0
|
||||
|| setgid(process->gid) != 0 || setuid(process->uid) != 0) {
|
||||
return -1;
|
||||
}
|
||||
if (setuid(0) == 0) {
|
||||
return -1; /* root must be gone for good */
|
||||
}
|
||||
#ifdef __linux__
|
||||
struct __user_cap_header_struct header = { _LINUX_CAPABILITY_VERSION_3, 0 };
|
||||
struct __user_cap_data_struct data[2];
|
||||
memset(data, 0, sizeof(data));
|
||||
data[0].effective = data[0].permitted = data[0].inheritable = 1U << CAP_NET_BIND_SERVICE;
|
||||
if (syscall(SYS_capset, &header, data) == 0) {
|
||||
#ifdef PR_CAP_AMBIENT
|
||||
/* Ambient, so it survives execve() of a non-setcap binary. */
|
||||
(void)prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, CAP_NET_BIND_SERVICE, 0, 0);
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
fork_as_user(struct ss_process *process, char **env)
|
||||
{
|
||||
ProgramPath path;
|
||||
if (!find_program(process->program, path)) {
|
||||
return ENOENT;
|
||||
}
|
||||
/* The child reports a failed switch or exec through this pipe. */
|
||||
int report[2];
|
||||
if (pipe(report) != 0) {
|
||||
return errno;
|
||||
}
|
||||
(void)fcntl(report[1], F_SETFD, FD_CLOEXEC);
|
||||
pid_t pid = fork();
|
||||
if (pid == 0) {
|
||||
close(report[0]);
|
||||
(void)setpgid(0, 0);
|
||||
sigset_t empty;
|
||||
sigemptyset(&empty);
|
||||
(void)sigprocmask(SIG_SETMASK, &empty, NULL);
|
||||
signal(SIGPIPE, SIG_DFL);
|
||||
signal(SIGTERM, SIG_DFL);
|
||||
signal(SIGINT, SIG_DFL);
|
||||
int error = EPERM;
|
||||
if (drop_in_child(process) == 0) {
|
||||
execve(path.data(), process->argv, env); // NOLINT(clang-analyzer-optin.taint.GenericTaint): the PATH lookup posix_spawnp() would do
|
||||
error = errno;
|
||||
}
|
||||
ssize_t ignored = write(report[1], &error, sizeof(error));
|
||||
(void)ignored;
|
||||
_exit(127);
|
||||
}
|
||||
int error = pid < 0 ? errno : 0;
|
||||
close(report[1]);
|
||||
if (pid > 0) {
|
||||
int child_error = 0;
|
||||
ssize_t n;
|
||||
do {
|
||||
n = read(report[0], &child_error, sizeof(child_error));
|
||||
} while (n < 0 && errno == EINTR);
|
||||
if (n == (ssize_t)sizeof(child_error)) {
|
||||
while (waitpid(pid, NULL, 0) < 0 && errno == EINTR) {}
|
||||
error = child_error;
|
||||
} else {
|
||||
process->pid = pid;
|
||||
}
|
||||
}
|
||||
close(report[0]);
|
||||
return error;
|
||||
}
|
||||
|
||||
static bool
|
||||
overridden(const struct ss_process *process, const char *entry)
|
||||
{
|
||||
@@ -100,6 +285,16 @@ ss_process_start(struct ss_process *process, uint16_t control_port)
|
||||
env[used++] = process->env[i];
|
||||
}
|
||||
env[used] = NULL;
|
||||
if (process->drop) {
|
||||
int error = fork_as_user(process, env);
|
||||
free(env); // heap-ok: plugin start, startup
|
||||
if (error != 0) {
|
||||
process->pid = 0;
|
||||
errno = error;
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
posix_spawnattr_t attr;
|
||||
int error = posix_spawnattr_init(&attr);
|
||||
if (error == 0) {
|
||||
@@ -159,6 +354,14 @@ stop_process(struct ss_process *process)
|
||||
}
|
||||
}
|
||||
#else
|
||||
int
|
||||
ss_process_user(struct ss_process *process, const char *user)
|
||||
{
|
||||
(void)process;
|
||||
(void)user;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static wchar_t *
|
||||
wide_string(const char *text)
|
||||
{
|
||||
|
||||
@@ -817,6 +817,7 @@ Same behavior as `-T`; see that option for details. Short alias: `-T`.
|
||||
snprintf(remote_str + len, buf_size - len, "|%s", remote_addr[i].host);
|
||||
len = strlen(remote_str);
|
||||
}
|
||||
set_plugin_user(user); // it drops root along with us
|
||||
int err = start_plugin(plugin, plugin_opts, remote_str,
|
||||
remote_port, plugin_host, plugin_port, MODE_CLIENT);
|
||||
if (err) {
|
||||
@@ -949,6 +950,7 @@ Same behavior as `-T`; see that option for details. Short alias: `-T`.
|
||||
LOGI("running from root user");
|
||||
}
|
||||
|
||||
daemon_ready();
|
||||
ss::heap_seal();
|
||||
{
|
||||
int code = program.run();
|
||||
|
||||
+5
-4
@@ -31,10 +31,11 @@ Status Method::init(const char *method, const char *password, const char *key, R
|
||||
role_ = role;
|
||||
// The salt/IV replay filter shared by every connection and datagram.
|
||||
if (!bloom_ready_) {
|
||||
if (role == Role::server) {
|
||||
ppbloom_init(kBloomEntriesServer, kBloomErrorServer);
|
||||
} else {
|
||||
ppbloom_init(kBloomEntriesClient, kBloomErrorClient);
|
||||
int err = role == Role::server ? ppbloom_init(kBloomEntriesServer, kBloomErrorServer)
|
||||
: ppbloom_init(kBloomEntriesClient, kBloomErrorClient);
|
||||
if (err != 0) {
|
||||
LOGE("failed to set up the replay filter");
|
||||
return fail(Errc::exhausted);
|
||||
}
|
||||
bloom_ready_ = true;
|
||||
}
|
||||
|
||||
@@ -30,6 +30,10 @@ public:
|
||||
// or a base64 key (AEAD-2022 needs the key). Also initialises libsodium
|
||||
// and the process-wide ppbloom replay filter, sized for the role.
|
||||
Status init(const char *method, const char *password, const char *key, Role role) noexcept;
|
||||
Method() = default;
|
||||
// It owns the process-wide replay filter: a copy would free it twice.
|
||||
Method(const Method &) = delete;
|
||||
Method &operator=(const Method &) = delete;
|
||||
~Method();
|
||||
|
||||
Role role() const noexcept { return role_; }
|
||||
|
||||
+19
-4
@@ -37,12 +37,12 @@ Status Resolver::init(event::Loop &loop, const char *nameservers, bool ipv6_firs
|
||||
if (status != ARES_SUCCESS) {
|
||||
LOGE("c-ares error: %s", ares_strerror(status));
|
||||
channel_ = nullptr;
|
||||
ares_library_cleanup();
|
||||
return fail(Errc::config);
|
||||
}
|
||||
if (nameservers != nullptr && ares_set_servers_ports_csv(channel_, nameservers) != ARES_SUCCESS) {
|
||||
LOGE("failed to set nameservers %s", nameservers);
|
||||
ares_destroy(channel_);
|
||||
channel_ = nullptr;
|
||||
abandon_channel();
|
||||
return fail(Errc::config);
|
||||
}
|
||||
loop_ = &loop;
|
||||
@@ -50,10 +50,21 @@ Status Resolver::init(event::Loop &loop, const char *nameservers, bool ipv6_firs
|
||||
for (Watch &w : watches_) {
|
||||
w.owner = this;
|
||||
}
|
||||
SS_TRY(timer_.open(loop, *this));
|
||||
if (Status opened = timer_.open(loop, *this); !opened) {
|
||||
abandon_channel();
|
||||
return opened;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
// Undo init() after the channel was created.
|
||||
void Resolver::abandon_channel() noexcept
|
||||
{
|
||||
ares_destroy(channel_);
|
||||
channel_ = nullptr;
|
||||
ares_library_cleanup();
|
||||
}
|
||||
|
||||
Status Resolver::resolve(const char *host, std::uint16_t port, const ResolveCallback &cb) noexcept
|
||||
{
|
||||
if (channel_ == nullptr) {
|
||||
@@ -89,6 +100,9 @@ void Resolver::on_host(void *arg, int status, int, hostent *he) noexcept
|
||||
} else if (verbose && status != ARES_EDESTRUCTION) {
|
||||
LOGI("failed to lookup address: %s", ares_strerror(status));
|
||||
}
|
||||
if (status == ARES_EDESTRUCTION || status == ARES_ECANCELLED) {
|
||||
q->cancelled = true;
|
||||
}
|
||||
q->owner->finish(q, status);
|
||||
}
|
||||
|
||||
@@ -99,7 +113,8 @@ void Resolver::finish(Query *q, int status) noexcept
|
||||
}
|
||||
PoolPtr<Query> owned = queries_.adopt(q);
|
||||
const sockaddr *best = nullptr;
|
||||
if (status != ARES_EDESTRUCTION) {
|
||||
(void)status;
|
||||
if (!q->cancelled) {
|
||||
if (ipv6_first_) {
|
||||
best = q->has6 ? reinterpret_cast<const sockaddr *>(&q->v6)
|
||||
: (q->has4 ? reinterpret_cast<const sockaddr *>(&q->v4) : nullptr);
|
||||
|
||||
@@ -67,6 +67,7 @@ private:
|
||||
std::uint16_t port = 0;
|
||||
std::uint8_t pending = 2; // A and AAAA
|
||||
bool has4 = false, has6 = false;
|
||||
bool cancelled = false; // shutdown() ended a lookup: report no address
|
||||
sockaddr_in v4{};
|
||||
sockaddr_in6 v6{};
|
||||
};
|
||||
@@ -81,6 +82,7 @@ private:
|
||||
static void sock_state(void *data, ares_socket_t s, int readable, int writable) noexcept;
|
||||
static void on_host(void *arg, int status, int timeouts, hostent *he) noexcept;
|
||||
void on_timer() noexcept;
|
||||
void abandon_channel() noexcept;
|
||||
void rearm() noexcept;
|
||||
void finish(Query *q, int status) noexcept;
|
||||
void on_closed_all() noexcept;
|
||||
|
||||
+77
-30
@@ -33,6 +33,7 @@ namespace {
|
||||
|
||||
constexpr std::uint64_t kDelayMs = 50; // wait for a first payload before connecting
|
||||
constexpr int kAcceptBurst = 64; // connections accepted per wakeup
|
||||
constexpr std::uint64_t kAcceptRetryMs = 1000; // pause after accept() runs out of fds
|
||||
constexpr int kPumpRounds = 16; // receives per direction per wakeup
|
||||
constexpr std::uint8_t kSocksVersion = 5;
|
||||
|
||||
@@ -144,6 +145,10 @@ Status TcpRelay::init(event::Loop &loop, Method &method, const TcpConfig &config
|
||||
|
||||
const std::uint64_t timeout = static_cast<std::uint64_t>(std::max(config.timeout_secs, 1));
|
||||
delay_list_.set_timeout(kDelayMs);
|
||||
// A handshake (SOCKS request, ss header, or the drain after a rejected
|
||||
// one) gets the plain --timeout, not the day-long idle minimum, so idle
|
||||
// sockets cannot hold the fixed pool.
|
||||
handshake_list_.set_timeout(timeout * 1000);
|
||||
connect_list_.set_timeout(std::min<std::uint64_t>(MAX_CONNECT_TIMEOUT, timeout) * 1000);
|
||||
idle_list_.set_timeout(std::max<std::uint64_t>(MIN_TCP_IDLE_TIMEOUT, timeout) * 1000);
|
||||
SS_TRY(timer_.open(loop, *this));
|
||||
@@ -183,9 +188,22 @@ void TcpRelay::accept_from(Listener &listener) noexcept
|
||||
if (fd < 0) {
|
||||
#endif
|
||||
int err = ss_socket_error();
|
||||
if (err != EAGAIN && err != EWOULDBLOCK && err != EINTR) {
|
||||
ERROR("accept");
|
||||
if (err == EAGAIN || err == EWOULDBLOCK || err == EINTR) {
|
||||
return;
|
||||
}
|
||||
if (err == ECONNABORTED) {
|
||||
continue;
|
||||
}
|
||||
// Out of descriptors or memory: the pending connection stays
|
||||
// queued, so a level-triggered poll would spin. Back off.
|
||||
std::uint64_t now = loop_->now();
|
||||
if (now - last_refusal_log_ >= 1000 || last_refusal_log_ == 0) {
|
||||
ERROR("accept");
|
||||
last_refusal_log_ = now;
|
||||
}
|
||||
pause_accept();
|
||||
accept_retry_at_ = now + kAcceptRetryMs;
|
||||
schedule();
|
||||
return;
|
||||
}
|
||||
if (pool_.full()) {
|
||||
@@ -203,9 +221,7 @@ void TcpRelay::accept_from(Listener &listener) noexcept
|
||||
if (config_.kind == Kind::server && config_.acl) {
|
||||
char peer[INET6_ADDRSTRLEN];
|
||||
peer_name(fd, peer, sizeof(peer));
|
||||
if (peer[0] != '\0'
|
||||
&& ((config_.acl_hooks.mode() == BLACK_LIST && config_.acl_hooks.match_host(peer) == 1)
|
||||
|| (config_.acl_hooks.mode() == WHITE_LIST && config_.acl_hooks.match_host(peer) >= 0))) {
|
||||
if (peer[0] != '\0' && config_.acl_hooks.peer_denied(peer)) {
|
||||
LOGE("Access denied from %s", peer);
|
||||
ss_socket_close(fd);
|
||||
continue;
|
||||
@@ -268,9 +284,10 @@ void TcpRelay::set_deadline(Connection &conn, Connection::Timed which) noexcept
|
||||
if (conn.timed_ != which) {
|
||||
clear_deadline(conn);
|
||||
}
|
||||
event::DeadlineList<Connection> *list = which == Connection::Timed::delay ? &delay_list_
|
||||
: which == Connection::Timed::connect ? &connect_list_
|
||||
: &idle_list_;
|
||||
event::DeadlineList<Connection> *list = which == Connection::Timed::delay ? &delay_list_
|
||||
: which == Connection::Timed::handshake ? &handshake_list_
|
||||
: which == Connection::Timed::connect ? &connect_list_
|
||||
: &idle_list_;
|
||||
std::uint64_t now = loop_->now();
|
||||
list->touch(conn, now);
|
||||
conn.timed_ = which;
|
||||
@@ -307,15 +324,20 @@ void TcpRelay::on_timer() noexcept
|
||||
};
|
||||
};
|
||||
delay_list_.expire(now, fire(Connection::Timed::delay));
|
||||
handshake_list_.expire(now, fire(Connection::Timed::handshake));
|
||||
connect_list_.expire(now, fire(Connection::Timed::connect));
|
||||
idle_list_.expire(now, fire(Connection::Timed::idle));
|
||||
if (accept_retry_at_ != 0 && accept_retry_at_ <= now) {
|
||||
accept_retry_at_ = 0;
|
||||
slot_freed();
|
||||
}
|
||||
schedule();
|
||||
}
|
||||
|
||||
void TcpRelay::schedule() noexcept
|
||||
{
|
||||
std::uint64_t due = 0;
|
||||
for (auto *list : { &delay_list_, &connect_list_, &idle_list_ }) {
|
||||
std::uint64_t due = accept_retry_at_;
|
||||
for (auto *list : { &delay_list_, &handshake_list_, &connect_list_, &idle_list_ }) {
|
||||
if (auto next = list->next_due()) {
|
||||
due = due == 0 ? *next : std::min(due, *next);
|
||||
}
|
||||
@@ -427,9 +449,9 @@ Flow Connection::start(ss_socket_t fd) noexcept
|
||||
}
|
||||
const TcpConfig &cfg = relay_->config_;
|
||||
// Every connection is timed from the start, so a client that never
|
||||
// finishes its handshake cannot hold a pool slot forever. Later stages
|
||||
// finishes its handshake cannot hold a pool slot for long. Later stages
|
||||
// replace this with their own deadline.
|
||||
relay_->set_deadline(*this, Timed::idle);
|
||||
relay_->set_deadline(*this, Timed::handshake);
|
||||
switch (cfg.kind) {
|
||||
case Kind::local:
|
||||
if (verbose) {
|
||||
@@ -550,7 +572,10 @@ Flow Connection::on_upstream(int events) noexcept
|
||||
if ((events & event::writable) && pump_to_upstream() == Flow::closed) {
|
||||
return Flow::closed;
|
||||
}
|
||||
if ((events & (event::readable | event::failed)) && pump_to_client() == Flow::closed) {
|
||||
// A rejected request (server_reject) leaves only the drain; the closing
|
||||
// upstream must not be read.
|
||||
if (stage_ == Stage::stream && (events & (event::readable | event::failed))
|
||||
&& pump_to_client() == Flow::closed) {
|
||||
return Flow::closed;
|
||||
}
|
||||
update_interest();
|
||||
@@ -948,18 +973,11 @@ Flow Connection::server_header() noexcept
|
||||
Flow Connection::server_connect(const sockaddr *target) noexcept
|
||||
{
|
||||
TcpConfig &cfg = relay_->config_;
|
||||
if (cfg.acl) {
|
||||
char ip[INET6_ADDRSTRLEN] = {};
|
||||
const void *raw = target->sa_family == AF_INET
|
||||
? static_cast<const void *>(&reinterpret_cast<const sockaddr_in *>(target)->sin_addr)
|
||||
: static_cast<const void *>(&reinterpret_cast<const sockaddr_in6 *>(target)->sin6_addr);
|
||||
ss_inet_ntop(target->sa_family, raw, ip, sizeof(ip));
|
||||
if (cfg.acl_hooks.outbound_blocked(ip) == 1) {
|
||||
if (verbose) {
|
||||
LOGI("outbound blocked %s", ip);
|
||||
}
|
||||
return close();
|
||||
if (cfg.acl && cfg.acl_hooks.target_blocked(target)) {
|
||||
if (verbose) {
|
||||
LOGI("outbound blocked %s", target_.host);
|
||||
}
|
||||
return close();
|
||||
}
|
||||
auto fd = open_upstream(target->sa_family, cfg.upstream, false);
|
||||
if (!fd) {
|
||||
@@ -982,6 +1000,7 @@ Flow Connection::server_connect(const sockaddr *target) noexcept
|
||||
}
|
||||
(void)to_upstream_.consume(*sent);
|
||||
stage_ = Stage::connecting;
|
||||
relay_->set_deadline(*this, Timed::connect);
|
||||
update_interest();
|
||||
return Flow::proceed;
|
||||
}
|
||||
@@ -990,9 +1009,7 @@ Flow Connection::server_connect(const sockaddr *target) noexcept
|
||||
|
||||
Flow Connection::connected() noexcept
|
||||
{
|
||||
if (client_kind()) {
|
||||
relay_->set_deadline(*this, Timed::idle); // replaces the connect deadline
|
||||
}
|
||||
relay_->set_deadline(*this, Timed::idle); // replaces the connect deadline
|
||||
stage_ = Stage::stream;
|
||||
if (pump_to_upstream() == Flow::closed) {
|
||||
return Flow::closed;
|
||||
@@ -1072,7 +1089,9 @@ Connection::Pull Connection::pull_into(ss_socket_t fd, bool decode, LeasedBuffer
|
||||
}
|
||||
}
|
||||
if (stage_ == Stage::stream) {
|
||||
relay_->set_deadline(*this, Timed::idle);
|
||||
// A half-closed connection gets the plain --timeout, not the
|
||||
// day-long idle minimum.
|
||||
relay_->set_deadline(*this, client_eof_ || upstream_eof_ ? Timed::handshake : Timed::idle);
|
||||
}
|
||||
std::span<const std::uint8_t> in(relay_->scratch_.data(), *r);
|
||||
if (decode) {
|
||||
@@ -1099,6 +1118,9 @@ Connection::Pull Connection::pull_into(ss_socket_t fd, bool decode, LeasedBuffer
|
||||
|
||||
Flow Connection::pump_to_upstream() noexcept
|
||||
{
|
||||
if (client_eof_) {
|
||||
return Flow::proceed; // nothing more comes from the client
|
||||
}
|
||||
const bool decode = !client_kind();
|
||||
for (int round = 0; round < kPumpRounds; ++round) {
|
||||
if (!to_upstream_.empty()) {
|
||||
@@ -1121,6 +1143,9 @@ Flow Connection::pump_to_upstream() noexcept
|
||||
case Pull::again:
|
||||
return Flow::proceed;
|
||||
case Pull::eof:
|
||||
// Everything read has been flushed: pass the EOF on.
|
||||
client_eof_ = true;
|
||||
return half_close(upstream_.fd());
|
||||
case Pull::error:
|
||||
return close();
|
||||
}
|
||||
@@ -1130,6 +1155,9 @@ Flow Connection::pump_to_upstream() noexcept
|
||||
|
||||
Flow Connection::pump_to_client() noexcept
|
||||
{
|
||||
if (upstream_eof_) {
|
||||
return Flow::proceed; // nothing more comes from upstream
|
||||
}
|
||||
const bool decode = client_kind();
|
||||
for (int round = 0; round < kPumpRounds; ++round) {
|
||||
if (!to_client_.empty()) {
|
||||
@@ -1161,6 +1189,8 @@ Flow Connection::pump_to_client() noexcept
|
||||
case Pull::again:
|
||||
return Flow::proceed;
|
||||
case Pull::eof:
|
||||
upstream_eof_ = true;
|
||||
return half_close(client_.fd());
|
||||
case Pull::error:
|
||||
return close();
|
||||
}
|
||||
@@ -1168,6 +1198,23 @@ Flow Connection::pump_to_client() noexcept
|
||||
return Flow::proceed;
|
||||
}
|
||||
|
||||
Flow Connection::half_close(ss_socket_t peer) noexcept
|
||||
{
|
||||
if (client_eof_ && upstream_eof_) {
|
||||
return close();
|
||||
}
|
||||
#ifdef _WIN32
|
||||
int how = SD_SEND;
|
||||
#else
|
||||
int how = SHUT_WR;
|
||||
#endif
|
||||
if (shutdown(peer, how) != 0) {
|
||||
return close();
|
||||
}
|
||||
relay_->set_deadline(*this, Timed::handshake);
|
||||
return Flow::proceed;
|
||||
}
|
||||
|
||||
void Connection::update_interest() noexcept
|
||||
{
|
||||
int c = 0, u = 0;
|
||||
@@ -1187,8 +1234,8 @@ void Connection::update_interest() noexcept
|
||||
u = event::writable;
|
||||
break;
|
||||
case Stage::stream:
|
||||
c = (to_upstream_.empty() ? event::readable : 0) | (to_client_.empty() ? 0 : event::writable);
|
||||
u = (to_client_.empty() ? event::readable : 0) | (to_upstream_.empty() ? 0 : event::writable);
|
||||
c = (to_upstream_.empty() && !client_eof_ ? event::readable : 0) | (to_client_.empty() ? 0 : event::writable);
|
||||
u = (to_client_.empty() && !upstream_eof_ ? event::readable : 0) | (to_upstream_.empty() ? 0 : event::writable);
|
||||
break;
|
||||
}
|
||||
if (client_.is_open() && !client_.is_closing()) {
|
||||
|
||||
+29
-1
@@ -44,6 +44,25 @@ struct AclHooks {
|
||||
int (*match_host)(const char *host) = nullptr; // > 0 listed as bypass/block, < 0 as proxy, 0 neither
|
||||
int (*mode)() = nullptr; // BLACK_LIST or WHITE_LIST
|
||||
int (*outbound_blocked)(const char *host) = nullptr;
|
||||
|
||||
// ss-server: is a client at `peer` (an address) refused?
|
||||
bool peer_denied(const char *peer) const noexcept
|
||||
{
|
||||
constexpr int kWhiteList = 1; // acl.h WHITE_LIST
|
||||
int listed = match_host(peer);
|
||||
return mode() == kWhiteList ? listed >= 0 : listed == 1;
|
||||
}
|
||||
|
||||
// ss-server: may a client reach `target`?
|
||||
bool target_blocked(const sockaddr *target) const noexcept
|
||||
{
|
||||
char ip[INET6_ADDRSTRLEN] = {};
|
||||
const void *raw = target->sa_family == AF_INET
|
||||
? static_cast<const void *>(&reinterpret_cast<const sockaddr_in *>(target)->sin_addr)
|
||||
: static_cast<const void *>(&reinterpret_cast<const sockaddr_in6 *>(target)->sin6_addr);
|
||||
ss_inet_ntop(target->sa_family, raw, ip, sizeof(ip));
|
||||
return outbound_blocked(ip) == 1;
|
||||
}
|
||||
};
|
||||
|
||||
struct TcpConfig {
|
||||
@@ -166,7 +185,7 @@ private:
|
||||
stream,
|
||||
stopped, // server: failed the handshake; drain until timeout
|
||||
};
|
||||
enum class Timed : std::uint8_t { none, delay, connect, idle };
|
||||
enum class Timed : std::uint8_t { none, delay, handshake, connect, idle };
|
||||
|
||||
event::Flow start(ss_socket_t fd) noexcept;
|
||||
event::Flow close() noexcept;
|
||||
@@ -185,6 +204,7 @@ private:
|
||||
event::Flow connected() noexcept;
|
||||
event::Flow pump_to_upstream() noexcept;
|
||||
event::Flow pump_to_client() noexcept;
|
||||
event::Flow half_close(ss_socket_t peer) noexcept;
|
||||
void update_interest() noexcept;
|
||||
|
||||
enum class Pull : std::uint8_t { data, nothing, again, eof, error };
|
||||
@@ -217,6 +237,10 @@ private:
|
||||
bool dns_done_ = false;
|
||||
bool dns_ok_ = false;
|
||||
bool replied_ = false; // first upstream bytes reached the client
|
||||
// Half-close: one side has sent EOF, which went on to the other side as
|
||||
// a shutdown(SHUT_WR); the other direction keeps flowing until its EOF.
|
||||
bool client_eof_ = false;
|
||||
bool upstream_eof_ = false;
|
||||
void *hook_state_ = nullptr;
|
||||
int tos_ = -1; // from the listener that accepted it
|
||||
sockaddr_storage dns_answer_{};
|
||||
@@ -238,6 +262,8 @@ public:
|
||||
// Close every connection and listener. Run the loop until closed().
|
||||
void shutdown() noexcept;
|
||||
bool closed() const noexcept;
|
||||
// Listening sockets the relay owns (and closes on shutdown).
|
||||
std::size_t listeners() const noexcept { return listener_count_; }
|
||||
|
||||
std::uint32_t live() const noexcept { return pool_.live(); }
|
||||
std::uint64_t refused() const noexcept { return refused_; }
|
||||
@@ -282,8 +308,10 @@ private:
|
||||
bool accept_paused_ = false;
|
||||
std::uint64_t refused_ = 0;
|
||||
std::uint64_t last_refusal_log_ = 0;
|
||||
std::uint64_t accept_retry_at_ = 0; // accept() failed hard (e.g. EMFILE): retry then
|
||||
|
||||
event::DeadlineList<Connection> delay_list_;
|
||||
event::DeadlineList<Connection> handshake_list_;
|
||||
event::DeadlineList<Connection> connect_list_;
|
||||
event::DeadlineList<Connection> idle_list_;
|
||||
event::Timer<TcpRelay, &TcpRelay::on_timer> timer_;
|
||||
|
||||
+71
-20
@@ -298,8 +298,11 @@ Status UdpRelay::init(event::Loop &loop, Method &method, const UdpConfig &config
|
||||
if (config.kind == Kind::server) {
|
||||
if (method.psk() != nullptr) {
|
||||
// An entry evicted from shared_ stays alive while a relay
|
||||
// session still references it (at most one per session).
|
||||
SS_TRY(shared_pool_.reserve(config.max_sessions_2022 + config.max_sessions));
|
||||
// session still references it (one per session, closing ones
|
||||
// included); pending DNS queries hold one too, and a new session
|
||||
// holds a slot while its first packet is checked.
|
||||
SS_TRY(shared_pool_.reserve(config.max_sessions_2022 + 2 * config.max_sessions
|
||||
+ config.max_pending_dns + 1));
|
||||
SS_TRY(shared_.reserve(config.max_sessions_2022, seed ^ 0x9E3779B97F4A7C15ULL));
|
||||
}
|
||||
SS_TRY(pending_.reserve(config.max_pending_dns));
|
||||
@@ -461,9 +464,6 @@ UdpSession *UdpRelay::session_for(std::uint8_t listener, const sockaddr_storage
|
||||
if (verbose) {
|
||||
LOGI("[%s] [udp] cache miss", config_.label);
|
||||
}
|
||||
if (sessions_.full()) {
|
||||
(void)sessions_.pop_oldest(); // closes the least recently used session
|
||||
}
|
||||
auto fd = open_upstream_socket(family);
|
||||
if (!fd) {
|
||||
return nullptr;
|
||||
@@ -482,6 +482,11 @@ UdpSession *UdpRelay::session_for(std::uint8_t listener, const sockaddr_storage
|
||||
s.key_ = raw;
|
||||
s.key_len_ = static_cast<std::uint8_t>(raw.size());
|
||||
(void)s.socket_.want(event::readable);
|
||||
// Evict only once the replacement exists, so a failure above loses no
|
||||
// live mapping.
|
||||
if (sessions_.full()) {
|
||||
(void)sessions_.pop_oldest(); // closes the least recently used session
|
||||
}
|
||||
auto inserted = sessions_.emplace(*key, std::move(*session));
|
||||
if (!inserted) {
|
||||
return nullptr; // the PoolPtr went out of scope: closed again
|
||||
@@ -568,17 +573,38 @@ void UdpRelay::client_packet(Listener &listener, const sockaddr_storage &from, s
|
||||
}
|
||||
}
|
||||
|
||||
RefPtr<Shared2022> UdpRelay::shared_for(const std::array<std::uint8_t, crypto::kSessionIdLen> &id) noexcept
|
||||
RefPtr<Shared2022> UdpRelay::shared_find(const std::array<std::uint8_t, crypto::kSessionIdLen> &id) noexcept
|
||||
{
|
||||
auto key = FixedKey<crypto::kSessionIdLen>::from(std::as_bytes(std::span(id)));
|
||||
if (!key) {
|
||||
return nullptr;
|
||||
}
|
||||
std::uint64_t now = now_secs();
|
||||
if (RefPtr<Shared2022> *found = shared_.find(*key)) {
|
||||
(*found)->last_used = now;
|
||||
(*found)->last_used = now_secs();
|
||||
return *found;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
RefPtr<Shared2022> UdpRelay::shared_new() noexcept
|
||||
{
|
||||
auto slot = shared_pool_.acquire(*this);
|
||||
if (!slot) {
|
||||
return nullptr;
|
||||
}
|
||||
Shared2022 *raw = slot->release(); // the references own it from here
|
||||
raw->last_used = now_secs();
|
||||
return RefPtr<Shared2022>(raw);
|
||||
}
|
||||
|
||||
bool UdpRelay::shared_insert(const std::array<std::uint8_t, crypto::kSessionIdLen> &id,
|
||||
const RefPtr<Shared2022> &ref) noexcept
|
||||
{
|
||||
auto key = FixedKey<crypto::kSessionIdLen>::from(std::as_bytes(std::span(id)));
|
||||
if (!key) {
|
||||
return false;
|
||||
}
|
||||
std::uint64_t now = now_secs();
|
||||
// Forget client sessions idle for longer than the TTL (oldest first).
|
||||
while (RefPtr<Shared2022> *oldest = shared_.oldest()) {
|
||||
if ((*oldest)->last_used + kTtl2022 > now) {
|
||||
@@ -589,17 +615,7 @@ RefPtr<Shared2022> UdpRelay::shared_for(const std::array<std::uint8_t, crypto::k
|
||||
if (shared_.full()) {
|
||||
(void)shared_.pop_oldest(); // relay sessions still using it keep it alive
|
||||
}
|
||||
auto slot = shared_pool_.acquire(*this);
|
||||
if (!slot) {
|
||||
return nullptr;
|
||||
}
|
||||
Shared2022 *raw = slot->release(); // the references own it from here
|
||||
raw->last_used = now;
|
||||
RefPtr<Shared2022> ref(raw);
|
||||
if (!shared_.emplace(*key, ref)) {
|
||||
return nullptr;
|
||||
}
|
||||
return ref;
|
||||
return shared_.emplace(*key, ref).has_value();
|
||||
}
|
||||
|
||||
void UdpRelay::server_packet(Listener &listener, const sockaddr_storage &from, std::span<std::uint8_t> packet) noexcept
|
||||
@@ -607,6 +623,19 @@ void UdpRelay::server_packet(Listener &listener, const sockaddr_storage &from, s
|
||||
if (config_.tx != nullptr) {
|
||||
*config_.tx += packet.size();
|
||||
}
|
||||
if (config_.acl) {
|
||||
char peer[INET6_ADDRSTRLEN] = {};
|
||||
const void *raw = from.ss_family == AF_INET
|
||||
? static_cast<const void *>(&reinterpret_cast<const sockaddr_in *>(&from)->sin_addr)
|
||||
: static_cast<const void *>(&reinterpret_cast<const sockaddr_in6 *>(&from)->sin6_addr);
|
||||
ss_inet_ntop(from.ss_family, raw, peer, sizeof(peer));
|
||||
if (peer[0] != '\0' && config_.acl_hooks.peer_denied(peer)) {
|
||||
if (verbose) {
|
||||
LOGI("[udp] access denied from %s", peer);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
Result<std::size_t> opened = fail(Errc::config);
|
||||
RefPtr<Shared2022> shared;
|
||||
if (const crypto::MasterKey *master = method_->aead()) {
|
||||
@@ -614,10 +643,20 @@ void UdpRelay::server_packet(Listener &listener, const sockaddr_storage &from, s
|
||||
} else if (const crypto::Psk *psk = method_->psk()) {
|
||||
auto pkt = crypto::udp2022_decrypt(*psk, packet, out_);
|
||||
if (pkt) {
|
||||
shared = shared_for(pkt->session_id);
|
||||
// A new client session enters the table only once its first
|
||||
// packet passes the timestamp and replay checks, so replayed
|
||||
// stale packets cannot evict live sessions.
|
||||
shared = shared_find(pkt->session_id);
|
||||
bool fresh = !shared;
|
||||
if (fresh) {
|
||||
shared = shared_new();
|
||||
}
|
||||
if (shared) {
|
||||
opened = crypto::udp2022_accept(Role::server, shared->state, *pkt, out_,
|
||||
static_cast<std::uint64_t>(std::time(nullptr)));
|
||||
if (opened && fresh && !shared_insert(pkt->session_id, shared)) {
|
||||
opened = fail(Errc::exhausted);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -655,6 +694,12 @@ void UdpRelay::server_packet(Listener &listener, const sockaddr_storage &from, s
|
||||
LOGE("[udp] invalid host name");
|
||||
return;
|
||||
}
|
||||
if (config_.acl && config_.acl_hooks.outbound_blocked(target.host) == 1) {
|
||||
if (verbose) {
|
||||
LOGI("[udp] outbound blocked %s", target.host);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Hold the payload until the name resolves.
|
||||
auto pending = pending_.acquire();
|
||||
if (!pending || resolver_ == nullptr) {
|
||||
@@ -699,6 +744,12 @@ void UdpRelay::resolved(const ResolveCallback &cb, const sockaddr *address) noex
|
||||
void UdpRelay::server_forward(std::uint8_t listener, const sockaddr_storage &client, const sockaddr *target,
|
||||
RefPtr<Shared2022> shared, std::span<const std::uint8_t> payload) noexcept
|
||||
{
|
||||
if (config_.acl && config_.acl_hooks.target_blocked(target)) {
|
||||
if (verbose) {
|
||||
LOGI("[udp] outbound blocked");
|
||||
}
|
||||
return;
|
||||
}
|
||||
UdpSession *session = session_for(listener, client, target->sa_family);
|
||||
if (session == nullptr) {
|
||||
return;
|
||||
|
||||
+6
-1
@@ -58,6 +58,8 @@ struct UdpConfig {
|
||||
std::uint64_t *tx = nullptr;
|
||||
std::uint64_t *rx = nullptr;
|
||||
void (*on_traffic)() = nullptr;
|
||||
bool acl = false; // server: with `acl_hooks` set, as for TCP
|
||||
AclHooks acl_hooks;
|
||||
};
|
||||
|
||||
// Bind the UDP socket clients send to (ss-redir: transparent, with the
|
||||
@@ -149,7 +151,10 @@ private:
|
||||
RefPtr<Shared2022> shared, std::span<const std::uint8_t> payload) noexcept;
|
||||
void reply(UdpSession &session) noexcept;
|
||||
UdpSession *session_for(std::uint8_t listener, const sockaddr_storage &client, int family) noexcept;
|
||||
RefPtr<Shared2022> shared_for(const std::array<std::uint8_t, crypto::kSessionIdLen> &id) noexcept;
|
||||
RefPtr<Shared2022> shared_find(const std::array<std::uint8_t, crypto::kSessionIdLen> &id) noexcept;
|
||||
RefPtr<Shared2022> shared_new() noexcept;
|
||||
bool shared_insert(const std::array<std::uint8_t, crypto::kSessionIdLen> &id,
|
||||
const RefPtr<Shared2022> &ref) noexcept;
|
||||
void expire(UdpSession &session) noexcept;
|
||||
Result<ss_socket_t> open_upstream_socket(int family) noexcept;
|
||||
static void resolved(const ResolveCallback &cb, const sockaddr *address) noexcept;
|
||||
|
||||
+21
-2
@@ -72,6 +72,10 @@ add_rule(rule_list_t *rules, rule_t *rule)
|
||||
rules->push_back(*rule);
|
||||
}
|
||||
|
||||
#if SS_ENABLE_REGEX
|
||||
static pcre2_match_context *match_context = NULL;
|
||||
#endif
|
||||
|
||||
int
|
||||
init_rule(rule_t *rule)
|
||||
{
|
||||
@@ -86,11 +90,22 @@ init_rule(rule_t *rule)
|
||||
return *rule->literal != '\0' ? 1 : 0;
|
||||
}
|
||||
#if SS_ENABLE_REGEX
|
||||
// Bounds the backtracking a rule can do on a client-chosen host name;
|
||||
// made here, at startup, not on a first match.
|
||||
if (match_context == NULL) {
|
||||
match_context = pcre2_match_context_create(NULL);
|
||||
if (match_context == NULL) {
|
||||
return -1;
|
||||
}
|
||||
pcre2_set_match_limit(match_context, 100000);
|
||||
pcre2_set_depth_limit(match_context, 1000);
|
||||
}
|
||||
if (rule->pattern_re == NULL) {
|
||||
int errcode;
|
||||
PCRE2_SIZE erroffset;
|
||||
// Host names are case-insensitive.
|
||||
rule->pattern_re = pcre2_compile((PCRE2_SPTR)rule->pattern,
|
||||
PCRE2_ZERO_TERMINATED, 0, &errcode, &erroffset, NULL);
|
||||
PCRE2_ZERO_TERMINATED, PCRE2_CASELESS, &errcode, &erroffset, NULL);
|
||||
if (rule->pattern_re == NULL) {
|
||||
PCRE2_UCHAR errbuf[256];
|
||||
pcre2_get_error_message(errcode, errbuf, sizeof(errbuf));
|
||||
@@ -135,7 +150,7 @@ rule_matches(rule_t *rule, const char *name, size_t name_len)
|
||||
}
|
||||
#if SS_ENABLE_REGEX
|
||||
return pcre2_match(rule->pattern_re, (PCRE2_SPTR)name,
|
||||
name_len, 0, 0, rule->match_data, NULL) >= 0;
|
||||
name_len, 0, 0, rule->match_data, match_context) >= 0;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
@@ -148,6 +163,10 @@ lookup_rule(rule_list_t *rules, const char *name, size_t name_len)
|
||||
name = "";
|
||||
name_len = 0;
|
||||
}
|
||||
// "example.com." is the same name as "example.com".
|
||||
while (name_len > 0 && name[name_len - 1] == '.') {
|
||||
name_len--;
|
||||
}
|
||||
|
||||
return rules->find_if([&](rule_t &rule) { return rule_matches(&rule, name, name_len); });
|
||||
}
|
||||
|
||||
+31
-23
@@ -164,14 +164,22 @@ static ss::relay::Resolver *stat_resolver = NULL;
|
||||
static void stat_resolved(const ss::relay::ResolveCallback &cb, const sockaddr *address) noexcept
|
||||
{
|
||||
char *message = (char *)cb.ctx;
|
||||
// One socket for the process's life: ss-manager tells who sent a
|
||||
// datagram by the owner of its source socket, which must still exist
|
||||
// when the manager looks.
|
||||
static int stat_fd[2] = { -1, -1 }; // AF_INET, AF_INET6
|
||||
if (address) {
|
||||
int fd = ss_socket(address->sa_family, SOCK_DGRAM, 0);
|
||||
int &fd = stat_fd[address->sa_family == AF_INET6 ? 1 : 0];
|
||||
if (fd == -1) {
|
||||
fd = ss_socket(address->sa_family, SOCK_DGRAM, 0);
|
||||
if (fd != -1) {
|
||||
setnonblocking(fd);
|
||||
}
|
||||
}
|
||||
if (fd != -1) {
|
||||
setnonblocking(fd);
|
||||
size_t length = strlen(message) + 1;
|
||||
if (sendto(fd, message, length, 0, address, get_sockaddr_len((struct sockaddr *)address)) != (ssize_t)length)
|
||||
ERROR("stat_sendto");
|
||||
ss_socket_close(fd);
|
||||
}
|
||||
}
|
||||
free(message); // heap-ok: one stats datagram every few seconds
|
||||
@@ -180,7 +188,7 @@ static void stat_resolved(const ss::relay::ResolveCallback &cb, const sockaddr *
|
||||
static void
|
||||
stat_update_cb(void *) noexcept
|
||||
{
|
||||
struct sockaddr_un svaddr, claddr;
|
||||
struct sockaddr_un svaddr;
|
||||
int sfd = -1;
|
||||
size_t msgLen;
|
||||
char resp[SOCKET_BUF_SIZE];
|
||||
@@ -204,24 +212,13 @@ stat_update_cb(void *) noexcept
|
||||
return;
|
||||
}
|
||||
|
||||
memset(&claddr, 0, sizeof(struct sockaddr_un));
|
||||
claddr.sun_family = AF_UNIX;
|
||||
snprintf(claddr.sun_path, sizeof(claddr.sun_path), "/tmp/shadowsocks.%s", remote_port);
|
||||
|
||||
unlink(claddr.sun_path);
|
||||
|
||||
if (bind(sfd, (struct sockaddr *)&claddr, sizeof(struct sockaddr_un)) == -1) {
|
||||
ERROR("stat_bind");
|
||||
ss_socket_close(sfd);
|
||||
return;
|
||||
}
|
||||
|
||||
// The manager never answers a stat datagram, so the socket stays
|
||||
// unnamed: no fixed path in /tmp for anyone to race.
|
||||
memset(&svaddr, 0, sizeof(struct sockaddr_un));
|
||||
svaddr.sun_family = AF_UNIX;
|
||||
if (strlen(manager_addr) >= sizeof(svaddr.sun_path)) {
|
||||
LOGE("manager unix socket path is too long");
|
||||
ss_socket_close(sfd);
|
||||
unlink(claddr.sun_path);
|
||||
return;
|
||||
}
|
||||
strncpy(svaddr.sun_path, manager_addr, sizeof(svaddr.sun_path) - 1);
|
||||
@@ -229,11 +226,8 @@ stat_update_cb(void *) noexcept
|
||||
if (sendto(sfd, resp, strlen(resp) + 1, 0, (struct sockaddr *)&svaddr,
|
||||
sizeof(struct sockaddr_un)) != (ssize_t)msgLen) {
|
||||
ERROR("stat_sendto");
|
||||
ss_socket_close(sfd);
|
||||
return;
|
||||
}
|
||||
|
||||
unlink(claddr.sun_path);
|
||||
ss_socket_close(sfd);
|
||||
} else {
|
||||
uint16_t port;
|
||||
if (parse_numeric_port(ip_addr.port, &port) == 0) {
|
||||
@@ -1076,7 +1070,11 @@ Same behavior as `-d`; see that option for details. Short alias: `-d`.
|
||||
break;
|
||||
case GETOPT_VAL_ACL:
|
||||
LOGI("initializing acl...");
|
||||
acl = !init_acl(optarg);
|
||||
if (init_acl(optarg) != 0) {
|
||||
LOGE("failed to load the ACL file %s", optarg);
|
||||
FATAL("invalid ACL");
|
||||
}
|
||||
acl = 1;
|
||||
break;
|
||||
case GETOPT_VAL_MANAGER_ADDRESS:
|
||||
manager_addr = optarg;
|
||||
@@ -1317,7 +1315,11 @@ Same behavior as `-d`; see that option for details. Short alias: `-d`.
|
||||
}
|
||||
if (acl == 0 && conf->acl != NULL) {
|
||||
LOGI("initializing acl...");
|
||||
acl = !init_acl(conf->acl);
|
||||
if (init_acl(conf->acl) != 0) {
|
||||
LOGE("failed to load the ACL file %s", conf->acl);
|
||||
FATAL("invalid ACL");
|
||||
}
|
||||
acl = 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1523,6 +1525,7 @@ Same behavior as `-d`; see that option for details. Short alias: `-d`.
|
||||
len = strlen(server_str);
|
||||
}
|
||||
|
||||
set_plugin_user(user); // it drops root along with us
|
||||
int err = start_plugin(plugin, plugin_opts, server_str,
|
||||
plugin_port, plugin_host, server_port,
|
||||
#ifdef __MINGW32__
|
||||
@@ -1634,6 +1637,10 @@ Same behavior as `-d`; see that option for details. Short alias: `-d`.
|
||||
udp_config.label = remote_port != NULL ? remote_port : "";
|
||||
udp_config.tx = &tx;
|
||||
udp_config.rx = ℞
|
||||
udp_config.acl = acl != 0;
|
||||
udp_config.acl_hooks.match_host = &acl_match_host;
|
||||
udp_config.acl_hooks.mode = &get_acl_mode;
|
||||
udp_config.acl_hooks.outbound_blocked = &outbound_block_match_host;
|
||||
if (is_bind_local_addr) {
|
||||
udp_config.bind_v4 = &local_addr_v4;
|
||||
udp_config.bind_v6 = &local_addr_v6;
|
||||
@@ -1692,6 +1699,7 @@ Same behavior as `-d`; see that option for details. Short alias: `-d`.
|
||||
}
|
||||
#endif
|
||||
|
||||
daemon_ready();
|
||||
// start ev loop
|
||||
ss::heap_seal();
|
||||
{
|
||||
|
||||
@@ -31,10 +31,12 @@ int ss_local_start(const profile_t *profile, ss_local_callback callback, void *u
|
||||
void ss_local_stop(ss_local_handle *handle);
|
||||
|
||||
/* Wait for the server thread and return what the server returned (0 after
|
||||
* a requested stop). Call it once. */
|
||||
* a requested stop). Later or concurrent calls return the same value. */
|
||||
int ss_local_wait(ss_local_handle *handle);
|
||||
|
||||
/* Release the handle, stopping and waiting for the server first if needed. */
|
||||
/* Release the handle, stopping and waiting for the server first if needed.
|
||||
* The handle is freed: no other thread may still be calling stop or wait
|
||||
* on it, or call them afterwards. */
|
||||
void ss_local_destroy(ss_local_handle *handle);
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
@@ -11,6 +11,9 @@ struct ss_process;
|
||||
struct ss_process *ss_process_new(const char *program);
|
||||
void ss_process_arg(struct ss_process *process, const char *argument);
|
||||
void ss_process_env(struct ss_process *process, const char *name, const char *value);
|
||||
/* Run the child as `user` (a name or a numeric uid) when started as root.
|
||||
* Returns -1 if the user is unknown. A no-op on Windows. */
|
||||
int ss_process_user(struct ss_process *process, const char *user);
|
||||
int ss_process_start(struct ss_process *process, uint16_t control_port);
|
||||
bool ss_process_running(struct ss_process *process);
|
||||
void ss_process_free(struct ss_process *process);
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
#include <span>
|
||||
|
||||
#include "base64.h"
|
||||
#include "ssurl.h"
|
||||
@@ -195,6 +196,9 @@ parse_query(const char *s, size_t len, ss_url_t *url)
|
||||
if (name_len == 6 && strncmp(s, "plugin", 6) == 0) {
|
||||
char *value = percent_decode(eq + 1, (size_t)(pair_end - eq - 1));
|
||||
char *semi = strchr(value, ';');
|
||||
// A repeated plugin= replaces the earlier one, options too.
|
||||
ss_free(url->plugin);
|
||||
ss_free(url->plugin_opts);
|
||||
if (semi != NULL) {
|
||||
*semi = '\0';
|
||||
url->plugin_opts = str_ndup(semi + 1, strlen(semi + 1));
|
||||
@@ -248,6 +252,13 @@ ss_url_parse(const char *uri, ss_url_t *url)
|
||||
const char *hash = (const char *)memchr(body, '#', body_len);
|
||||
if (hash != NULL) {
|
||||
url->tag = percent_decode(hash + 1, body_len - (size_t)(hash - body) - 1);
|
||||
/* It is only logged: no control characters (newlines, escapes)
|
||||
* from a link may reach the log. */
|
||||
for (char &c : std::span<char>(url->tag, strlen(url->tag))) {
|
||||
if ((unsigned char)c < 0x20 || c == 0x7f) {
|
||||
c = '?';
|
||||
}
|
||||
}
|
||||
body_len = (size_t)(hash - body);
|
||||
}
|
||||
|
||||
@@ -315,6 +326,13 @@ ss_url_parse(const char *uri, ss_url_t *url)
|
||||
size_t query_ofst = (size_t)(query - body) + 1;
|
||||
parse_query(body + query_ofst, body_len - query_ofst, url);
|
||||
}
|
||||
/* A link is often shared: let it name a plugin, but not a path to an
|
||||
* arbitrary program, and no empty name. The plugin is looked up on PATH. */
|
||||
if (url->plugin != NULL && (url->plugin[0] == '\0' || strchr(url->plugin, '/') != NULL
|
||||
|| strchr(url->plugin, '\\') != NULL)) {
|
||||
LOGE("the URL's plugin must be a plain program name, not a path");
|
||||
goto fail;
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
|
||||
@@ -906,6 +906,7 @@ Same behavior as `-V`; see that option for details. Short alias: `-V`.
|
||||
snprintf(remote_str + len, buf_size - len, "|%s", remote_addr[i].host);
|
||||
len = strlen(remote_str);
|
||||
}
|
||||
set_plugin_user(user); // it drops root along with us
|
||||
int err = start_plugin(plugin, plugin_opts, remote_str,
|
||||
remote_port, plugin_host, plugin_port,
|
||||
#ifdef __MINGW32__
|
||||
@@ -1044,6 +1045,7 @@ Same behavior as `-V`; see that option for details. Short alias: `-V`.
|
||||
}
|
||||
#endif
|
||||
|
||||
daemon_ready();
|
||||
ss::heap_seal();
|
||||
{
|
||||
int code = program.run();
|
||||
|
||||
+74
-9
@@ -145,6 +145,11 @@ int
|
||||
run_as(const char *user)
|
||||
{
|
||||
#ifndef __MINGW32__
|
||||
if (user[0] == '\0') {
|
||||
/* An empty --user (say, an unset variable) must not keep root. */
|
||||
LOGE("run_as user is empty");
|
||||
return 0;
|
||||
}
|
||||
if (user[0]) {
|
||||
/* Convert user to a long integer if it is a non-negative number.
|
||||
* -1 means it is a user name. */
|
||||
@@ -174,6 +179,13 @@ run_as(const char *user)
|
||||
err = uid >= 0 ? getpwuid_r((uid_t)uid, &pwdbuf, buf, buflen, &pwd)
|
||||
: getpwnam_r(user, &pwdbuf, buf, buflen, &pwd);
|
||||
|
||||
if (err == 0 && pwd && getuid() == pwd->pw_uid && geteuid() == pwd->pw_uid
|
||||
&& getgid() == pwd->pw_gid) {
|
||||
/* Already that user (say, started by an ss-manager that has
|
||||
* switched to it): nothing to drop, and initgroups() would
|
||||
* need root. */
|
||||
break;
|
||||
}
|
||||
if (err == 0 && pwd) {
|
||||
/* setgid first, because we may not be allowed to do it anymore after setuid */
|
||||
if (setgid(pwd->pw_gid) != 0) {
|
||||
@@ -779,6 +791,25 @@ usage(void)
|
||||
puts("Manual: https://shadowsocks.github.io/shadowsocks-c/");
|
||||
}
|
||||
|
||||
#ifndef __MINGW32__
|
||||
static int daemon_ready_fd = -1;
|
||||
#endif
|
||||
|
||||
void
|
||||
daemon_ready(void)
|
||||
{
|
||||
#ifndef __MINGW32__
|
||||
if (daemon_ready_fd == -1) {
|
||||
return;
|
||||
}
|
||||
char byte = 1;
|
||||
ssize_t ignored = write(daemon_ready_fd, &byte, 1);
|
||||
(void)ignored;
|
||||
close(daemon_ready_fd);
|
||||
daemon_ready_fd = -1;
|
||||
#endif
|
||||
}
|
||||
|
||||
void
|
||||
daemonize(const char *path)
|
||||
{
|
||||
@@ -786,6 +817,29 @@ daemonize(const char *path)
|
||||
/* Our process ID and Session ID */
|
||||
pid_t pid, sid;
|
||||
|
||||
/* Open the pid file before forking, so a bad path fails the start
|
||||
* instead of leaving an untracked daemon, and never through a symlink
|
||||
* someone planted in a shared directory. */
|
||||
int flags = O_WRONLY | O_CREAT | O_TRUNC;
|
||||
#ifdef O_NOFOLLOW
|
||||
flags |= O_NOFOLLOW;
|
||||
#endif
|
||||
#ifdef O_CLOEXEC
|
||||
flags |= O_CLOEXEC;
|
||||
#endif
|
||||
int pid_fd = open(path, flags, 0644);
|
||||
if (pid_fd == -1) {
|
||||
LOGE("cannot open pid file %s: %s", path, strerror(errno));
|
||||
FATAL("Invalid pid file");
|
||||
}
|
||||
|
||||
/* The daemon says over this pipe when it is serving (daemon_ready());
|
||||
* the starting process exits with success only then. */
|
||||
int ready[2];
|
||||
if (pipe(ready) != 0) {
|
||||
FATAL("pipe");
|
||||
}
|
||||
|
||||
/* Fork off the parent process */
|
||||
pid = fork();
|
||||
if (pid < 0) {
|
||||
@@ -795,18 +849,29 @@ daemonize(const char *path)
|
||||
/* If we got a good PID, then
|
||||
* we can exit the parent process. */
|
||||
if (pid > 0) {
|
||||
FILE *file = fopen(path, "w");
|
||||
if (file == NULL) {
|
||||
FATAL("Invalid pid file\n");
|
||||
close(ready[1]);
|
||||
char text[32];
|
||||
int len = snprintf(text, sizeof(text), "%d", (int)pid);
|
||||
if (write(pid_fd, text, (size_t)len) != len) {
|
||||
ERROR("write pid file");
|
||||
}
|
||||
|
||||
fprintf(file, "%d", (int)pid);
|
||||
fclose(file);
|
||||
exit(EXIT_SUCCESS);
|
||||
close(pid_fd);
|
||||
char byte = 0;
|
||||
ssize_t n;
|
||||
do {
|
||||
n = read(ready[0], &byte, 1);
|
||||
} while (n < 0 && errno == EINTR);
|
||||
/* EOF: the daemon exited (or exec'd) before it was ready. */
|
||||
exit(n == 1 ? EXIT_SUCCESS : EXIT_FAILURE);
|
||||
}
|
||||
close(pid_fd);
|
||||
close(ready[0]);
|
||||
(void)fcntl(ready[1], F_SETFD, FD_CLOEXEC); /* plugins must not hold it open */
|
||||
daemon_ready_fd = ready[1];
|
||||
|
||||
/* Change the file mode mask */
|
||||
umask(0);
|
||||
/* Files the daemon creates (configs with passwords, control sockets)
|
||||
* must not be world-writable. */
|
||||
umask(022);
|
||||
|
||||
/* Open any logs here */
|
||||
|
||||
|
||||
@@ -226,6 +226,10 @@ void usage(void);
|
||||
void cli_version(void);
|
||||
void cli_error(const char *message, int option, const char *token) __attribute__((noreturn));
|
||||
void daemonize(const char *path);
|
||||
/* After daemonize(): the daemon is serving, so the starting process may
|
||||
* exit successfully (it fails if the daemon exits first). A no-op without
|
||||
* daemonize(). */
|
||||
void daemon_ready(void);
|
||||
char *ss_strndup(const char *s, size_t n);
|
||||
#ifdef HAVE_SETRLIMIT
|
||||
int set_nofile(int nofile);
|
||||
|
||||
@@ -398,6 +398,14 @@ if(SS_BUILD_EXECUTABLES AND NOT CMAKE_CROSSCOMPILING)
|
||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test_limits.py
|
||||
--bin ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
|
||||
set_tests_properties(test_limits PROPERTIES LABELS "integration" TIMEOUT 60)
|
||||
add_test(NAME test_server_acl
|
||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test_server_acl.py
|
||||
--bin ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
|
||||
set_tests_properties(test_server_acl PROPERTIES LABELS "integration" TIMEOUT 90)
|
||||
add_test(NAME test_half_close
|
||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test_half_close.py
|
||||
--bin ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
|
||||
set_tests_properties(test_half_close PROPERTIES LABELS "integration" TIMEOUT 90)
|
||||
add_test(NAME test_tunnel_interop
|
||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/tunnel_interop.py ${SS_TUNNEL_INTEROP_ARGS})
|
||||
set_tests_properties(test_tunnel_interop PROPERTIES LABELS "integration" TIMEOUT 300)
|
||||
|
||||
+71
-31
@@ -1,8 +1,9 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// ACL file parsing (src/acl.cpp): invalid rules are discarded, valid ones
|
||||
// still match.
|
||||
// ACL file parsing (src/acl.cpp): an invalid entry fails the whole load,
|
||||
// valid ones match, and IPv4-mapped addresses meet the IPv4 rules.
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <initializer_list>
|
||||
|
||||
#include "acl.h"
|
||||
#include "test_helpers.h"
|
||||
@@ -14,27 +15,73 @@ int verbose = 0;
|
||||
|
||||
namespace {
|
||||
|
||||
void test_invalid_regex_is_not_installed()
|
||||
// Writes `body` to a temporary ACL file and loads it.
|
||||
int load_acl(const char *body)
|
||||
{
|
||||
char path[4096];
|
||||
FILE *f = test_tempfile(path, sizeof(path));
|
||||
SS_CHECK(f != nullptr);
|
||||
std::fprintf(f, "[black_list]\n");
|
||||
std::fprintf(f, "[invalid\n");
|
||||
std::fprintf(f, "^blocked\\.example$\n");
|
||||
std::fprintf(f, "[outbound_block_list]\n");
|
||||
std::fprintf(f, "[also-invalid\n");
|
||||
std::fprintf(f, "^outbound\\.example$\n");
|
||||
std::fputs(body, f);
|
||||
std::fclose(f);
|
||||
int rc = init_acl(path);
|
||||
std::remove(path);
|
||||
return rc;
|
||||
}
|
||||
|
||||
SS_CHECK(init_acl(path) == 0);
|
||||
void test_invalid_regex_fails_the_load()
|
||||
{
|
||||
// An unusable rule must not silently drop out of a block list: the
|
||||
// whole file is refused and nothing stays installed.
|
||||
SS_CHECK(load_acl("[black_list]\n[invalid\n^blocked\\.example$\n") == -1);
|
||||
SS_CHECK(acl_match_host("blocked.example") == 0);
|
||||
SS_CHECK(load_acl("[outbound_block_list]\n^outbound\\.example$\n[also-invalid\n") == -1);
|
||||
SS_CHECK(outbound_block_match_host("outbound.example") == 0);
|
||||
SS_CHECK(get_acl_mode() == BLACK_LIST);
|
||||
|
||||
SS_CHECK(load_acl("[black_list]\n^blocked\\.example$\n[outbound_block_list]\n^outbound\\.example$\n") == 0);
|
||||
SS_CHECK(acl_match_host("does-not-crash.example") == 0);
|
||||
SS_CHECK(acl_match_host("blocked.example") == 1);
|
||||
SS_CHECK(outbound_block_match_host("does-not-crash.example") == 0);
|
||||
SS_CHECK(outbound_block_match_host("outbound.example") == 1);
|
||||
free_acl();
|
||||
}
|
||||
|
||||
std::remove(path);
|
||||
void test_reload_starts_from_nothing()
|
||||
{
|
||||
SS_CHECK(load_acl("[reject_all]\n[white_list]\n1.1.1.1\n") == 0);
|
||||
SS_CHECK(get_acl_mode() == WHITE_LIST);
|
||||
SS_CHECK(load_acl("[black_list]\n2.2.2.2\n") == 0);
|
||||
SS_CHECK(get_acl_mode() == BLACK_LIST);
|
||||
SS_CHECK(acl_match_host("1.1.1.1") == 0);
|
||||
SS_CHECK(acl_match_host("2.2.2.2") == 1);
|
||||
free_acl();
|
||||
}
|
||||
|
||||
void test_rules_ignore_case_and_trailing_dot()
|
||||
{
|
||||
SS_CHECK(load_acl("[outbound_block_list]\n(^|\\.)localhost$\nfull:internal.example\n") == 0);
|
||||
SS_CHECK(outbound_block_match_host("LOCALHOST") == 1);
|
||||
SS_CHECK(outbound_block_match_host("localhost.") == 1);
|
||||
SS_CHECK(outbound_block_match_host("Internal.Example.") == 1);
|
||||
SS_CHECK(outbound_block_match_host("example") == 0);
|
||||
free_acl();
|
||||
}
|
||||
|
||||
void test_ipv4_mapped_addresses_match_ipv4_rules()
|
||||
{
|
||||
SS_CHECK(load_acl("[outbound_block_list]\n127.0.0.0/8\n169.254.169.254\n::ffff:10.0.0.0/104\n") == 0);
|
||||
SS_CHECK(outbound_block_match_host("127.0.0.1") == 1);
|
||||
SS_CHECK(outbound_block_match_host("::ffff:127.0.0.1") == 1);
|
||||
SS_CHECK(outbound_block_match_host("::FFFF:169.254.169.254") == 1);
|
||||
SS_CHECK(outbound_block_match_host("10.1.2.3") == 1); // from the mapped /104
|
||||
SS_CHECK(outbound_block_match_host("::ffff:10.1.2.3") == 1);
|
||||
SS_CHECK(outbound_block_match_host("::ffff:8.8.8.8") == 0);
|
||||
SS_CHECK(outbound_block_match_host("::1") == 0);
|
||||
free_acl();
|
||||
|
||||
SS_CHECK(load_acl("[black_list]\n203.0.113.0/24\n") == 0);
|
||||
SS_CHECK(acl_match_host("::ffff:203.0.113.7") == 1); // a v4 client on a dual-stack socket
|
||||
free_acl();
|
||||
}
|
||||
|
||||
void test_long_acl_line_is_discarded()
|
||||
@@ -57,36 +104,29 @@ void test_long_acl_line_is_discarded()
|
||||
std::remove(path);
|
||||
}
|
||||
|
||||
void test_invalid_cidr_is_discarded()
|
||||
void test_invalid_cidr_fails_the_load()
|
||||
{
|
||||
char path[4096];
|
||||
FILE *f = test_tempfile(path, sizeof(path));
|
||||
SS_CHECK(f != nullptr);
|
||||
std::fprintf(f, "[black_list]\n");
|
||||
std::fprintf(f, "1.2.3.0/999\n");
|
||||
std::fprintf(f, "2.2.2.2/-1\n");
|
||||
std::fprintf(f, "3.3.3.3/abc\n");
|
||||
std::fprintf(f, "4.4.4.0/24\n");
|
||||
std::fprintf(f, "^path/with/slash\\.example$\n");
|
||||
std::fclose(f);
|
||||
|
||||
SS_CHECK(init_acl(path) == 0);
|
||||
SS_CHECK(acl_match_host("1.2.3.1") == 0);
|
||||
SS_CHECK(acl_match_host("2.2.2.2") == 0);
|
||||
SS_CHECK(acl_match_host("3.3.3.3") == 0);
|
||||
for (const char *line : { "1.2.3.0/999\n", "2.2.2.2/-1\n", "3.3.3.3/abc\n", "::1/129\n", "::ffff:1.2.3.4/64\n" }) {
|
||||
char body[64];
|
||||
std::snprintf(body, sizeof(body), "[black_list]\n4.4.4.0/24\n%s", line);
|
||||
SS_CHECK(load_acl(body) == -1);
|
||||
SS_CHECK(acl_match_host("4.4.4.8") == 0);
|
||||
}
|
||||
SS_CHECK(load_acl("[black_list]\n4.4.4.0/24\n^path/with/slash\\.example$\n") == 0);
|
||||
SS_CHECK(acl_match_host("4.4.4.8") == 1);
|
||||
SS_CHECK(acl_match_host("path/with/slash.example") == 1);
|
||||
free_acl();
|
||||
|
||||
std::remove(path);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main()
|
||||
{
|
||||
SS_RUN(test_invalid_regex_is_not_installed);
|
||||
SS_RUN(test_invalid_regex_fails_the_load);
|
||||
SS_RUN(test_reload_starts_from_nothing);
|
||||
SS_RUN(test_rules_ignore_case_and_trailing_dot);
|
||||
SS_RUN(test_ipv4_mapped_addresses_match_ipv4_rules);
|
||||
SS_RUN(test_long_acl_line_is_discarded);
|
||||
SS_RUN(test_invalid_cidr_is_discarded);
|
||||
SS_RUN(test_invalid_cidr_fails_the_load);
|
||||
return 0;
|
||||
}
|
||||
@@ -351,6 +351,41 @@ void udp_window()
|
||||
SS_CHECK(pkt && !sc::udp2022_accept(sc::Role::server, server, *pkt, out, std::time(nullptr) + 31));
|
||||
}
|
||||
|
||||
// A client keeps the replay window of the server session it heard from
|
||||
// before, so a server that starts a new session cannot have its old replies
|
||||
// replayed into a fresh window.
|
||||
void udp_server_session_change()
|
||||
{
|
||||
sc::Psk psk = psk_of("2022-blake3-aes-128-gcm");
|
||||
sc::UdpSession2022 client, server1, server2;
|
||||
std::uint8_t plain[sizeof(kAddr)];
|
||||
std::memcpy(plain, kAddr, sizeof(kAddr));
|
||||
std::vector<std::uint8_t> req(sizeof(plain) + sc::udp2022_overhead(psk, sc::Role::client));
|
||||
SS_CHECK(sc::udp2022_seal(psk, sc::Role::client, client, plain, req));
|
||||
for (sc::UdpSession2022 *server : { &server1, &server2 }) {
|
||||
std::vector<std::uint8_t> out(req.size());
|
||||
auto pkt = sc::udp2022_decrypt(psk, req, out);
|
||||
SS_CHECK(pkt && sc::udp2022_accept(sc::Role::server, *server, *pkt, out, std::time(nullptr)));
|
||||
}
|
||||
auto reply = [&](sc::UdpSession2022 &server) {
|
||||
std::vector<std::uint8_t> p(sizeof(plain) + sc::udp2022_overhead(psk, sc::Role::server));
|
||||
SS_CHECK(sc::udp2022_seal(psk, sc::Role::server, server, plain, p));
|
||||
return p;
|
||||
};
|
||||
auto deliver = [&](const std::vector<std::uint8_t> &p) {
|
||||
std::vector<std::uint8_t> out(p.size());
|
||||
auto pkt = sc::udp2022_decrypt(psk, p, out);
|
||||
SS_CHECK(pkt);
|
||||
return static_cast<bool>(sc::udp2022_accept(sc::Role::client, client, *pkt, out, std::time(nullptr)));
|
||||
};
|
||||
auto a1 = reply(server1), b1 = reply(server1), a2 = reply(server2);
|
||||
SS_CHECK(deliver(a1));
|
||||
SS_CHECK(deliver(a2)); // the server moved to a new session
|
||||
SS_CHECK(!deliver(a1)); // a replay of the old session is still caught
|
||||
SS_CHECK(deliver(b1)); // a late packet of the old session is not
|
||||
SS_CHECK(!deliver(a2) && !deliver(b1));
|
||||
}
|
||||
|
||||
void salt_pool()
|
||||
{
|
||||
sc::SaltPool pool;
|
||||
@@ -358,7 +393,11 @@ void salt_pool()
|
||||
std::uint8_t a[32] = { 1 }, b[32] = { 2 }, d[32] = { 3 };
|
||||
SS_CHECK(pool.check_and_add(a, 100) && !pool.check_and_add(a, 159));
|
||||
SS_CHECK(pool.check_and_add(a, 160)); // retention over
|
||||
SS_CHECK(pool.check_and_add(b, 161) && pool.check_and_add(d, 161) && pool.size() == 2);
|
||||
SS_CHECK(pool.check_and_add(b, 161) && pool.size() == 2);
|
||||
// Full of salts still inside the window: a new one is refused rather
|
||||
// than evicting one that could then be replayed.
|
||||
SS_CHECK(!pool.check_and_add(d, 161) && pool.size() == 2 && !pool.check_and_add(a, 161));
|
||||
SS_CHECK(pool.check_and_add(d, 221)); // a and b have expired
|
||||
}
|
||||
|
||||
void steady(const sc::Psk &psk, sc::SaltPool &pool)
|
||||
@@ -395,6 +434,7 @@ void test_methods()
|
||||
}
|
||||
tcp_rejects();
|
||||
udp_window();
|
||||
udp_server_session_change();
|
||||
salt_pool();
|
||||
SS_CHECK(!sc::find_aead2022("aes-256-gcm"));
|
||||
auto spec = sc::find_aead2022("2022-blake3-aes-256-gcm");
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env python3
|
||||
"""TCP half-close through ss-local and ss-server: a client that sends its
|
||||
request and then shuts down its write side still gets the whole response,
|
||||
and the origin sees the EOF."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import os
|
||||
from pathlib import Path
|
||||
import socket
|
||||
import socketserver
|
||||
import threading
|
||||
|
||||
from interop import free_port, peers, socks
|
||||
|
||||
|
||||
class ReplyAfterEOF(socketserver.BaseRequestHandler):
|
||||
"""Reads to EOF, then answers with the digest and a large body."""
|
||||
|
||||
def handle(self):
|
||||
self.request.settimeout(15)
|
||||
digest = hashlib.sha256()
|
||||
while chunk := self.request.recv(65536):
|
||||
digest.update(chunk)
|
||||
body = digest.digest() * 32768 # 1 MiB
|
||||
self.request.sendall(body)
|
||||
|
||||
|
||||
class Origin(socketserver.ThreadingTCPServer):
|
||||
daemon_threads = True
|
||||
allow_reuse_address = True
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--bin", required=True, type=Path)
|
||||
args = parser.parse_args()
|
||||
bins = args.bin.resolve()
|
||||
with Origin(("127.0.0.1", 0), ReplyAfterEOF) as origin:
|
||||
threading.Thread(target=origin.serve_forever, daemon=True).start()
|
||||
for method in ("aes-256-gcm", "2022-blake3-aes-128-gcm"):
|
||||
key = ["-k", "pw"] if not method.startswith("2022") else ["-k", "AAAAAAAAAAAAAAAAAAAAAA=="]
|
||||
sport, lport = free_port(), free_port()
|
||||
server = [str(bins / "ss-server"), "-s", "127.0.0.1", "-p", str(sport), "-m", method] + key
|
||||
local = [str(bins / "ss-local"), "-s", "127.0.0.1", "-p", str(sport), "-l", str(lport), "-m",
|
||||
method] + key
|
||||
with peers([server, local], [sport, lport]):
|
||||
sock, _ = socks(lport, 1, ("127.0.0.1", origin.server_address[1]))
|
||||
with sock:
|
||||
sock.settimeout(15)
|
||||
request = os.urandom(200000)
|
||||
sock.sendall(request)
|
||||
sock.shutdown(socket.SHUT_WR)
|
||||
received = bytearray()
|
||||
while chunk := sock.recv(65536):
|
||||
received += chunk
|
||||
expected = hashlib.sha256(request).digest() * 32768
|
||||
assert bytes(received) == expected, \
|
||||
f"{method}: got {len(received)} of {len(expected)} bytes after a half-close"
|
||||
print(f"PASS half-close: {method}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+93
-18
@@ -1,11 +1,13 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ss-manager end to end: add a server over the control socket, relay through
|
||||
it with ss-local, see its traffic in ping, list it, and remove it."""
|
||||
it with ss-local, see its traffic in ping, list it, and remove it; over UDP
|
||||
and over a unix socket."""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import socket
|
||||
import stat as statmode
|
||||
import subprocess
|
||||
import tempfile
|
||||
import threading
|
||||
@@ -21,6 +23,70 @@ def control(port, message, timeout=5):
|
||||
return sock.recvfrom(65535)[0].decode().rstrip("\0")
|
||||
|
||||
|
||||
def unix_control(path, message, timeout=5):
|
||||
with tempfile.TemporaryDirectory() as temp, socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) as sock:
|
||||
sock.bind(os.path.join(temp, "c"))
|
||||
sock.settimeout(timeout)
|
||||
sock.sendto(message.encode(), path)
|
||||
return sock.recvfrom(65535)[0].decode().rstrip("\0")
|
||||
|
||||
|
||||
def wait_closed(port, deadline=10):
|
||||
end = time.monotonic() + deadline
|
||||
while time.monotonic() < end:
|
||||
try:
|
||||
with socket.create_connection(("127.0.0.1", port), timeout=.2):
|
||||
time.sleep(.05)
|
||||
except OSError:
|
||||
return
|
||||
raise AssertionError(f"managed server on {port} kept running")
|
||||
|
||||
|
||||
def relay_and_count(bins, ask, server_port, local_port, origin):
|
||||
local = [str(bins / "ss-local"), "-s", "127.0.0.1", "-p", str(server_port), "-l", str(local_port),
|
||||
"-k", "secret", "-m", "aes-256-gcm"]
|
||||
with peers([local], [local_port]):
|
||||
sock, _ = socks(local_port, 1, ("127.0.0.1", origin.server_address[1]))
|
||||
payload = os.urandom(65536)
|
||||
with sock:
|
||||
sock.sendall(payload)
|
||||
assert receive(sock, len(payload)) == payload, "relay through the managed server failed"
|
||||
# ss-server reports traffic to the manager every few seconds.
|
||||
deadline = time.monotonic() + 15
|
||||
traffic = 0
|
||||
while time.monotonic() < deadline and traffic == 0:
|
||||
stat = json.loads(ask("ping")[len("stat: "):])
|
||||
traffic = stat.get(str(server_port), 0)
|
||||
time.sleep(.5)
|
||||
assert traffic > 0, "no traffic statistics from the managed server"
|
||||
|
||||
|
||||
def unix_socket_case(bins, origin):
|
||||
server_port, local_port = free_port(), free_port()
|
||||
with tempfile.TemporaryDirectory() as workdir:
|
||||
path = os.path.join(workdir, "m.sock")
|
||||
manager = subprocess.Popen(
|
||||
[str(bins / "ss-manager"), "--manager-address", path, "--executable", str(bins / "ss-server"),
|
||||
"-D", workdir, "-s", "127.0.0.1", "-m", "aes-256-gcm"],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
ask = lambda message: unix_control(path, message)
|
||||
try:
|
||||
for _ in range(50):
|
||||
if os.path.exists(path):
|
||||
break
|
||||
time.sleep(.1)
|
||||
# No authentication on the control socket: only its owner may use it.
|
||||
assert statmode.S_IMODE(os.stat(path).st_mode) == 0o600, oct(os.stat(path).st_mode)
|
||||
assert ask('add: {"server_port": %d, "password": "secret"}' % server_port) == "ok"
|
||||
wait_listening(server_port)
|
||||
relay_and_count(bins, ask, server_port, local_port, origin)
|
||||
assert ask('remove: {"server_port": %d}' % server_port) == "ok"
|
||||
finally:
|
||||
manager.terminate()
|
||||
out = manager.communicate(timeout=10)[0].decode(errors="replace")
|
||||
assert manager.returncode == 0, f"ss-manager exited with {manager.returncode}:\n{out}"
|
||||
|
||||
|
||||
def wait_listening(port, deadline=10):
|
||||
end = time.monotonic() + deadline
|
||||
while time.monotonic() < end:
|
||||
@@ -59,31 +125,40 @@ def main():
|
||||
reply = control(manager_port, 'add: {"server_port": %d, "password": "secret"}' % server_port)
|
||||
assert reply == "ok", reply
|
||||
wait_listening(server_port)
|
||||
local = [str(bins / "ss-local"), "-s", "127.0.0.1", "-p", str(server_port), "-l", str(local_port),
|
||||
"-k", "secret", "-m", "aes-256-gcm"]
|
||||
with peers([local], [local_port]):
|
||||
sock, _ = socks(local_port, 1, ("127.0.0.1", origin.server_address[1]))
|
||||
payload = os.urandom(65536)
|
||||
with sock:
|
||||
sock.sendall(payload)
|
||||
assert receive(sock, len(payload)) == payload, "relay through the managed server failed"
|
||||
# ss-server reports traffic to the manager every few seconds.
|
||||
deadline = time.monotonic() + 15
|
||||
traffic = 0
|
||||
while time.monotonic() < deadline and traffic == 0:
|
||||
stat = json.loads(control(manager_port, "ping")[len("stat: "):])
|
||||
traffic = stat.get(str(server_port), 0)
|
||||
time.sleep(.5)
|
||||
assert traffic > 0, "no traffic statistics from the managed server"
|
||||
conf = Path(workdir) / f".shadowsocks_{server_port}.conf"
|
||||
# The config holds the password.
|
||||
assert statmode.S_IMODE(conf.stat().st_mode) == 0o600, oct(conf.stat().st_mode)
|
||||
relay_and_count(bins, lambda message: control(manager_port, message), server_port, local_port, origin)
|
||||
listed = json.loads(control(manager_port, "list"))
|
||||
assert any(entry.get("server_port") == str(server_port) for entry in listed), listed
|
||||
assert control(manager_port, 'remove: {"server_port": %d}' % server_port) == "ok"
|
||||
assert json.loads(control(manager_port, "list")) == [], "server still listed after remove"
|
||||
assert not conf.exists(), "the config (and its password) outlived remove"
|
||||
# "0<port>" names the same server as "<port>".
|
||||
reply = control(manager_port, 'add: {"server_port": "0%d", "password": "secret"}' % server_port)
|
||||
assert reply == "ok", reply
|
||||
wait_listening(server_port)
|
||||
listed = json.loads(control(manager_port, "list"))
|
||||
assert [entry.get("server_port") for entry in listed] == [str(server_port)], listed
|
||||
assert control(manager_port, 'remove: {"server_port": %d}' % server_port) == "ok"
|
||||
wait_closed(server_port)
|
||||
# A per-server plugin names a program to run: not over UDP.
|
||||
reply = control(manager_port,
|
||||
'add: {"server_port": %d, "password": "x", "plugin": "/bin/true"}' % server_port)
|
||||
assert reply != "ok", "a plugin was accepted over the UDP control channel"
|
||||
assert json.loads(control(manager_port, "list")) == []
|
||||
finally:
|
||||
manager.terminate()
|
||||
out = manager.communicate(timeout=10)[0].decode(errors="replace")
|
||||
assert manager.returncode == 0, f"ss-manager exited with {manager.returncode}:\n{out}"
|
||||
print("PASS ss-manager add/ping/list/remove with traffic statistics")
|
||||
unix_socket_case(bins, origin)
|
||||
# Over IP the unauthenticated protocol is loopback-only.
|
||||
refused = subprocess.run(
|
||||
[str(bins / "ss-manager"), "--manager-address", f"0.0.0.0:{free_port()}",
|
||||
"--executable", str(bins / "ss-server"), "-D", workdir, "-m", "aes-256-gcm"],
|
||||
capture_output=True, timeout=10)
|
||||
assert refused.returncode != 0, "ss-manager listened on a non-loopback address"
|
||||
print("PASS ss-manager add/ping/list/remove with traffic statistics (UDP and unix socket)")
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
@@ -222,6 +222,7 @@ SERIAL_LOG="$WORK_DIR/serial.log"
|
||||
|
||||
qemu-system-x86_64 \
|
||||
$KVM_FLAG \
|
||||
-cpu max \
|
||||
-m 512 \
|
||||
-kernel "$WORK_DIR/vmlinuz" \
|
||||
-initrd "$WORK_DIR/initramfs.gz" \
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ss-server --acl: the outbound block list stops TCP and UDP alike, also
|
||||
for an IPv4-mapped IPv6 target, and an idle handshake does not hold a
|
||||
connection slot past --timeout."""
|
||||
import argparse
|
||||
import subprocess
|
||||
import os
|
||||
from pathlib import Path
|
||||
import socket
|
||||
import struct
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
|
||||
from interop import TCPHandler, TCPOrigin, UDPHandler, UDPOrigin, free_port, peers, receive, socks
|
||||
|
||||
|
||||
def tcp_reaches(lport, host, port):
|
||||
try:
|
||||
sock, _ = socks(lport, 1, (host, port))
|
||||
except (OSError, AssertionError):
|
||||
return False
|
||||
with sock:
|
||||
sock.settimeout(3)
|
||||
try:
|
||||
sock.sendall(b"ping")
|
||||
return receive(sock, 4) == b"ping"
|
||||
except (OSError, AssertionError):
|
||||
return False
|
||||
|
||||
|
||||
def udp_reaches(lport, port):
|
||||
control, relay = socks(lport, 3, ("0.0.0.0", 0))
|
||||
host, rport = relay
|
||||
if host in ("0.0.0.0", "::"):
|
||||
host = "127.0.0.1"
|
||||
with control, socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
|
||||
sock.settimeout(2)
|
||||
payload = os.urandom(32)
|
||||
header = b"\x00\x00\x00\x01" + socket.inet_aton("127.0.0.1") + struct.pack("!H", port)
|
||||
sock.sendto(header + payload, (host, rport))
|
||||
try:
|
||||
reply, _ = sock.recvfrom(65535)
|
||||
except (TimeoutError, socket.timeout):
|
||||
return False
|
||||
return reply.endswith(payload)
|
||||
|
||||
|
||||
def run(bins, acl, check):
|
||||
sport, lport = free_port(), free_port()
|
||||
server = [str(bins / "ss-server"), "-s", "127.0.0.1", "-p", str(sport), "-k", "pw", "-m", "aes-256-gcm", "-u",
|
||||
"-t", "2", "--max-connections", "4"]
|
||||
if acl is not None:
|
||||
server += ["--acl", acl]
|
||||
local = [str(bins / "ss-local"), "-s", "127.0.0.1", "-p", str(sport), "-l", str(lport), "-k", "pw",
|
||||
"-m", "aes-256-gcm", "-u"]
|
||||
with peers([server, local], [sport, lport]):
|
||||
check(sport, lport)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--bin", required=True, type=Path)
|
||||
args = parser.parse_args()
|
||||
bins = args.bin.resolve()
|
||||
with TCPOrigin(("127.0.0.1", 0), TCPHandler) as tcp_origin, UDPOrigin(("127.0.0.1", 0), UDPHandler) as udp_origin, \
|
||||
tempfile.TemporaryDirectory(prefix="ss-acl-") as temp:
|
||||
threading.Thread(target=tcp_origin.serve_forever, daemon=True).start()
|
||||
threading.Thread(target=udp_origin.serve_forever, daemon=True).start()
|
||||
tport, uport = tcp_origin.server_address[1], udp_origin.server_address[1]
|
||||
acl = Path(temp) / "block.acl"
|
||||
acl.write_text("[outbound_block_list]\n127.0.0.0/8\n")
|
||||
mapped = {}
|
||||
|
||||
def open_relay(_, lport):
|
||||
assert tcp_reaches(lport, "127.0.0.1", tport), "TCP relay failed without an ACL"
|
||||
assert udp_reaches(lport, uport), "UDP relay failed without an ACL"
|
||||
# Whether this host connects ::ffff:127.0.0.1 at all (dual stack).
|
||||
mapped["works"] = tcp_reaches(lport, "::ffff:127.0.0.1", tport)
|
||||
|
||||
def blocked(sport, lport):
|
||||
assert not tcp_reaches(lport, "127.0.0.1", tport), "TCP reached a blocked address"
|
||||
if mapped["works"]:
|
||||
assert not tcp_reaches(lport, "::ffff:127.0.0.1", tport), \
|
||||
"TCP reached a blocked address through its IPv4-mapped form"
|
||||
assert not udp_reaches(lport, uport), "UDP reached a blocked address"
|
||||
# Sockets that never finish a handshake are closed after -t 2,
|
||||
# so they cannot hold the four slots.
|
||||
idle = [socket.create_connection(("127.0.0.1", sport)) for _ in range(4)]
|
||||
for sock in idle:
|
||||
sock.sendall(b"x")
|
||||
time.sleep(4)
|
||||
for sock in idle:
|
||||
sock.settimeout(2)
|
||||
try:
|
||||
assert sock.recv(1) == b"", "an idle handshake outlived --timeout"
|
||||
except ConnectionResetError:
|
||||
pass
|
||||
sock.close()
|
||||
|
||||
run(bins, None, open_relay)
|
||||
run(bins, str(acl), blocked)
|
||||
|
||||
# An ACL that cannot be loaded stops the server instead of running
|
||||
# it without the block list.
|
||||
bad = Path(temp) / "bad.acl"
|
||||
bad.write_text("[outbound_block_list]\n[unterminated\n")
|
||||
result = subprocess.run([str(bins / "ss-server"), "-s", "127.0.0.1", "-p", str(free_port()), "-k", "pw",
|
||||
"-m", "aes-256-gcm", "--acl", str(bad)], capture_output=True, timeout=10)
|
||||
assert result.returncode != 0, "ss-server started with an invalid ACL"
|
||||
print("PASS ss-server ACL (TCP, IPv4-mapped, UDP), handshake timeout, invalid ACL"
|
||||
+ ("" if mapped.get("works") else " (no dual-stack: mapped case skipped)"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,6 +1,8 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// ss:// URI parsing (src/ssurl.cpp): SIP002, the legacy form and malformed
|
||||
// input.
|
||||
#include <cstdio>
|
||||
#include <initializer_list>
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
|
||||
@@ -60,6 +62,25 @@ void test_sip002_plugin()
|
||||
ss_url_free(&u);
|
||||
}
|
||||
|
||||
/* A shared link may name a plugin but not a path to a program; a repeated
|
||||
* plugin= replaces the first; the tag cannot carry control characters. */
|
||||
void test_sip002_plugin_hardening()
|
||||
{
|
||||
ss_url_t u;
|
||||
const char *base = "ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ@192.168.100.1:8888/?plugin=";
|
||||
char url[256];
|
||||
for (const char *bad : { "%2Ftmp%2Fevil", "..%2Fevil", "C:%5Cevil.exe", "", "%3Bopts" }) {
|
||||
std::snprintf(url, sizeof(url), "%s%s", base, bad);
|
||||
SS_CHECK(ss_url_parse(url, &u) == -1);
|
||||
}
|
||||
std::snprintf(url, sizeof(url), "%s%s", base, "first%3Ba&plugin=second%3Bb#line%0Abreak%1B");
|
||||
SS_CHECK(ss_url_parse(url, &u) == 0);
|
||||
SS_CHECK(streq(u.plugin, "second"));
|
||||
SS_CHECK(streq(u.plugin_opts, "b"));
|
||||
SS_CHECK(streq(u.tag, "line?break?"));
|
||||
ss_url_free(&u);
|
||||
}
|
||||
|
||||
/* The trailing slash before a query is allowed, and so is no query at all. */
|
||||
void test_sip002_trailing_slash()
|
||||
{
|
||||
@@ -212,6 +233,7 @@ int main()
|
||||
SS_RUN(test_sip002_basic);
|
||||
SS_RUN(test_sip002_tag);
|
||||
SS_RUN(test_sip002_plugin);
|
||||
SS_RUN(test_sip002_plugin_hardening);
|
||||
SS_RUN(test_sip002_trailing_slash);
|
||||
SS_RUN(test_sip002_ipv6);
|
||||
SS_RUN(test_sip002_plain_userinfo);
|
||||
|
||||
Reference in new issue
Block a user