refactor(ohos): 拆分 OHRS 包并按 socket 精细保护 VPN 流量 (#2543)

* refactor(ohos): split facade feature and kernel crates
* feat(ohos): protect transport sockets individually
* fix(ohos): keep local proxy subnets off tun
* fix(ohos): expose valid config enum values
* refactor(ohos): finalize reusable core boundary
* test(ohos): verify split package contracts

* fix(port-forward): support wildcard userspace listeners
Keep the existing Host listener intact while adding a DataPlane listener for force-smoltcp IPv4 wildcard rules. Keep literal loopback destinations on the local Host path instead of exporting them through an exit node.

* chore(ohos): refresh split workspace lockfile
* fix(socket): normalize Windows raw socket handles
* refactor(socket): carry VPN protection through host bind options
* refactor(socket): simplify protection defaults and TUN ingress
* refactor(socket): consolidate native protection and socket creation
* fix(socket): protect outbound UDP paths
* fix(socket): preserve VPN routing for RPC listeners

---------

Co-authored-by: FrankHan <frankhan@FrankHans-Mac-mini.local>
Co-authored-by: KKRainbow <443152178@qq.com>
This commit is contained in:
authored and GitHub committed 2026-09-09 22:12:47 +08:00
1 parent b1f87f025b
commit 38e2a621bb
63 files changed
+2011 -397

No files matched your search

@@ -262,6 +262,7 @@ impl<R: TcpProxyRuntime + 'static, F: VirtualTcpListenerFactory, C: TcpProxyDest
.bind_tcp(
TcpListenOptions::proxy_nat(listen_addr).with_bind(
TcpBindOptions::default()
.with_need_protect(false)
.with_context(
self.socket_context
.clone()
+5
View File
@@ -67,6 +67,7 @@ where
fn udp_bind_options(&self) -> UdpBindOptions {
UdpBindOptions::socks5()
.with_need_protect(false)
.with_context(self.socket_context.clone().with_ip_version(IpVersion::V6))
.with_local_addr(Some(SocketAddr::V6(SocketAddrV6::new(
Ipv6Addr::UNSPECIFIED,
@@ -289,6 +290,10 @@ mod tests {
);
assert_eq!(options[0].context.socket_mark, context.socket_mark);
assert_eq!(options[0].context.ip_version, IpVersion::V6);
assert!(
!options[0].need_protect,
"SOCKS5 association socket is inbound"
);
}
#[tokio::test]
+4
View File
@@ -47,6 +47,10 @@ pub trait DnsRecordResolver: Send + Sync + 'static {
/// Submit methods must return without waiting for DNS. Completion methods own
/// their returned data and must not retain guest-memory borrows. Cancellation
/// removes pending or completed-but-unobserved operation state.
/// When the host uses VPN bypass, every underlying DNS socket (including TCP
/// fallback) must be protected before sending queries. A system resolver is only
/// suitable if the host can guarantee equivalent routing; errors must not fall
/// back to unprotected DNS traffic.
pub trait HostDnsIo: Send + Sync + 'static {
fn submit_resolve(&self, operation: HostOperationId, query: &DnsQuery) -> io::Result<()>;
+3
View File
@@ -13,6 +13,9 @@ use super::socket::{HostOperationId, HostSocketRuntime};
/// take retains the operation; a `Ready` take consumes both successful and
/// failed results. Cancellation must remove pending and completed-but-unread
/// state and is idempotent for an already-absent operation.
/// Source-address probes describe the underlay route: if VPN bypass is active,
/// protect the probe socket before connect/source-address selection. Protection
/// acknowledgement belongs inside this host operation, not a later guest call.
pub trait HostConnectorEnvironmentIo: Send + Sync + 'static {
fn submit_local_addr_for_remote(
&self,
+8 -1
View File
@@ -31,6 +31,9 @@ pub struct HostUdpBindResult {
/// their handles and address metadata. Canceling an operation must atomically
/// stop pending creation or close and discard a resource that completed before
/// core observed it, so dropping a factory future cannot leak a host socket.
/// When bind options request socket protection, the host must complete and
/// acknowledge protection before connect, datagram I/O, or successful creation
/// completion. A protection error fails the creation operation.
pub trait HostSocketFactoryIo: HostSocketIo {
fn submit_tcp_connect(
&self,
@@ -388,7 +391,8 @@ mod tests {
.with_local_addr(Some("192.0.2.1:0".parse().unwrap()))
.with_socket_mark(Some(7))
.with_bind_device(Some("host-device".to_owned()))
.with_reuse_port(true),
.with_reuse_port(true)
.with_need_protect(true),
purpose: TcpSocketPurpose::ManualConnect,
};
let task = tokio::spawn({
@@ -407,6 +411,7 @@ mod tests {
panic!("operation is not TCP connect");
};
assert_eq!(submitted, &options);
assert!(submitted.bind.need_protect);
}
io.complete_tcp(
@@ -436,6 +441,7 @@ mod tests {
let (runtime, factory) = test_factory(io.clone());
let options = UdpBindOptions {
context: crate::socket::SocketContext::default().with_socket_mark(Some(9)),
need_protect: false,
local_addr: Some("[::]:11013".parse().unwrap()),
bind_device: Some("host-device".to_owned()),
reuse_addr: true,
@@ -459,6 +465,7 @@ mod tests {
panic!("operation is not UDP bind");
};
assert_eq!(submitted, &options);
assert!(!submitted.need_protect);
}
io.complete_udp(
@@ -19,6 +19,9 @@ pub struct HostTcpBindResult {
/// Bind cancellation must close a completed but unobserved listener. Accepted
/// connections stay in a host-owned listener queue until `take_tcp_accept` is
/// called from a guest poll; canceling an accept waiter must not remove one.
/// When bind options request socket protection, the host must complete and
/// acknowledge protection before bind/listen and before exposing each accepted
/// child. A protection error fails the corresponding bind or accept operation.
pub trait HostTcpListenerIo: HostSocketIo {
fn submit_tcp_bind(
&self,
+6
View File
@@ -10,6 +10,12 @@ pub mod ring;
pub mod tcp;
pub mod udp;
/// Keep Rust constructors and deserialization consistent: host sockets normally
/// need VPN bypass; local/TUN-facing endpoint constructors explicitly opt out.
pub(crate) const fn default_need_protect() -> bool {
true
}
use std::{fmt::Debug, sync::Arc};
use async_trait::async_trait;
+59 -5
View File
@@ -55,6 +55,12 @@ pub enum TcpSocketPurpose {
pub struct TcpBindOptions {
#[serde(default)]
pub context: SocketContext,
/// Request host VPN bypass during creation, before bind/connect/listen.
/// Hosts with a protection service must await its acknowledgement; local
/// listeners opt out explicitly. Defaults to true, including when omitted
/// from serialized options. Accepted children inherit this requirement.
#[serde(default = "super::default_need_protect")]
pub need_protect: bool,
pub local_addr: Option<SocketAddr>,
pub bind_device: Option<String>,
/// `None` delegates the platform default to the host socket adapter.
@@ -67,6 +73,7 @@ impl TcpBindOptions {
pub fn new() -> Self {
Self {
context: SocketContext::default(),
need_protect: super::default_need_protect(),
local_addr: None,
bind_device: None,
reuse_addr: None,
@@ -90,6 +97,11 @@ impl TcpBindOptions {
self
}
pub fn with_need_protect(mut self, need_protect: bool) -> Self {
self.need_protect = need_protect;
self
}
pub fn with_ip_version(mut self, ip_version: IpVersion) -> Self {
self.context.ip_version = ip_version;
self
@@ -250,28 +262,36 @@ impl TcpListenOptions {
pub fn proxy_nat(local_addr: SocketAddr) -> Self {
Self {
bind: TcpBindOptions::default().with_local_addr(Some(local_addr)),
bind: TcpBindOptions::default()
.with_need_protect(false)
.with_local_addr(Some(local_addr)),
purpose: TcpListenPurpose::ProxyNat,
}
}
pub fn socks5(local_addr: SocketAddr) -> Self {
Self {
bind: TcpBindOptions::default().with_local_addr(Some(local_addr)),
bind: TcpBindOptions::default()
.with_need_protect(false)
.with_local_addr(Some(local_addr)),
purpose: TcpListenPurpose::Socks5,
}
}
pub fn port_forward(local_addr: SocketAddr) -> Self {
Self {
bind: TcpBindOptions::default().with_local_addr(Some(local_addr)),
bind: TcpBindOptions::default()
.with_need_protect(false)
.with_local_addr(Some(local_addr)),
purpose: TcpListenPurpose::PortForward,
}
}
pub fn port_lease(local_addr: SocketAddr) -> Self {
Self {
bind: TcpBindOptions::default().with_local_addr(Some(local_addr)),
bind: TcpBindOptions::default()
.with_need_protect(false)
.with_local_addr(Some(local_addr)),
purpose: TcpListenPurpose::PortLease,
}
}
@@ -612,7 +632,9 @@ mod tests {
assert_eq!(
TcpListenOptions::proxy_nat(local_addr),
TcpListenOptions {
bind: TcpBindOptions::default().with_local_addr(Some(local_addr)),
bind: TcpBindOptions::default()
.with_need_protect(false)
.with_local_addr(Some(local_addr)),
purpose: TcpListenPurpose::ProxyNat,
}
);
@@ -633,6 +655,7 @@ mod tests {
options,
TcpBindOptions {
context: SocketContext::default().with_socket_mark(Some(7)),
need_protect: true,
local_addr: Some(local_addr),
bind_device: Some("eth0".to_owned()),
reuse_addr: Some(true),
@@ -645,6 +668,37 @@ mod tests {
#[test]
fn tcp_bind_default_delegates_reuse_addr_policy_to_host() {
assert_eq!(TcpBindOptions::default().reuse_addr, None);
assert!(TcpBindOptions::default().need_protect);
}
#[test]
fn tcp_constructor_protection_defaults_match_endpoint_role() {
let remote = SocketAddr::from(([192, 0, 2, 1], 11010));
let local = SocketAddr::from(([0, 0, 0, 0], 11010));
assert!(TcpConnectOptions::direct_connect(remote).bind.need_protect);
assert!(TcpConnectOptions::proxy_nat(remote).bind.need_protect);
assert!(TcpListenOptions::direct_connect(local).bind.need_protect);
assert!(TcpListenOptions::hole_punch(local).bind.need_protect);
assert!(TcpListenOptions::manual_connect(local).bind.need_protect);
assert!(!TcpListenOptions::proxy_nat(local).bind.need_protect);
assert!(!TcpListenOptions::socks5(local).bind.need_protect);
assert!(!TcpListenOptions::port_forward(local).bind.need_protect);
assert!(!TcpListenOptions::port_lease(local).bind.need_protect);
}
#[test]
fn tcp_bind_serde_defaults_to_protected_and_preserves_opt_out() {
let options: TcpBindOptions = serde_json::from_str(
r#"{"local_addr":null,"bind_device":null,"reuse_addr":null,"reuse_port":false,"only_v6":false}"#,
)
.unwrap();
assert!(options.need_protect);
let local = TcpListenOptions::port_forward("0.0.0.0:15555".parse().unwrap());
let restored: TcpListenOptions =
serde_json::from_str(&serde_json::to_string(&local).unwrap()).unwrap();
assert_eq!(restored, local);
assert!(!restored.bind.need_protect);
}
#[tokio::test]
+9 -1
View File
@@ -39,6 +39,7 @@ fn bind_options_constructors_describe_socket_purpose() {
reuse_port: false,
only_v6: false,
purpose: UdpSocketPurpose::HolePunchControl,
need_protect: true,
}
);
assert_eq!(
@@ -51,6 +52,7 @@ fn bind_options_constructors_describe_socket_purpose() {
reuse_port: false,
only_v6: false,
purpose: UdpSocketPurpose::HolePunchCandidate,
need_protect: true,
}
);
assert_eq!(
@@ -63,6 +65,7 @@ fn bind_options_constructors_describe_socket_purpose() {
reuse_port: false,
only_v6: false,
purpose: UdpSocketPurpose::DirectConnect,
need_protect: true,
}
);
assert_eq!(
@@ -75,6 +78,7 @@ fn bind_options_constructors_describe_socket_purpose() {
reuse_port: false,
only_v6: false,
purpose: UdpSocketPurpose::PortBoundListener,
need_protect: true,
}
);
assert_eq!(
@@ -87,6 +91,7 @@ fn bind_options_constructors_describe_socket_purpose() {
reuse_port: false,
only_v6: false,
purpose: UdpSocketPurpose::Socks5,
need_protect: true,
}
);
assert_eq!(
@@ -99,7 +104,7 @@ fn bind_options_constructors_describe_socket_purpose() {
);
assert_eq!(
UdpBindOptions::default(),
UdpBindOptions::hole_punch_control()
UdpBindOptions::hole_punch_control().with_need_protect(true)
);
}
@@ -123,6 +128,7 @@ fn session_connect_request_keeps_peer_scoped_udp_shape() {
reuse_port: false,
only_v6: false,
purpose: UdpSocketPurpose::PortBoundListener,
need_protect: true,
}
);
}
@@ -2045,6 +2051,7 @@ async fn v4_hole_punch_control_sender_uses_factory_socket() {
factory.bind_options(),
vec![
UdpBindOptions::hole_punch_control()
.with_need_protect(false)
.with_context(context.with_ip_version(IpVersion::V4))
.with_local_addr(Some(SocketAddr::V4(SocketAddrV4::new(
Ipv4Addr::LOCALHOST,
@@ -2089,6 +2096,7 @@ async fn v6_hole_punch_control_sender_uses_factory_socket() {
factory.bind_options(),
vec![
UdpBindOptions::hole_punch_control()
.with_need_protect(false)
.with_context(context.with_ip_version(IpVersion::V6))
.with_local_addr(Some(SocketAddr::V6(SocketAddrV6::new(
Ipv6Addr::LOCALHOST,
+57 -3
View File
@@ -141,6 +141,7 @@ where
let socket = factory
.bind_udp(
UdpBindOptions::hole_punch_control()
.with_need_protect(false)
.with_context(context.with_ip_version(IpVersion::V4))
.with_local_addr(Some(SocketAddr::V4(SocketAddrV4::new(
Ipv4Addr::LOCALHOST,
@@ -167,6 +168,7 @@ where
let socket = factory
.bind_udp(
UdpBindOptions::hole_punch_control()
.with_need_protect(false)
.with_context(context.with_ip_version(IpVersion::V6))
.with_local_addr(Some(SocketAddr::V6(SocketAddrV6::new(
Ipv6Addr::LOCALHOST,
@@ -199,6 +201,11 @@ pub enum UdpSocketPurpose {
pub struct UdpBindOptions {
#[serde(default)]
pub context: SocketContext,
/// Request host VPN bypass before bind or datagram I/O. Protection must be
/// acknowledged inside creation, not emitted as a fire-and-forget event.
/// Defaults to true; local/TUN-facing endpoints explicitly opt out.
#[serde(default = "crate::socket::default_need_protect")]
pub need_protect: bool,
pub local_addr: Option<SocketAddr>,
pub bind_device: Option<String>,
pub reuse_addr: bool,
@@ -211,6 +218,7 @@ impl UdpBindOptions {
fn for_purpose(purpose: UdpSocketPurpose) -> Self {
Self {
context: SocketContext::default(),
need_protect: crate::socket::default_need_protect(),
local_addr: None,
bind_device: None,
reuse_addr: false,
@@ -252,11 +260,15 @@ impl UdpBindOptions {
}
pub fn port_forward(local_addr: SocketAddr) -> Self {
Self::for_purpose(UdpSocketPurpose::PortForward).with_local_addr(Some(local_addr))
Self::for_purpose(UdpSocketPurpose::PortForward)
.with_need_protect(false)
.with_local_addr(Some(local_addr))
}
pub fn port_lease(local_addr: SocketAddr) -> Self {
Self::for_purpose(UdpSocketPurpose::PortLease).with_local_addr(Some(local_addr))
Self::for_purpose(UdpSocketPurpose::PortLease)
.with_need_protect(false)
.with_local_addr(Some(local_addr))
}
pub fn with_local_addr(mut self, local_addr: Option<SocketAddr>) -> Self {
@@ -274,6 +286,11 @@ impl UdpBindOptions {
self
}
pub fn with_need_protect(mut self, need_protect: bool) -> Self {
self.need_protect = need_protect;
self
}
pub fn with_ip_version(mut self, ip_version: IpVersion) -> Self {
self.context.ip_version = ip_version;
self
@@ -302,7 +319,44 @@ impl UdpBindOptions {
impl Default for UdpBindOptions {
fn default() -> Self {
Self::hole_punch_control()
// Preserve the existing default purpose/socket setup; local-only
// control packets opt out explicitly at their loopback call sites.
Self::for_purpose(UdpSocketPurpose::HolePunchControl)
}
}
#[cfg(test)]
mod option_tests {
use super::*;
#[test]
fn udp_constructor_protection_defaults_match_endpoint_role() {
let local = SocketAddr::from(([0, 0, 0, 0], 11010));
assert!(UdpBindOptions::default().need_protect);
assert!(UdpBindOptions::hole_punch_control().need_protect);
assert!(UdpBindOptions::hole_punch_candidate().need_protect);
assert!(UdpBindOptions::direct_connect().need_protect);
assert!(UdpBindOptions::port_bound_listener(local).need_protect);
assert!(UdpBindOptions::proxy_nat().need_protect);
assert!(UdpBindOptions::stun_probe().need_protect);
assert!(UdpBindOptions::socks5().need_protect);
assert!(!UdpBindOptions::port_forward(local).need_protect);
assert!(!UdpBindOptions::port_lease(local).need_protect);
}
#[test]
fn udp_bind_serde_defaults_to_protected_and_preserves_opt_out() {
let options: UdpBindOptions = serde_json::from_str(
r#"{"local_addr":null,"bind_device":null,"reuse_addr":false,"reuse_port":false,"only_v6":false,"purpose":"DirectConnect"}"#,
)
.unwrap();
assert!(options.need_protect);
let local = UdpBindOptions::port_forward("0.0.0.0:15555".parse().unwrap());
let restored: UdpBindOptions =
serde_json::from_str(&serde_json::to_string(&local).unwrap()).unwrap();
assert_eq!(restored, local);
assert!(!restored.need_protect);
}
}
+10 -2
View File
@@ -114,30 +114,36 @@ unsafe extern "C" {
pub(crate) fn take_udp_send_ready(operation: u64) -> i32;
/// Starts a TCP connection using an encoded `TcpConnectOptions` document.
/// Requested socket protection must complete before the host connects.
pub(crate) fn start_tcp_connect(operation: u64, options: u32, options_len: u32) -> i32;
/// Copies the completed TCP connection handle and addresses into `result`.
pub(crate) fn take_tcp_connect(operation: u64, result: u32, result_len: u32) -> i32;
/// Starts a UDP bind using an encoded `UdpBindOptions` document.
/// Starts a UDP bind using an encoded `UdpBindOptions` document. Requested
/// socket protection must complete before bind or datagram I/O.
pub(crate) fn start_udp_bind(operation: u64, options: u32, options_len: u32) -> i32;
/// Copies the completed UDP socket handle and local address into `result`.
pub(crate) fn take_udp_bind(operation: u64, result: u32, result_len: u32) -> i32;
/// Starts a TCP listener bind using an encoded `TcpListenOptions` document.
/// Requested socket protection must complete before bind/listen.
pub(crate) fn start_tcp_bind(operation: u64, options: u32, options_len: u32) -> i32;
/// Copies the completed listener handle and local address into `result`.
pub(crate) fn take_tcp_bind(operation: u64, result: u32, result_len: u32) -> i32;
/// Starts accepting one TCP stream from a listener handle.
/// Starts accepting one TCP stream from a listener handle. A protected
/// listener's accepted child must be protected before it is exposed.
pub(crate) fn start_tcp_accept(handle: u64, operation: u64) -> i32;
/// Copies the accepted TCP stream handle and addresses into `result`.
pub(crate) fn take_tcp_accept(operation: u64, result: u32, result_len: u32) -> i32;
/// Starts an address-record DNS lookup for an encoded [`crate::host::dns::DnsQuery`].
/// When VPN bypass is active, the host must protect every underlying DNS
/// socket before sending a query or opening a DNS TCP connection.
pub(crate) fn start_dns_resolve(operation: u64, query: u32, query_len: u32) -> i32;
/// Probes or copies the encoded DNS address result for `operation`.
@@ -159,6 +165,8 @@ unsafe extern "C" {
pub(crate) fn take_dns_srv(operation: u64, result: u32, result_capacity: u32) -> i32;
/// Starts finding the local address and source context needed to reach `remote_addr`.
/// When VPN bypass is active, the host must protect the underlying route-
/// probe socket before connecting or sending through it.
pub(crate) fn start_local_addr_for_remote(
operation: u64,
remote_addr: u32,
+46 -15
View File
@@ -13,13 +13,13 @@ use crate::socket::{
use super::socket::{SOCKET_ADDRESS_LEN, decode_socket_address, encode_socket_address};
const OPTIONS_VERSION: u8 = 2;
const OPTIONS_VERSION: u8 = 3;
pub(crate) const TCP_SOCKET_RESULT_LEN: usize = 8 + SOCKET_ADDRESS_LEN * 2;
pub(crate) const BOUND_SOCKET_RESULT_LEN: usize = 8 + SOCKET_ADDRESS_LEN;
pub(crate) fn encode_tcp_connect_options(options: &TcpConnectOptions) -> io::Result<Vec<u8>> {
let mut encoded = Vec::with_capacity(
75 + context_variable_len(&options.bind.context)
76 + context_variable_len(&options.bind.context)
+ bind_device_len(&options.bind.bind_device),
);
encoded.push(OPTIONS_VERSION);
@@ -44,13 +44,14 @@ pub(crate) fn encode_tcp_connect_options(options: &TcpConnectOptions) -> io::Res
TcpSocketPurpose::PortForward => 7,
TcpSocketPurpose::DataPlane => 8,
});
encoded.push(u8::from(options.bind.need_protect));
encode_bind_device(&mut encoded, &options.bind.bind_device)?;
Ok(encoded)
}
pub(crate) fn encode_udp_bind_options(options: &UdpBindOptions) -> io::Result<Vec<u8>> {
let mut encoded = Vec::with_capacity(
48 + context_variable_len(&options.context) + bind_device_len(&options.bind_device),
49 + context_variable_len(&options.context) + bind_device_len(&options.bind_device),
);
encoded.push(OPTIONS_VERSION);
encode_optional_address(&mut encoded, options.local_addr);
@@ -69,13 +70,14 @@ pub(crate) fn encode_udp_bind_options(options: &UdpBindOptions) -> io::Result<Ve
UdpSocketPurpose::PortForward => 7,
UdpSocketPurpose::PortLease => 8,
});
encoded.push(u8::from(options.need_protect));
encode_bind_device(&mut encoded, &options.bind_device)?;
Ok(encoded)
}
pub(crate) fn encode_tcp_listen_options(options: &TcpListenOptions) -> io::Result<Vec<u8>> {
let mut encoded = Vec::with_capacity(
48 + context_variable_len(&options.bind.context)
49 + context_variable_len(&options.bind.context)
+ bind_device_len(&options.bind.bind_device),
);
encoded.push(OPTIONS_VERSION);
@@ -97,6 +99,7 @@ pub(crate) fn encode_tcp_listen_options(options: &TcpListenOptions) -> io::Resul
TcpListenPurpose::PortForward => 5,
TcpListenPurpose::PortLease => 6,
});
encoded.push(u8::from(options.bind.need_protect));
encode_bind_device(&mut encoded, &options.bind.bind_device)?;
Ok(encoded)
}
@@ -221,13 +224,13 @@ mod tests {
purpose: TcpSocketPurpose::ManualConnect,
};
let encoded = encode_tcp_connect_options(&options).unwrap();
assert_eq!(encoded.len(), 82);
assert_eq!(encoded.len(), 83);
assert_eq!(encoded[0], OPTIONS_VERSION);
assert_eq!(&encoded[55..66], &[2, 1, 1, 2, 3, 4, 0, 0, 0, 0, 0]);
assert_eq!(&encoded[66..70], &[2, 1, 1, 3]);
assert_eq!(encoded[70], 1);
assert_eq!(&encoded[71..75], &7_u32.to_be_bytes());
assert_eq!(&encoded[75..], b"device0");
assert_eq!(&encoded[66..71], &[2, 1, 1, 3, 1]);
assert_eq!(encoded[71], 1);
assert_eq!(&encoded[72..76], &7_u32.to_be_bytes());
assert_eq!(&encoded[76..], b"device0");
}
#[test]
@@ -239,16 +242,16 @@ mod tests {
.with_bind(TcpBindOptions::default().with_bind_device(Some(String::new()))),
)
.unwrap();
assert_eq!(&none[70..75], &[0, 0, 0, 0, 0]);
assert_eq!(&empty[70..75], &[1, 0, 0, 0, 0]);
assert_eq!(&none[71..76], &[0, 0, 0, 0, 0]);
assert_eq!(&empty[71..76], &[1, 0, 0, 0, 0]);
let udp_none = encode_udp_bind_options(&UdpBindOptions::direct_connect()).unwrap();
let udp_empty = encode_udp_bind_options(
&UdpBindOptions::direct_connect().with_bind_device(Some(String::new())),
)
.unwrap();
assert_eq!(&udp_none[43..48], &[0, 0, 0, 0, 0]);
assert_eq!(&udp_empty[43..48], &[1, 0, 0, 0, 0]);
assert_eq!(&udp_none[44..49], &[0, 0, 0, 0, 0]);
assert_eq!(&udp_empty[44..49], &[1, 0, 0, 0, 0]);
let listen_none = encode_tcp_listen_options(&TcpListenOptions::direct_connect(
"192.0.2.1:11013".parse().unwrap(),
@@ -259,8 +262,8 @@ mod tests {
.with_bind(TcpBindOptions::default().with_bind_device(Some(String::new()))),
)
.unwrap();
assert_eq!(&listen_none[43..48], &[0, 0, 0, 0, 0]);
assert_eq!(&listen_empty[43..48], &[1, 0, 0, 0, 0]);
assert_eq!(&listen_none[44..49], &[0, 0, 0, 0, 0]);
assert_eq!(&listen_empty[44..49], &[1, 0, 0, 0, 0]);
}
#[test]
@@ -329,6 +332,34 @@ mod tests {
assert_eq!(udp[42], 5);
}
#[test]
fn encodes_socket_protection_request_in_existing_options() {
let remote = "192.0.2.2:11013".parse().unwrap();
let local = "0.0.0.0:11013".parse().unwrap();
let tcp_true =
encode_tcp_connect_options(&TcpConnectOptions::direct_connect(remote)).unwrap();
let tcp_false = encode_tcp_connect_options(
&TcpConnectOptions::direct_connect(remote)
.with_bind(TcpBindOptions::default().with_need_protect(false)),
)
.unwrap();
assert_eq!(tcp_true[70], 1);
assert_eq!(tcp_false[70], 0);
let listen_true =
encode_tcp_listen_options(&TcpListenOptions::direct_connect(local)).unwrap();
let listen_false = encode_tcp_listen_options(&TcpListenOptions::proxy_nat(local)).unwrap();
assert_eq!(listen_true[43], 1);
assert_eq!(listen_false[43], 0);
let udp_true = encode_udp_bind_options(&UdpBindOptions::direct_connect()).unwrap();
let udp_false =
encode_udp_bind_options(&UdpBindOptions::socks5().with_need_protect(false)).unwrap();
assert_eq!(udp_true[43], 1);
assert_eq!(udp_false[43], 0);
}
#[test]
fn encodes_gateway_purposes_with_stable_values() {
let remote = "192.0.2.2:443".parse().unwrap();