feat(local-http): support HTTP Basic auth (#1994)

This commit is contained in:
zonyitoo
2025-12-19 03:26:17 +08:00
parent d5b08c02c0
commit 1008eeec1d
31 changed files with 540 additions and 203 deletions
Generated
+1
View File
@@ -3120,6 +3120,7 @@ name = "shadowsocks-service"
version = "1.24.0"
dependencies = [
"arc-swap",
"base64",
"brotli",
"bson",
"byte_string",
+22 -1
View File
@@ -614,7 +614,10 @@ Example configuration:
"local_address": "127.0.0.1",
"local_port": 3128,
// OPTIONAL. macOS launchd activate socket
"launchd_tcp_socket_name": "TCPListener"
"launchd_tcp_socket_name": "TCPListener",
// OPTIONAL. Authentication configuration file
// Configuration file document could be found in the next section.
"http_auth_config_path": "/path/to/auth.json",
},
{
// DNS local server (feature = "local-dns")
@@ -955,6 +958,24 @@ The configuration file is set by `socks5_auth_config_path` in `locals`.
}
```
### HTTP Authentication Configuration
The configuration file is set by `http_auth_config_path` in `locals`.
```jsonc
{
// Basic Authentication (RFC9110)
"basic": {
"users": [
{
"user_name": "USERNAME in UTF-8",
"password": "PASSWORD in UTF-8"
}
]
}
}
```
### Environment Variables
- `SS_SERVER_PASSWORD`: A default password for servers that created from command line argument (`--server-addr`)
+8 -7
View File
@@ -19,14 +19,15 @@ fn main() -> ExitCode {
// Allow running `ssservice` as symlink of `sslocal`, `ssserver` and `ssmanager`
if let Some(program_path) = env::args().next()
&& let Some(program_name) = Path::new(&program_path).file_name() {
match program_name.to_str() {
Some("sslocal") => return local::main(&local::define_command_line_options(app).get_matches()),
Some("ssserver") => return server::main(&server::define_command_line_options(app).get_matches()),
Some("ssmanager") => return manager::main(&manager::define_command_line_options(app).get_matches()),
_ => {}
}
&& let Some(program_name) = Path::new(&program_path).file_name()
{
match program_name.to_str() {
Some("sslocal") => return local::main(&local::define_command_line_options(app).get_matches()),
Some("ssserver") => return server::main(&server::define_command_line_options(app).get_matches()),
Some("ssmanager") => return manager::main(&manager::define_command_line_options(app).get_matches()),
_ => {}
}
}
let matches = app
.subcommand_required(true)
+2 -1
View File
@@ -60,7 +60,7 @@ local-dns-relay = ["local-dns"]
# Currently is only used in Android
local-flow-stat = ["local"]
# Enable HTTP protocol for sslocal
local-http = ["local", "hyper", "http", "http-body-util"]
local-http = ["local", "hyper", "http", "http-body-util", "base64"]
local-http-native-tls = ["local-http", "tokio-native-tls", "native-tls"]
local-http-native-tls-vendored = [
"local-http-native-tls",
@@ -179,6 +179,7 @@ mime = { version = "0.3", optional = true }
flate2 = { version = "1.0", optional = true }
brotli = { version = "8.0", optional = true }
zstd = { version = "0.13", optional = true }
base64 = { version = "0.22", optional = true }
etherparse = { version = "0.19", optional = true }
smoltcp = { version = "0.12", optional = true, default-features = false, features = [
+7 -6
View File
@@ -209,13 +209,14 @@ impl ParsingRules {
}
}
} else if let Some(set_rule) = caps.get(2)
&& let Ok(set_rule) = str::from_utf8(set_rule.as_bytes()) {
let set_rule = set_rule.replace("\\.", ".");
if self.add_set_rule_inner(&set_rule).is_ok() {
trace!("REGEX-RULE {} => SET-RULE {}", rule, set_rule);
return;
}
&& let Ok(set_rule) = str::from_utf8(set_rule.as_bytes())
{
let set_rule = set_rule.replace("\\.", ".");
if self.add_set_rule_inner(&set_rule).is_ok() {
trace!("REGEX-RULE {} => SET-RULE {}", rule, set_rule);
return;
}
}
}
trace!("REGEX-RULE {}", rule);
+69 -40
View File
@@ -80,6 +80,8 @@ use shadowsocks::{
use crate::acl::AccessControl;
#[cfg(feature = "local-dns")]
use crate::local::dns::NameServerAddr;
#[cfg(feature = "local-http")]
use crate::local::http::config::HttpAuthConfig;
#[cfg(feature = "local")]
use crate::local::socks::config::Socks5AuthConfig;
@@ -323,6 +325,11 @@ struct SSLocalExtConfig {
#[serde(skip_serializing_if = "Option::is_none")]
socks5_auth_config_path: Option<String>,
/// HTTP
#[cfg(feature = "local")]
#[serde(skip_serializing_if = "Option::is_none")]
http_auth_config_path: Option<String>,
/// Fake DNS
#[cfg(feature = "local-fake-dns")]
#[serde(skip_serializing_if = "Option::is_none")]
@@ -1038,6 +1045,10 @@ pub struct LocalConfig {
#[cfg(feature = "local")]
pub socks5_auth: Socks5AuthConfig,
/// HTTP Authentication configuration
#[cfg(feature = "local")]
pub http_auth: HttpAuthConfig,
/// Fake DNS record expire seconds
#[cfg(feature = "local-fake-dns")]
pub fake_dns_record_expire_duration: Option<Duration>,
@@ -1108,6 +1119,9 @@ impl LocalConfig {
#[cfg(feature = "local")]
socks5_auth: Socks5AuthConfig::default(),
#[cfg(feature = "local-http")]
http_auth: HttpAuthConfig::default(),
#[cfg(feature = "local-fake-dns")]
fake_dns_record_expire_duration: None,
#[cfg(feature = "local-fake-dns")]
@@ -1832,6 +1846,11 @@ impl Config {
local_config.socks5_auth = Socks5AuthConfig::load_from_file(&socks5_auth_config_path)?;
}
#[cfg(feature = "local-http")]
if let Some(http_auth_config_path) = local.http_auth_config_path {
local_config.http_auth = HttpAuthConfig::load_from_file(&http_auth_config_path)?;
}
#[cfg(feature = "local-fake-dns")]
{
if let Some(d) = local.fake_dns_record_expire_duration {
@@ -2390,15 +2409,16 @@ impl Config {
// Security
if let Some(sec) = config.security
&& let Some(replay_attack) = sec.replay_attack
&& let Some(policy) = replay_attack.policy {
match policy.parse::<ReplayAttackPolicy>() {
Ok(p) => nconfig.security.replay_attack.policy = p,
Err(..) => {
let err = Error::new(ErrorKind::Invalid, "invalid replay attack policy", None);
return Err(err);
}
}
&& let Some(policy) = replay_attack.policy
{
match policy.parse::<ReplayAttackPolicy>() {
Ok(p) => nconfig.security.replay_attack.policy = p,
Err(..) => {
let err = Error::new(ErrorKind::Invalid, "invalid replay attack policy", None);
return Err(err);
}
}
}
if let Some(balancer) = config.balancer {
nconfig.balancer = BalancerConfig {
@@ -2619,16 +2639,18 @@ impl Config {
// Balancer related checks
if let Some(rtt) = self.balancer.max_server_rtt
&& rtt.as_secs() == 0 {
let err = Error::new(ErrorKind::Invalid, "balancer.max_server_rtt must be > 0", None);
return Err(err);
}
&& rtt.as_secs() == 0
{
let err = Error::new(ErrorKind::Invalid, "balancer.max_server_rtt must be > 0", None);
return Err(err);
}
if let Some(intv) = self.balancer.check_interval
&& intv.as_secs() == 0 {
let err = Error::new(ErrorKind::Invalid, "balancer.check_interval must be > 0", None);
return Err(err);
}
&& intv.as_secs() == 0
{
let err = Error::new(ErrorKind::Invalid, "balancer.check_interval must be > 0", None);
return Err(err);
}
}
if self.config_type.is_server() && self.server.is_empty() {
@@ -2664,10 +2686,11 @@ impl Config {
// Plugin shouldn't be an empty string
if let Some(plugin) = server.plugin()
&& plugin.plugin.trim().is_empty() {
let err = Error::new(ErrorKind::Malformed, "`plugin` shouldn't be an empty string", None);
return Err(err);
}
&& plugin.plugin.trim().is_empty()
{
let err = Error::new(ErrorKind::Malformed, "`plugin` shouldn't be an empty string", None);
return Err(err);
}
// Server's domain name shouldn't be an empty string
match server.addr() {
@@ -2899,6 +2922,9 @@ impl fmt::Display for Config {
#[cfg(feature = "local")]
socks5_auth_config_path: None,
#[cfg(feature = "local-http")]
http_auth_config_path: None,
#[cfg(feature = "local-fake-dns")]
fake_dns_record_expire_duration: local.fake_dns_record_expire_duration.map(|d| d.as_secs()),
#[cfg(feature = "local-fake-dns")]
@@ -3068,20 +3094,22 @@ impl fmt::Display for Config {
}
if jconf.method.is_none()
&& let Some(ref m) = m.method {
jconf.method = Some(m.to_string());
}
&& let Some(ref m) = m.method
{
jconf.method = Some(m.to_string());
}
if jconf.plugin.is_none()
&& let Some(ref p) = m.plugin {
jconf.plugin = Some(p.plugin.clone());
if let Some(ref o) = p.plugin_opts {
jconf.plugin_opts = Some(o.clone());
}
if !p.plugin_args.is_empty() {
jconf.plugin_args = Some(p.plugin_args.clone());
}
&& let Some(ref p) = m.plugin
{
jconf.plugin = Some(p.plugin.clone());
if let Some(ref o) = p.plugin_opts {
jconf.plugin_opts = Some(o.clone());
}
if !p.plugin_args.is_empty() {
jconf.plugin_args = Some(p.plugin_args.clone());
}
}
}
if self.no_delay {
@@ -3188,17 +3216,18 @@ impl fmt::Display for Config {
/// It will return the original value if fails to read `${VAR_NAME}`.
pub fn read_variable_field_value(value: &str) -> Cow<'_, str> {
if let Some(left_over) = value.strip_prefix("${")
&& let Some(var_name) = left_over.strip_suffix('}') {
match env::var(var_name) {
Ok(value) => return value.into(),
Err(err) => {
warn!(
"couldn't read password from environment variable {}, error: {}",
var_name, err
);
}
&& let Some(var_name) = left_over.strip_suffix('}')
{
match env::var(var_name) {
Ok(value) => return value.into(),
Err(err) => {
warn!(
"couldn't read password from environment variable {}, error: {}",
var_name, err
);
}
}
}
value.into()
}
@@ -0,0 +1,172 @@
//! HTTP protocol configuration
use std::{
collections::HashMap,
fs::OpenOptions,
io::{self, Read},
path::Path,
};
use base64::Engine as _;
use log::trace;
use serde::Deserialize;
const BASIC_AUTH_BASE64_ENGINE: base64::engine::GeneralPurpose = base64::engine::GeneralPurpose::new(
&base64::alphabet::STANDARD,
base64::engine::GeneralPurposeConfig::new()
.with_encode_padding(true)
.with_decode_padding_mode(base64::engine::DecodePaddingMode::Indifferent),
);
#[derive(Deserialize, Debug)]
struct SSHttpAuthBasicUserConfig {
user_name: String,
password: String,
}
#[derive(Deserialize, Debug)]
struct SSHttpAuthBasicConfig {
users: Vec<SSHttpAuthBasicUserConfig>,
}
#[derive(Deserialize, Debug)]
struct SSHttpAuthConfig {
#[serde(skip_serializing_if = "Option::is_none")]
basic: Option<SSHttpAuthBasicConfig>,
}
/// HTTP Authentication method
#[derive(Debug, Clone)]
pub struct HttpAuthConfig {
pub basic: HttpAuthBasicConfig,
}
impl HttpAuthConfig {
/// Create a new HTTP Authentication configuration
pub fn new() -> Self {
Self {
basic: HttpAuthBasicConfig::new(),
}
}
/// Load from configuration file
///
/// ```json
/// {
/// "basic": {
/// "users": [
/// {
/// "user_name": "USER_NAME",
/// "password": "PASSWORD"
/// }
/// ]
/// }
/// }
pub fn load_from_file<P: AsRef<Path> + ?Sized>(filename: &P) -> io::Result<Self> {
let filename = filename.as_ref();
trace!(
"loading socks5 authentication configuration from {}",
filename.display()
);
let mut reader = OpenOptions::new().read(true).open(filename)?;
let mut content = String::new();
reader.read_to_string(&mut content)?;
let jconf: SSHttpAuthConfig = match json5::from_str(&content) {
Ok(c) => c,
Err(err) => return Err(io::Error::other(err)),
};
let mut basic = HttpAuthBasicConfig::new();
if let Some(p) = jconf.basic {
for user in p.users {
basic.add_user(user.user_name, user.password);
}
}
Ok(Self { basic })
}
/// Check if authentication is required
pub fn auth_required(&self) -> bool {
self.basic.total_users() > 0
}
/// Check Basic Authentication
pub fn verify_basic_auth(&self, header_value: &str) -> bool {
const BASIC_PREFIX: &str = "Basic ";
if !header_value.starts_with(BASIC_PREFIX) {
return false;
}
let b64_encoded = &header_value[BASIC_PREFIX.len()..];
let decoded_bytes = match BASIC_AUTH_BASE64_ENGINE.decode(b64_encoded) {
Ok(b) => b,
Err(_) => return false,
};
let decoded_str = match String::from_utf8(decoded_bytes) {
Ok(s) => s,
Err(_) => return false,
};
let (user_name, password) = match decoded_str.split_once(':') {
Some((u, p)) => (u, p),
None => return false,
};
self.basic.check_user(user_name, password)
}
}
impl Default for HttpAuthConfig {
fn default() -> Self {
Self::new()
}
}
/// HTTP server User/Password Authentication configuration
///
/// RFC9110 https://httpwg.org/specs/rfc9110.html#auth.client.proxy
#[derive(Debug, Clone)]
pub struct HttpAuthBasicConfig {
users: HashMap<String, String>,
}
impl HttpAuthBasicConfig {
/// Create an empty `Passwd` configuration
pub fn new() -> Self {
Self { users: HashMap::new() }
}
/// Add a user with password
pub fn add_user<U, P>(&mut self, user_name: U, password: P)
where
U: Into<String>,
P: Into<String>,
{
self.users.insert(user_name.into(), password.into());
}
/// Check if `user_name` exists and validate `password`
pub fn check_user<U, P>(&self, user_name: U, password: P) -> bool
where
U: AsRef<str>,
P: AsRef<str>,
{
match self.users.get(user_name.as_ref()) {
Some(pwd) => pwd == password.as_ref(),
None => false,
}
}
/// Total users
pub fn total_users(&self) -> usize {
self.users.len()
}
}
impl Default for HttpAuthBasicConfig {
fn default() -> Self {
Self::new()
}
}
@@ -21,6 +21,7 @@ use crate::local::{
};
use super::{
config::HttpAuthConfig,
http_client::HttpClient,
utils::{authority_addr, check_keep_alive, connect_host, host_addr},
};
@@ -30,6 +31,7 @@ pub struct HttpService {
peer_addr: SocketAddr,
http_client: HttpClient<body::Incoming>,
balancer: PingBalancer,
http_auth: Arc<HttpAuthConfig>,
}
impl HttpService {
@@ -38,12 +40,14 @@ impl HttpService {
peer_addr: SocketAddr,
http_client: HttpClient<body::Incoming>,
balancer: PingBalancer,
http_auth: Arc<HttpAuthConfig>,
) -> Self {
Self {
context,
peer_addr,
http_client,
balancer,
http_auth,
}
}
@@ -53,6 +57,31 @@ impl HttpService {
) -> hyper::Result<Response<BoxBody<Bytes, hyper::Error>>> {
trace!("request {} {:?}", self.peer_addr, req);
if self.http_auth.auth_required() {
// Only support Basic authentication for now
match req.headers().get(header::PROXY_AUTHORIZATION) {
Some(header_value) => {
if let Ok(header_str) = header_value.to_str() {
if !self.http_auth.verify_basic_auth(header_str) {
debug!("HTTP {} invalid proxy authorization: {}", req.method(), header_str);
return make_proxy_authentication_required();
}
} else {
debug!(
"HTTP {} invalid proxy authorization header encoding: {:?}",
req.method(),
header_value
);
return make_proxy_authentication_required();
}
}
None => {
debug!("HTTP {} missing proxy authorization", req.method());
return make_proxy_authentication_required();
}
}
}
// Parse URI
//
// Proxy request URI must contains a host
@@ -204,6 +233,14 @@ fn make_bad_request() -> Result<Response<BoxBody<Bytes, hyper::Error>>, hyper::E
.unwrap())
}
fn make_proxy_authentication_required() -> Result<Response<BoxBody<Bytes, hyper::Error>>, hyper::Error> {
Ok(Response::builder()
.status(StatusCode::PROXY_AUTHENTICATION_REQUIRED)
.header(header::PROXY_AUTHENTICATE, "Basic charset=\"UTF-8\"")
.body(empty_body())
.unwrap())
}
fn make_internal_server_error() -> Result<Response<BoxBody<Bytes, hyper::Error>>, hyper::Error> {
Ok(Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
@@ -7,6 +7,7 @@ pub use self::{
server::{Http, HttpBuilder, HttpConnectionHandler},
};
pub mod config;
mod http_client;
mod http_service;
mod http_stream;
@@ -16,7 +16,7 @@ use crate::local::{
context::ServiceContext, loadbalancing::PingBalancer, net::tcp::listener::create_standard_tcp_listener,
};
use super::{http_client::HttpClient, http_service::HttpService, tokio_rt::TokioIo};
use super::{config::HttpAuthConfig, http_client::HttpClient, http_service::HttpService, tokio_rt::TokioIo};
/// HTTP Local server builder
pub struct HttpBuilder {
@@ -25,6 +25,7 @@ pub struct HttpBuilder {
balancer: PingBalancer,
#[cfg(target_os = "macos")]
launchd_tcp_socket_name: Option<String>,
http_auth: HttpAuthConfig,
}
impl HttpBuilder {
@@ -42,6 +43,7 @@ impl HttpBuilder {
balancer,
#[cfg(target_os = "macos")]
launchd_tcp_socket_name: None,
http_auth: HttpAuthConfig::default(),
}
}
@@ -50,6 +52,10 @@ impl HttpBuilder {
self.launchd_tcp_socket_name = Some(n);
}
pub fn set_http_auth(&mut self, auth: HttpAuthConfig) {
self.http_auth = auth;
}
/// Build HTTP server instance
pub async fn build(self) -> io::Result<Http> {
cfg_if::cfg_if! {
@@ -77,6 +83,7 @@ impl HttpBuilder {
context: self.context,
listener,
balancer: self.balancer,
http_auth: Arc::new(self.http_auth),
})
}
}
@@ -86,6 +93,7 @@ pub struct Http {
context: Arc<ServiceContext>,
listener: TcpListener,
balancer: PingBalancer,
http_auth: Arc<HttpAuthConfig>,
}
impl Http {
@@ -104,7 +112,7 @@ impl Http {
self.listener.local_addr().expect("http local_addr")
);
let handler = HttpConnectionHandler::new(self.context, self.balancer);
let handler = HttpConnectionHandler::new(self.context, self.balancer, self.http_auth);
loop {
let (stream, peer_addr) = match self.listener.accept().await {
@@ -135,15 +143,17 @@ pub struct HttpConnectionHandler {
context: Arc<ServiceContext>,
balancer: PingBalancer,
http_client: HttpClient<body::Incoming>,
http_auth: Arc<HttpAuthConfig>,
}
impl HttpConnectionHandler {
/// Create a new Handler
pub fn new(context: Arc<ServiceContext>, balancer: PingBalancer) -> Self {
pub fn new(context: Arc<ServiceContext>, balancer: PingBalancer, http_auth: Arc<HttpAuthConfig>) -> Self {
Self {
context,
balancer,
http_client: HttpClient::new(),
http_auth,
}
}
@@ -156,6 +166,7 @@ impl HttpConnectionHandler {
context,
balancer,
http_client,
http_auth,
} = self;
let io = TokioIo::new(stream);
@@ -169,8 +180,14 @@ impl HttpConnectionHandler {
.serve_connection(
io,
service::service_fn(move |req| {
HttpService::new(context.clone(), peer_addr, http_client.clone(), balancer.clone())
.serve_connection(req)
HttpService::new(
context.clone(),
peer_addr,
http_client.clone(),
balancer.clone(),
http_auth.clone(),
)
.serve_connection(req)
}),
)
.with_upgrades()
@@ -164,9 +164,10 @@ impl PingBalancerBuilder {
pub async fn build(self) -> io::Result<PingBalancer> {
if let Some(intv) = self.check_best_interval
&& intv > self.check_interval {
return Err(io::Error::other("check_interval must be >= check_best_interval"));
}
&& intv > self.check_interval
{
return Err(io::Error::other("check_interval must be >= check_best_interval"));
}
let (shared_context, task_abortable) = PingBalancerContext::new(
self.servers,
@@ -279,6 +279,8 @@ impl Server {
let mut server_builder = SocksBuilder::with_context(context.clone(), client_addr, balancer);
server_builder.set_mode(local_config.mode);
server_builder.set_socks5_auth(local_config.socks5_auth);
#[cfg(feature = "local-http")]
server_builder.set_http_auth(local_config.http_auth);
if let Some(c) = config.udp_max_associations {
server_builder.set_udp_capacity(c);
@@ -349,6 +351,7 @@ impl Server {
#[allow(unused_mut)]
let mut builder = HttpBuilder::with_context(context.clone(), client_addr, balancer);
builder.set_http_auth(local_config.http_auth);
#[cfg(target_os = "macos")]
if let Some(n) = local_config.launchd_tcp_socket_name {
@@ -515,11 +515,10 @@ where
}
};
if UDP_SOCKET_SUPPORT_DUAL_STACK
&& let SocketAddr::V4(saddr) = target_addr {
let mapped_ip = saddr.ip().to_ipv6_mapped();
target_addr = SocketAddr::V6(SocketAddrV6::new(mapped_ip, saddr.port(), 0, 0));
}
if UDP_SOCKET_SUPPORT_DUAL_STACK && let SocketAddr::V4(saddr) = target_addr {
let mapped_ip = saddr.ip().to_ipv6_mapped();
target_addr = SocketAddr::V6(SocketAddrV6::new(mapped_ip, saddr.port(), 0, 0));
}
let n = socket.send_to(data, target_addr).await?;
if n != data.len() {
@@ -215,13 +215,14 @@ impl OnlineConfigService {
if let Some(ref allowed_plugins) = self.allowed_plugins {
for server in &online_config.server {
if let Some(plugin) = server.config.plugin()
&& !allowed_plugins.contains(&plugin.plugin) {
error!(
"server-loader task found not allowed plugin: {}, url: {}",
plugin.plugin, self.config_url
);
return Err(io::Error::other(format!("not allowed plugin: {}", plugin.plugin)));
}
&& !allowed_plugins.contains(&plugin.plugin)
{
error!(
"server-loader task found not allowed plugin: {}, url: {}",
plugin.plugin, self.config_url
);
return Err(io::Error::other(format!("not allowed plugin: {}", plugin.plugin)));
}
}
}
@@ -64,9 +64,10 @@ async fn handle_redir_client(
//
// Try to convert IPv4 mapped IPv6 address for dual-stack mode.
if let SocketAddr::V6(ref a) = daddr
&& let Some(v4) = to_ipv4_mapped(a.ip()) {
daddr = SocketAddr::new(IpAddr::from(v4), a.port());
}
&& let Some(v4) = to_ipv4_mapped(a.ip())
{
daddr = SocketAddr::new(IpAddr::from(v4), a.port());
}
let target_addr = Address::from(daddr);
establish_client_tcp_redir(context, balancer, s, peer_addr, &target_addr).await
}
@@ -71,16 +71,15 @@ impl UdpRedirSocket {
socket.set_nonblocking(true)?;
socket.set_reuse_address(true)?;
if reuse_port
&& let Err(err) = socket.set_reuse_port(true) {
if let Some(libc::ENOPROTOOPT) = err.raw_os_error() {
// SO_REUSEPORT is supported after 3.9
trace!("failed to set SO_REUSEPORT, error: {}", err);
} else {
error!("failed to set SO_REUSEPORT, error: {}", err);
return Err(err);
}
if reuse_port && let Err(err) = socket.set_reuse_port(true) {
if let Some(libc::ENOPROTOOPT) = err.raw_os_error() {
// SO_REUSEPORT is supported after 3.9
trace!("failed to set SO_REUSEPORT, error: {}", err);
} else {
error!("failed to set SO_REUSEPORT, error: {}", err);
return Err(err);
}
}
let sock_addr = SockAddr::from(addr);
@@ -52,15 +52,14 @@ impl UdpRedirSocket {
socket.set_nonblocking(true)?;
socket.set_reuse_address(true)?;
if reuse_port
&& let Err(err) = socket.set_reuse_port(true) {
if let Some(libc::ENOPROTOOPT) = err.raw_os_error() {
trace!("failed to set SO_REUSEPORT, error: {}", err);
} else {
error!("failed to set SO_REUSEPORT, error: {}", err);
return Err(err);
}
if reuse_port && let Err(err) = socket.set_reuse_port(true) {
if let Some(libc::ENOPROTOOPT) = err.raw_os_error() {
trace!("failed to set SO_REUSEPORT, error: {}", err);
} else {
error!("failed to set SO_REUSEPORT, error: {}", err);
return Err(err);
}
}
let sock_addr = SockAddr::from(addr);
@@ -11,6 +11,8 @@ pub use self::server::{SocksTcpServer, SocksTcpServerBuilder, SocksUdpServer};
use self::socks5::Socks5UdpServerBuilder;
use super::config::Socks5AuthConfig;
#[cfg(feature = "local-http")]
use crate::local::http::config::HttpAuthConfig;
#[allow(clippy::module_inception)]
mod server;
@@ -33,6 +35,8 @@ pub struct SocksBuilder {
launchd_tcp_socket_name: Option<String>,
#[cfg(target_os = "macos")]
launchd_udp_socket_name: Option<String>,
#[cfg(feature = "local-http")]
http_auth: HttpAuthConfig,
}
impl SocksBuilder {
@@ -58,6 +62,8 @@ impl SocksBuilder {
launchd_tcp_socket_name: None,
#[cfg(target_os = "macos")]
launchd_udp_socket_name: None,
#[cfg(feature = "local-http")]
http_auth: HttpAuthConfig::default(),
}
}
@@ -93,6 +99,12 @@ impl SocksBuilder {
self.socks5_auth = p;
}
/// Set HTTP Authentication configuration
#[cfg(feature = "local-http")]
pub fn set_http_auth(&mut self, p: HttpAuthConfig) {
self.http_auth = p;
}
/// macOS launchd activate socket
#[cfg(target_os = "macos")]
pub fn set_launchd_tcp_socket_name(&mut self, n: String) {
@@ -144,6 +156,8 @@ impl SocksBuilder {
self.balancer.clone(),
self.mode,
self.socks5_auth,
#[cfg(feature = "local-http")]
self.http_auth,
);
#[cfg(target_os = "macos")]
@@ -5,7 +5,7 @@ use shadowsocks::{ServerAddr, config::Mode, net::TcpListener as ShadowTcpListene
use tokio::{net::TcpStream, time};
#[cfg(feature = "local-http")]
use crate::local::http::HttpConnectionHandler;
use crate::local::http::{HttpConnectionHandler, config::HttpAuthConfig};
use crate::local::{
context::ServiceContext, loadbalancing::PingBalancer, net::tcp::listener::create_standard_tcp_listener,
socks::config::Socks5AuthConfig,
@@ -24,6 +24,8 @@ pub struct SocksTcpServerBuilder {
socks5_auth: Arc<Socks5AuthConfig>,
#[cfg(target_os = "macos")]
launchd_socket_name: Option<String>,
#[cfg(feature = "local-http")]
http_auth: Arc<HttpAuthConfig>,
}
impl SocksTcpServerBuilder {
@@ -34,6 +36,7 @@ impl SocksTcpServerBuilder {
balancer: PingBalancer,
mode: Mode,
socks5_auth: Socks5AuthConfig,
#[cfg(feature = "local-http")] http_auth: HttpAuthConfig,
) -> Self {
Self {
context,
@@ -44,6 +47,8 @@ impl SocksTcpServerBuilder {
socks5_auth: Arc::new(socks5_auth),
#[cfg(target_os = "macos")]
launchd_socket_name: None,
#[cfg(feature = "local-http")]
http_auth: Arc::new(http_auth),
}
}
@@ -80,6 +85,8 @@ impl SocksTcpServerBuilder {
balancer: self.balancer,
mode: self.mode,
socks5_auth: self.socks5_auth,
#[cfg(feature = "local-http")]
http_auth: self.http_auth,
})
}
}
@@ -92,6 +99,8 @@ pub struct SocksTcpServer {
balancer: PingBalancer,
mode: Mode,
socks5_auth: Arc<Socks5AuthConfig>,
#[cfg(feature = "local-http")]
http_auth: Arc<HttpAuthConfig>,
}
impl SocksTcpServer {
@@ -107,7 +116,8 @@ impl SocksTcpServer {
// If UDP is enabled, SOCK5 UDP_ASSOCIATE command will let client to send requests to this address
let udp_associate_addr = Arc::new(self.udp_associate_addr);
#[cfg(feature = "local-http")]
let http_handler = HttpConnectionHandler::new(self.context.clone(), self.balancer.clone());
let http_handler =
HttpConnectionHandler::new(self.context.clone(), self.balancer.clone(), self.http_auth.clone());
loop {
let (stream, peer_addr) = match self.listener.accept().await {
+28 -20
View File
@@ -160,9 +160,10 @@ impl AsyncRead for TcpConnection {
// Nothing could be read. Wait for notify.
if let Some(old_waker) = control.recv_waker.replace(cx.waker().clone())
&& !old_waker.will_wake(cx.waker()) {
old_waker.wake();
}
&& !old_waker.will_wake(cx.waker())
{
old_waker.wake();
}
return Poll::Pending;
}
@@ -191,9 +192,10 @@ impl AsyncWrite for TcpConnection {
if control.send_buffer.is_full() {
if let Some(old_waker) = control.send_waker.replace(cx.waker().clone())
&& !old_waker.will_wake(cx.waker()) {
old_waker.wake();
}
&& !old_waker.will_wake(cx.waker())
{
old_waker.wake();
}
return Poll::Pending;
}
@@ -223,9 +225,10 @@ impl AsyncWrite for TcpConnection {
}
if let Some(old_waker) = control.send_waker.replace(cx.waker().clone())
&& !old_waker.will_wake(cx.waker()) {
old_waker.wake();
}
&& !old_waker.will_wake(cx.waker())
{
old_waker.wake();
}
self.manager_notify.notify();
Poll::Pending
@@ -418,10 +421,12 @@ impl TcpTun {
wake_receiver = true;
}
if wake_receiver && control.recv_waker.is_some()
&& let Some(waker) = control.recv_waker.take() {
waker.wake();
}
if wake_receiver
&& control.recv_waker.is_some()
&& let Some(waker) = control.recv_waker.take()
{
waker.wake();
}
// Check if writable
let mut wake_sender = false;
@@ -452,10 +457,12 @@ impl TcpTun {
}
}
if wake_sender && control.send_waker.is_some()
&& let Some(waker) = control.send_waker.take() {
waker.wake();
}
if wake_sender
&& control.send_waker.is_some()
&& let Some(waker) = control.send_waker.take()
{
waker.wake();
}
}
for socket_handle in sockets_to_remove {
@@ -597,9 +604,10 @@ async fn handle_redir_client(
//
// Try to convert IPv4 mapped IPv6 address for dual-stack mode.
if let SocketAddr::V6(ref a) = daddr
&& let Some(v4) = to_ipv4_mapped(a.ip()) {
daddr = SocketAddr::new(IpAddr::from(v4), a.port());
}
&& let Some(v4) = to_ipv4_mapped(a.ip())
{
daddr = SocketAddr::new(IpAddr::from(v4), a.port());
}
let target_addr = Address::from(daddr);
establish_client_tcp_redir(context, balancer, s, peer_addr, &target_addr).await
}
@@ -330,22 +330,23 @@ impl Manager {
let pid_path = self.server_pid_path(port);
if pid_path.exists()
&& let Ok(mut pid_file) = File::open(&pid_path) {
let mut pid_content = String::new();
if pid_file.read_to_string(&mut pid_content).is_ok() {
let pid_content = pid_content.trim();
&& let Ok(mut pid_file) = File::open(&pid_path)
{
let mut pid_content = String::new();
if pid_file.read_to_string(&mut pid_content).is_ok() {
let pid_content = pid_content.trim();
match pid_content.parse::<libc::pid_t>() {
Ok(pid) => {
let _ = unsafe { libc::kill(pid, libc::SIGTERM) };
debug!("killed standalone server port {}, pid: {}", port, pid);
}
Err(..) => {
warn!("failed to read pid from {}", pid_path.display());
}
match pid_content.parse::<libc::pid_t>() {
Ok(pid) => {
let _ = unsafe { libc::kill(pid, libc::SIGTERM) };
debug!("killed standalone server port {}, pid: {}", port, pid);
}
Err(..) => {
warn!("failed to read pid from {}", pid_path.display());
}
}
}
}
let server_config_path = self.server_config_path(port);
@@ -594,13 +594,14 @@ impl UdpAssociationContext {
control: &Option<UdpSocketControlData>,
) {
if let Some(ref mut session) = self.client_session
&& peer_addr != self.peer_addr {
debug!(
"udp relay for {} changed to {}, session: {:?}",
self.peer_addr, peer_addr, session.client_session_id
);
self.peer_addr = peer_addr;
}
&& peer_addr != self.peer_addr
{
debug!(
"udp relay for {} changed to {}, session: {:?}",
self.peer_addr, peer_addr, session.client_session_id
);
self.peer_addr = peer_addr;
}
trace!(
"udp relay {} -> {} with {} bytes, control: {:?}",
@@ -737,9 +738,10 @@ impl UdpAssociationContext {
// But for some implementations, they may expect the target address to be IPv4, because
// the peer address is IPv4 when calling `sendto`.
if let Address::SocketAddress(SocketAddr::V6(ref v6)) = addr
&& let Some(v4) = to_ipv4_mapped(v6.ip()) {
addr = Address::SocketAddress(SocketAddr::new(v4.into(), v6.port()));
}
&& let Some(v4) = to_ipv4_mapped(v6.ip())
{
addr = Address::SocketAddress(SocketAddr::new(v4.into(), v6.port()));
}
match self.client_session {
None => {
+8 -6
View File
@@ -696,18 +696,20 @@ impl ServerConfig {
/// Get server's TCP external address
pub fn tcp_external_addr(&self) -> &ServerAddr {
if let Some(plugin) = self.plugin()
&& plugin.plugin_mode.enable_tcp() {
return self.plugin_addr.as_ref().unwrap_or(&self.addr);
}
&& plugin.plugin_mode.enable_tcp()
{
return self.plugin_addr.as_ref().unwrap_or(&self.addr);
}
&self.addr
}
/// Get server's UDP external address
pub fn udp_external_addr(&self) -> &ServerAddr {
if let Some(plugin) = self.plugin()
&& plugin.plugin_mode.enable_udp() {
return self.plugin_addr.as_ref().unwrap_or(&self.addr);
}
&& plugin.plugin_mode.enable_udp()
{
return self.plugin_addr.as_ref().unwrap_or(&self.addr);
}
&self.addr
}
+7 -6
View File
@@ -168,13 +168,14 @@ static IP_STACK_CAPABILITIES: LazyLock<IpStackCapabilities> = LazyLock::new(|| {
// Check IPv6 (::1)
if let Ok(ipv6_socket) = Socket::new(Domain::IPV6, Type::STREAM, Some(Protocol::TCP))
&& ipv6_socket.set_only_v6(true).is_ok() {
let local_host = SockAddr::from(SocketAddr::new(Ipv6Addr::LOCALHOST.into(), 0));
if ipv6_socket.bind(&local_host).is_ok() {
caps.support_ipv6 = true;
debug!("IpStackCapability support_ipv6=true");
}
&& ipv6_socket.set_only_v6(true).is_ok()
{
let local_host = SockAddr::from(SocketAddr::new(Ipv6Addr::LOCALHOST.into(), 0));
if ipv6_socket.bind(&local_host).is_ok() {
caps.support_ipv6 = true;
debug!("IpStackCapability support_ipv6=true");
}
}
// Check IPv4-mapped-IPv6 (127.0.0.1)
if check_ipv4_mapped_ipv6_capability().is_ok() {
@@ -391,9 +391,10 @@ pub async fn bind_outbound_udp_socket(bind_addr: &SocketAddr, config: &ConnectOp
};
if !config.udp.allow_fragmentation
&& let Err(err) = set_disable_ip_fragmentation(af, &socket) {
warn!("failed to disable IP fragmentation, error: {}", err);
}
&& let Err(err) = set_disable_ip_fragmentation(af, &socket)
{
warn!("failed to disable IP fragmentation, error: {}", err);
}
// Set IP_BOUND_IF for BSD-like
if let Some(ref iface) = config.bind_interface {
@@ -311,9 +311,10 @@ pub async fn bind_outbound_udp_socket(bind_addr: &SocketAddr, config: &ConnectOp
};
if !config.udp.allow_fragmentation
&& let Err(err) = set_disable_ip_fragmentation(af, &socket) {
warn!("failed to disable IP fragmentation, error: {}", err);
}
&& let Err(err) = set_disable_ip_fragmentation(af, &socket)
{
warn!("failed to disable IP fragmentation, error: {}", err);
}
// Any traffic except localhost should be protected
// This is a workaround for VPNService
+34 -26
View File
@@ -35,11 +35,11 @@ use futures::ready;
use tokio::io::Interest;
use tokio::{io::ReadBuf, net::ToSocketAddrs};
use crate::{context::Context, relay::socks5::Address, ServerAddr};
use crate::{ServerAddr, context::Context, relay::socks5::Address};
use super::{
sys::{bind_outbound_udp_socket, create_inbound_udp_socket, create_outbound_udp_socket},
AcceptOpts, AddrFamily, ConnectOpts,
sys::{bind_outbound_udp_socket, create_inbound_udp_socket, create_outbound_udp_socket},
};
/// Message struct for `batch_send`
@@ -190,9 +190,10 @@ impl UdpSocket {
pub fn poll_send(&self, cx: &mut TaskContext<'_>, buf: &[u8]) -> Poll<io::Result<usize>> {
// Check MTU
if let Some(mtu) = self.mtu
&& buf.len() > mtu {
return Err(make_mtu_error(buf.len(), mtu)).into();
}
&& buf.len() > mtu
{
return Err(make_mtu_error(buf.len(), mtu)).into();
}
self.socket.poll_send(cx, buf)
}
@@ -202,9 +203,10 @@ impl UdpSocket {
pub async fn send(&self, buf: &[u8]) -> io::Result<usize> {
// Check MTU
if let Some(mtu) = self.mtu
&& buf.len() > mtu {
return Err(make_mtu_error(buf.len(), mtu));
}
&& buf.len() > mtu
{
return Err(make_mtu_error(buf.len(), mtu));
}
self.socket.send(buf).await
}
@@ -213,9 +215,10 @@ impl UdpSocket {
pub fn poll_send_to(&self, cx: &mut TaskContext<'_>, buf: &[u8], target: SocketAddr) -> Poll<io::Result<usize>> {
// Check MTU
if let Some(mtu) = self.mtu
&& buf.len() > mtu {
return Err(make_mtu_error(buf.len(), mtu)).into();
}
&& buf.len() > mtu
{
return Err(make_mtu_error(buf.len(), mtu)).into();
}
self.socket.poll_send_to(cx, buf, target)
}
@@ -225,9 +228,10 @@ impl UdpSocket {
pub async fn send_to<A: ToSocketAddrs>(&self, buf: &[u8], target: A) -> io::Result<usize> {
// Check MTU
if let Some(mtu) = self.mtu
&& buf.len() > mtu {
return Err(make_mtu_error(buf.len(), mtu));
}
&& buf.len() > mtu
{
return Err(make_mtu_error(buf.len(), mtu));
}
self.socket.send_to(buf, target).await
}
@@ -238,9 +242,10 @@ impl UdpSocket {
ready!(self.socket.poll_recv(cx, buf))?;
if let Some(mtu) = self.mtu
&& buf.filled().len() > mtu {
return Err(make_mtu_error(buf.filled().len(), mtu)).into();
}
&& buf.filled().len() > mtu
{
return Err(make_mtu_error(buf.filled().len(), mtu)).into();
}
Ok(()).into()
}
@@ -251,9 +256,10 @@ impl UdpSocket {
let n = self.socket.recv(buf).await?;
if let Some(mtu) = self.mtu
&& n > mtu {
return Err(make_mtu_error(n, mtu));
}
&& n > mtu
{
return Err(make_mtu_error(n, mtu));
}
Ok(n)
}
@@ -264,9 +270,10 @@ impl UdpSocket {
let addr = ready!(self.socket.poll_recv_from(cx, buf))?;
if let Some(mtu) = self.mtu
&& buf.filled().len() > mtu {
return Err(make_mtu_error(buf.filled().len(), mtu)).into();
}
&& buf.filled().len() > mtu
{
return Err(make_mtu_error(buf.filled().len(), mtu)).into();
}
Ok(addr).into()
}
@@ -277,9 +284,10 @@ impl UdpSocket {
let (n, addr) = self.socket.recv_from(buf).await?;
if let Some(mtu) = self.mtu
&& n > mtu {
return Err(make_mtu_error(n, mtu));
}
&& n > mtu
{
return Err(make_mtu_error(n, mtu));
}
Ok((n, addr))
}
@@ -160,9 +160,10 @@ where
// Wakeup writer task because we have already received the salt
#[cfg(feature = "aead-cipher-2022")]
if let ProxyServerStreamWriteState::PrepareHeader(waker) = this.writer_state
&& let Some(waker) = waker.take() {
waker.wake();
}
&& let Some(waker) = waker.take()
{
waker.wake();
}
Ok(()).into()
}
@@ -190,9 +191,10 @@ where
} else {
// Reader didn't receive the salt from client yet.
if let Some(waker) = waker.take()
&& !waker.will_wake(cx.waker()) {
waker.wake();
}
&& !waker.will_wake(cx.waker())
{
waker.wake();
}
*waker = Some(cx.waker().clone());
return Poll::Pending;
}
+1 -2
View File
@@ -6,7 +6,6 @@
// option. This file may not be copied, modified, or distributed
// except according to those terms.
//!
//! daemonize is a library for writing system daemons. Inspired by the Python library [thesharp/daemonize](https://github.com/thesharp/daemonize).
//!
//! The respository is located at <https://github.com/knsd/daemonize/>.
@@ -211,7 +210,6 @@ impl<T> Outcome<T> {
/// * change root directory;
/// * change the pid-file ownership to provided user (and/or) group;
/// * execute any provided action just before dropping privileges.
///
pub struct Daemonize<T> {
directory: PathBuf,
pid_file: Option<PathBuf>,
@@ -339,6 +337,7 @@ impl<T> Daemonize<T> {
self.stderr = stdio.into();
self
}
/// Start daemonization process, terminate parent after first fork, returns privileged action
/// result to the child.
pub fn start(self) -> Result<T, Error> {
+8 -6
View File
@@ -756,14 +756,16 @@ pub fn create(matches: &ArgMatches) -> ShadowsocksResult<(Runtime, impl Future<O
#[cfg(feature = "local-redir")]
{
if RedirType::tcp_default() != RedirType::NotSupported
&& let Some(tcp_redir) = matches.get_one::<String>("TCP_REDIR") {
local_config.tcp_redir = tcp_redir.parse::<RedirType>().expect("tcp-redir");
}
&& let Some(tcp_redir) = matches.get_one::<String>("TCP_REDIR")
{
local_config.tcp_redir = tcp_redir.parse::<RedirType>().expect("tcp-redir");
}
if RedirType::udp_default() != RedirType::NotSupported
&& let Some(udp_redir) = matches.get_one::<String>("UDP_REDIR") {
local_config.udp_redir = udp_redir.parse::<RedirType>().expect("udp-redir");
}
&& let Some(udp_redir) = matches.get_one::<String>("UDP_REDIR")
{
local_config.udp_redir = udp_redir.parse::<RedirType>().expect("udp-redir");
}
}
#[cfg(feature = "local-dns")]
+8 -6
View File
@@ -405,14 +405,16 @@ pub fn create(matches: &ArgMatches) -> ShadowsocksResult<(Runtime, impl Future<O
// Overrides
if matches.get_flag("UDP_ONLY")
&& let Some(ref mut m) = config.manager {
m.mode = Mode::UdpOnly;
}
&& let Some(ref mut m) = config.manager
{
m.mode = Mode::UdpOnly;
}
if matches.get_flag("TCP_AND_UDP")
&& let Some(ref mut m) = config.manager {
m.mode = Mode::TcpAndUdp;
}
&& let Some(ref mut m) = config.manager
{
m.mode = Mode::TcpAndUdp;
}
if let Some(acl_file) = matches.get_one::<String>("ACL") {
let acl = AccessControl::load_from_file(acl_file)