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:
Max LvandClaude authored and GitHub committed 2026-10-28 09:34:36 +08:00
1 parent db861130d7
commit 50f1ae54f7
47 files changed
+1601 -249

No files matched your search

+1 -1
View File
@@ -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
View File
@@ -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}
+4
View File
@@ -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.
+20
View File
@@ -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
View File
@@ -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
View File
@@ -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;
+2
View File
@@ -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
View File
@@ -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);
}
+12
View File
@@ -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
View File
@@ -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
+19
View File
@@ -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
View File
@@ -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());
}
+6 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
+12
View File
@@ -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
+3
View File
@@ -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();
+1
View File
@@ -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
View File
@@ -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)
{
+2
View File
@@ -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
View File
@@ -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;
}
+4
View File
@@ -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
View File
@@ -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);
+2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 = &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();
{
+4 -2
View File
@@ -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
+3
View File
@@ -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);
+18
View File
@@ -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;
+2
View File
@@ -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
View File
@@ -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 */
+4
View File
@@ -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);
+8
View File
@@ -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
View File
@@ -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;
}
+41 -1
View File
@@ -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");
+63
View File
@@ -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
View File
@@ -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
+1
View File
@@ -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" \
+116
View File
@@ -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()
+22
View File
@@ -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);