diff --git a/.github/instructions/rust_rules.instructions.md b/.github/instructions/rust_rules.instructions.md new file mode 100644 index 0000000..75ac0e4 --- /dev/null +++ b/.github/instructions/rust_rules.instructions.md @@ -0,0 +1,135 @@ +--- +description: 'Rust programming language coding conventions and best practices' +applyTo: '**/*.rs' +--- + +# Rust Coding Conventions and Best Practices + +Follow idiomatic Rust practices and community standards when writing Rust code. + +These instructions are based on [The Rust Book](https://doc.rust-lang.org/book/), [Rust API Guidelines](https://rust-lang.github.io/api-guidelines/), [RFC 430 naming conventions](https://github.com/rust-lang/rfcs/blob/master/text/0430-finalizing-naming-conventions.md), and the broader Rust community at [users.rust-lang.org](https://users.rust-lang.org). + +## General Instructions + +- Always prioritize readability, safety, and maintainability. +- Use strong typing and leverage Rust's ownership system for memory safety. +- Break down complex functions into smaller, more manageable functions. +- For algorithm-related code, include explanations of the approach used. +- Write code with good maintainability practices, including comments on why certain design decisions were made. +- Handle errors gracefully using `Result` and provide meaningful error messages. +- For external dependencies, mention their usage and purpose in documentation. +- Use consistent naming conventions following [RFC 430](https://github.com/rust-lang/rfcs/blob/master/text/0430-finalizing-naming-conventions.md). +- Write idiomatic, safe, and efficient Rust code that follows the borrow checker's rules. +- Ensure code compiles without warnings. + +## Patterns to Follow + +- Use modules (`mod`) and public interfaces (`pub`) to encapsulate logic. +- Handle errors properly using `?`, `match`, or `if let`. +- Use `serde` for serialization and `thiserror` or `anyhow` for custom errors. +- Implement traits to abstract services or external dependencies. +- Structure async code using `async/await` and `tokio` or `async-std`. +- Prefer enums over flags and states for type safety. +- Use builders for complex object creation. +- Split binary and library code (`main.rs` vs `lib.rs`) for testability and reuse. +- Use `rayon` for data parallelism and CPU-bound tasks. +- Use iterators instead of index-based loops as they're often faster and safer. +- Use `&str` instead of `String` for function parameters when you don't need ownership. +- Prefer borrowing and zero-copy operations to avoid unnecessary allocations. + +### Ownership, Borrowing, and Lifetimes + +- Prefer borrowing (`&T`) over cloning unless ownership transfer is necessary. +- Use `&mut T` when you need to modify borrowed data. +- Explicitly annotate lifetimes when the compiler cannot infer them. +- Use `Rc` for single-threaded reference counting and `Arc` for thread-safe reference counting. +- Use `RefCell` for interior mutability in single-threaded contexts and `Mutex` or `RwLock` for multi-threaded contexts. + +## Patterns to Avoid + +- Don't use `unwrap()` or `expect()` unless absolutely necessary—prefer proper error handling. +- Avoid panics in library code—return `Result` instead. +- Don't rely on global mutable state—use dependency injection or thread-safe containers. +- Avoid deeply nested logic—refactor with functions or combinators. +- Don't ignore warnings—treat them as errors during CI. +- Avoid `unsafe` unless required and fully documented. +- Don't overuse `clone()`, use borrowing instead of cloning unless ownership transfer is needed. +- Avoid premature `collect()`, keep iterators lazy until you actually need the collection. +- Avoid unnecessary allocations—prefer borrowing and zero-copy operations. + +## Code Style and Formatting + +- Follow the Rust Style Guide and use `rustfmt` for automatic formatting. +- Keep lines under 100 characters when possible. +- Place function and struct documentation immediately before the item using `///`. +- Use `cargo clippy` to catch common mistakes and enforce best practices. + +## Error Handling + +- Use `Result` for recoverable errors and `panic!` only for unrecoverable errors. +- Prefer `?` operator over `unwrap()` or `expect()` for error propagation. +- Create custom error types using `thiserror` or implement `std::error::Error`. +- Use `Option` for values that may or may not exist. +- Provide meaningful error messages and context. +- Error types should be meaningful and well-behaved (implement standard traits). +- Validate function arguments and return appropriate errors for invalid input. + +## API Design Guidelines + +### Common Traits Implementation +Eagerly implement common traits where appropriate: +- `Copy`, `Clone`, `Eq`, `PartialEq`, `Ord`, `PartialOrd`, `Hash`, `Debug`, `Display`, `Default` +- Use standard conversion traits: `From`, `AsRef`, `AsMut` +- Collections should implement `FromIterator` and `Extend` +- Note: `Send` and `Sync` are auto-implemented by the compiler when safe; avoid manual implementation unless using `unsafe` code + +### Type Safety and Predictability +- Use newtypes to provide static distinctions +- Arguments should convey meaning through types; prefer specific types over generic `bool` parameters +- Use `Option` appropriately for truly optional values +- Functions with a clear receiver should be methods +- Only smart pointers should implement `Deref` and `DerefMut` + +### Future Proofing +- Use sealed traits to protect against downstream implementations +- Structs should have private fields +- Functions should validate their arguments +- All public types must implement `Debug` + +## Testing and Documentation + +- Write comprehensive unit tests using `#[cfg(test)]` modules and `#[test]` annotations. +- Use test modules alongside the code they test (`mod tests { ... }`). +- Write integration tests in `tests/` directory with descriptive filenames. +- Write clear and concise comments for each function, struct, enum, and complex logic. +- Ensure functions have descriptive names and include comprehensive documentation. +- Document all public APIs with rustdoc (`///` comments) following the [API Guidelines](https://rust-lang.github.io/api-guidelines/). +- Use `#[doc(hidden)]` to hide implementation details from public documentation. +- Document error conditions, panic scenarios, and safety considerations. +- Examples should use `?` operator, not `unwrap()` or deprecated `try!` macro. + +## Project Organization + +- Use semantic versioning in `Cargo.toml`. +- Include comprehensive metadata: `description`, `license`, `repository`, `keywords`, `categories`. +- Use feature flags for optional functionality. +- Organize code into modules using `mod.rs` or named files. +- Keep `main.rs` or `lib.rs` minimal - move logic to modules. + +## Quality Checklist + +Before publishing or reviewing Rust code, ensure: + +### Core Requirements +- [ ] **Naming**: Follows RFC 430 naming conventions +- [ ] **Traits**: Implements `Debug`, `Clone`, `PartialEq` where appropriate +- [ ] **Error Handling**: Uses `Result` and provides meaningful error types +- [ ] **Documentation**: All public items have rustdoc comments with examples +- [ ] **Testing**: Comprehensive test coverage including edge cases + +### Safety and Quality +- [ ] **Safety**: No unnecessary `unsafe` code, proper error handling +- [ ] **Performance**: Efficient use of iterators, minimal allocations +- [ ] **API Design**: Functions are predictable, flexible, and type-safe +- [ ] **Future Proofing**: Private fields in structs, sealed traits where appropriate +- [ ] **Tooling**: Code passes `cargo fmt`, `cargo clippy`, and `cargo test` diff --git a/.github/instructions/self-explanatory-code-commenting.instructions.md b/.github/instructions/self-explanatory-code-commenting.instructions.md new file mode 100644 index 0000000..03a559f --- /dev/null +++ b/.github/instructions/self-explanatory-code-commenting.instructions.md @@ -0,0 +1,162 @@ +--- +description: 'Guidelines for GitHub Copilot to write comments to achieve self-explanatory code with less comments. Examples are in JavaScript but it should work on any language that has comments.' +applyTo: '**' +--- + +# Self-explanatory Code Commenting Instructions + +## Core Principle +**Write code that speaks for itself. Comment only when necessary to explain WHY, not WHAT.** +We do not need comments most of the time. + +## Commenting Guidelines + +### ❌ AVOID These Comment Types + +**Obvious Comments** +```javascript +// Bad: States the obvious +let counter = 0; // Initialize counter to zero +counter++; // Increment counter by one +``` + +**Redundant Comments** +```javascript +// Bad: Comment repeats the code +function getUserName() { + return user.name; // Return the user's name +} +``` + +**Outdated Comments** +```javascript +// Bad: Comment doesn't match the code +// Calculate tax at 5% rate +const tax = price * 0.08; // Actually 8% +``` + +### ✅ WRITE These Comment Types + +**Complex Business Logic** +```javascript +// Good: Explains WHY this specific calculation +// Apply progressive tax brackets: 10% up to 10k, 20% above +const tax = calculateProgressiveTax(income, [0.10, 0.20], [10000]); +``` + +**Non-obvious Algorithms** +```javascript +// Good: Explains the algorithm choice +// Using Floyd-Warshall for all-pairs shortest paths +// because we need distances between all nodes +for (let k = 0; k < vertices; k++) { + for (let i = 0; i < vertices; i++) { + for (let j = 0; j < vertices; j++) { + // ... implementation + } + } +} +``` + +**Regex Patterns** +```javascript +// Good: Explains what the regex matches +// Match email format: username@domain.extension +const emailPattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; +``` + +**API Constraints or Gotchas** +```javascript +// Good: Explains external constraint +// GitHub API rate limit: 5000 requests/hour for authenticated users +await rateLimiter.wait(); +const response = await fetch(githubApiUrl); +``` + +## Decision Framework + +Before writing a comment, ask: +1. **Is the code self-explanatory?** → No comment needed +2. **Would a better variable/function name eliminate the need?** → Refactor instead +3. **Does this explain WHY, not WHAT?** → Good comment +4. **Will this help future maintainers?** → Good comment + +## Special Cases for Comments + +### Public APIs +```javascript +/** + * Calculate compound interest using the standard formula. + * + * @param {number} principal - Initial amount invested + * @param {number} rate - Annual interest rate (as decimal, e.g., 0.05 for 5%) + * @param {number} time - Time period in years + * @param {number} compoundFrequency - How many times per year interest compounds (default: 1) + * @returns {number} Final amount after compound interest + */ +function calculateCompoundInterest(principal, rate, time, compoundFrequency = 1) { + // ... implementation +} +``` + +### Configuration and Constants +```javascript +// Good: Explains the source or reasoning +const MAX_RETRIES = 3; // Based on network reliability studies +const API_TIMEOUT = 5000; // AWS Lambda timeout is 15s, leaving buffer +``` + +### Annotations +```javascript +// TODO: Replace with proper user authentication after security review +// FIXME: Memory leak in production - investigate connection pooling +// HACK: Workaround for bug in library v2.1.0 - remove after upgrade +// NOTE: This implementation assumes UTC timezone for all calculations +// WARNING: This function modifies the original array instead of creating a copy +// PERF: Consider caching this result if called frequently in hot path +// SECURITY: Validate input to prevent SQL injection before using in query +// BUG: Edge case failure when array is empty - needs investigation +// REFACTOR: Extract this logic into separate utility function for reusability +// DEPRECATED: Use newApiFunction() instead - this will be removed in v3.0 +``` + +## Anti-Patterns to Avoid + +### Dead Code Comments +```javascript +// Bad: Don't comment out code +// const oldFunction = () => { ... }; +const newFunction = () => { ... }; +``` + +### Changelog Comments +```javascript +// Bad: Don't maintain history in comments +// Modified by John on 2023-01-15 +// Fixed bug reported by Sarah on 2023-02-03 +function processData() { + // ... implementation +} +``` + +### Divider Comments +```javascript +// Bad: Don't use decorative comments +//===================================== +// UTILITY FUNCTIONS +//===================================== +``` + +## Quality Checklist + +Before committing, ensure your comments: +- [ ] Explain WHY, not WHAT +- [ ] Are grammatically correct and clear +- [ ] Will remain accurate as code evolves +- [ ] Add genuine value to code understanding +- [ ] Are placed appropriately (above the code they describe) +- [ ] Use proper spelling and professional language + +## Summary + +Remember: **The best comment is the one you don't need to write because the code is self-documenting.** \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index e29b473..06ea5c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2087,7 +2087,7 @@ dependencies = [ [[package]] name = "telemt" -version = "3.1.3" +version = "3.3.15" dependencies = [ "aes", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index 2f7ea3c..afdf5b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -73,3 +73,6 @@ futures = "0.3" [[bench]] name = "crypto_bench" harness = false + +[profile.release] +lto = "thin" diff --git a/README.md b/README.md index b59e509..8807393 100644 --- a/README.md +++ b/README.md @@ -264,6 +264,11 @@ git clone https://github.com/telemt/telemt cd telemt # Starting Release Build cargo build --release + +# Low-RAM devices (1 GB, e.g. NanoPi Neo3 / Raspberry Pi Zero 2): +# release profile uses lto = "thin" to reduce peak linker memory. +# If your custom toolchain overrides profiles, avoid enabling fat LTO. + # Move to /bin mv ./target/release/telemt /bin # Make executable @@ -272,6 +277,12 @@ chmod +x /bin/telemt telemt config.toml ``` +### OpenBSD +- Build and service setup guide: [OpenBSD Guide (EN)](docs/OPENBSD.en.md) +- Example rc.d script: [contrib/openbsd/telemt.rcd](contrib/openbsd/telemt.rcd) +- Status: OpenBSD sandbox hardening with `pledge(2)` and `unveil(2)` is not implemented yet. + + ## Why Rust? - Long-running reliability and idempotent behavior - Rust's deterministic resource management - RAII diff --git a/contrib/openbsd/telemt.rcd b/contrib/openbsd/telemt.rcd new file mode 100644 index 0000000..c3dece1 --- /dev/null +++ b/contrib/openbsd/telemt.rcd @@ -0,0 +1,16 @@ +#!/bin/ksh +# /etc/rc.d/telemt +# +# rc.d(8) script for Telemt MTProxy daemon. +# Tokio runtime does not daemonize itself, so rc_bg=YES is used. + +daemon="/usr/local/bin/telemt" +daemon_user="_telemt" +daemon_flags="/etc/telemt/config.toml" + +. /etc/rc.d/rc.subr + +rc_bg=YES +rc_reload=NO + +rc_cmd $1 diff --git a/docs/OPENBSD.en.md b/docs/OPENBSD.en.md new file mode 100644 index 0000000..943e599 --- /dev/null +++ b/docs/OPENBSD.en.md @@ -0,0 +1,132 @@ +# Telemt on OpenBSD (Build, Run, and rc.d) + +This guide covers a practical OpenBSD deployment flow for Telemt: +- build from source, +- install binary and config, +- run as an rc.d daemon, +- verify basic runtime behavior. + +## 1. Prerequisites + +Install required packages: + +```sh +doas pkg_add rust git +``` + +Notes: +- Telemt release installer (`install.sh`) is Linux-only. +- On OpenBSD, use source build with `cargo`. + +## 2. Build from source + +```sh +git clone https://github.com/telemt/telemt +cd telemt +cargo build --release +./target/release/telemt --version +``` + +For low-RAM systems, this repository already uses `lto = "thin"` in release profile. + +## 3. Install binary and config + +```sh +doas install -d -m 0755 /usr/local/bin +doas install -m 0755 ./target/release/telemt /usr/local/bin/telemt + +doas install -d -m 0750 /etc/telemt +doas install -m 0640 ./config.toml /etc/telemt/config.toml +``` + +## 4. Create runtime user + +```sh +doas useradd -L daemon -s /sbin/nologin -d /var/empty _telemt +``` + +If `_telemt` already exists, continue. + +## 5. Install rc.d service + +Install the provided script: + +```sh +doas install -m 0555 ./contrib/openbsd/telemt.rcd /etc/rc.d/telemt +``` + +Enable and start: + +```sh +doas rcctl enable telemt +# Optional: send daemon output to syslog +#doas rcctl set telemt logger daemon.info + +doas rcctl start telemt +``` + +Service controls: + +```sh +doas rcctl check telemt +doas rcctl restart telemt +doas rcctl stop telemt +``` + +## 6. Resource limits (recommended) + +OpenBSD rc.d can apply limits via login class. Add class `telemt` and assign it to `_telemt`. + +Example class entry: + +```text +telemt:\ + :openfiles-cur=8192:openfiles-max=16384:\ + :datasize-cur=768M:datasize-max=1024M:\ + :coredumpsize=0:\ + :tc=daemon: +``` + +These values are conservative defaults for small and medium deployments. +Increase `openfiles-*` only if logs show descriptor exhaustion under load. + +Then rebuild database and assign class: + +```sh +doas cap_mkdb /etc/login.conf +#doas usermod -L telemt _telemt +``` + +Uncomment `usermod` if you want this class bound to the Telemt user. + +## 7. Functional smoke test + +1. Validate service state: + +```sh +doas rcctl check telemt +``` + +2. Check listener is present (replace 443 if needed): + +```sh +netstat -n -f inet -p tcp | grep LISTEN | grep '\.443' +``` + +3. Verify process user: + +```sh +ps -o user,pid,command -ax | grep telemt | grep -v grep +``` + +4. If startup fails, debug in foreground: + +```sh +RUST_LOG=debug /usr/local/bin/telemt /etc/telemt/config.toml +``` + +## 8. OpenBSD-specific caveats + +- OpenBSD does not support per-socket keepalive retries/interval tuning in the same way as Linux. +- Telemt source already uses target-aware cfg gates for keepalive setup. +- Use rc.d/rcctl, not systemd. diff --git a/install.sh b/install.sh index 9d96f0e..2dd207b 100644 --- a/install.sh +++ b/install.sh @@ -19,6 +19,15 @@ need_cmd() { command -v "$1" >/dev/null 2>&1 || die "required command not found: $1" } +detect_os() { + os="$(uname -s)" + case "$os" in + Linux) printf 'linux\n' ;; + OpenBSD) printf 'openbsd\n' ;; + *) printf '%s\n' "$os" ;; + esac +} + detect_arch() { arch="$(uname -m)" case "$arch" in @@ -68,6 +77,19 @@ need_cmd grep need_cmd install ARCH="$(detect_arch)" +OS="$(detect_os)" + +if [ "$OS" != "linux" ]; then + case "$OS" in + openbsd) + die "install.sh installs only Linux release artifacts. On OpenBSD, build from source (see docs/OPENBSD.en.md)." + ;; + *) + die "unsupported operating system for install.sh: $OS" + ;; + esac +fi + LIBC="$(detect_libc)" case "$VERSION" in diff --git a/src/transport/middle_proxy/handshake.rs b/src/transport/middle_proxy/handshake.rs index 245a331..0d7626c 100644 --- a/src/transport/middle_proxy/handshake.rs +++ b/src/transport/middle_proxy/handshake.rs @@ -199,10 +199,26 @@ impl MePool { fn configure_keepalive(stream: &TcpStream) -> std::io::Result<()> { let sock = SockRef::from(stream); - let ka = TcpKeepalive::new() - .with_time(Duration::from_secs(30)) - .with_interval(Duration::from_secs(10)) - .with_retries(3); + let ka = TcpKeepalive::new().with_time(Duration::from_secs(30)); + + // Mirror socket2 v0.5.10 target gate for with_retries(), the stricter method. + #[cfg(any( + target_os = "android", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "fuchsia", + target_os = "illumos", + target_os = "ios", + target_os = "visionos", + target_os = "linux", + target_os = "macos", + target_os = "netbsd", + target_os = "tvos", + target_os = "watchos", + target_os = "cygwin", + ))] + let ka = ka.with_interval(Duration::from_secs(10)).with_retries(3); + sock.set_tcp_keepalive(&ka)?; sock.set_keepalive(true)?; Ok(()) @@ -697,3 +713,66 @@ fn hex_dump(data: &[u8]) -> String { } out } + +#[cfg(test)] +mod tests { + use super::*; + use std::io::ErrorKind; + use tokio::net::{TcpListener, TcpStream}; + + #[tokio::test] + async fn test_configure_keepalive_loopback() { + let listener = match TcpListener::bind("127.0.0.1:0").await { + Ok(listener) => listener, + Err(error) if error.kind() == ErrorKind::PermissionDenied => return, + Err(error) => panic!("bind failed: {error}"), + }; + + let addr = match listener.local_addr() { + Ok(addr) => addr, + Err(error) => panic!("local_addr failed: {error}"), + }; + + let stream = match TcpStream::connect(addr).await { + Ok(stream) => stream, + Err(error) if error.kind() == ErrorKind::PermissionDenied => return, + Err(error) => panic!("connect failed: {error}"), + }; + + if let Err(error) = MePool::configure_keepalive(&stream) { + if error.kind() == ErrorKind::PermissionDenied { + return; + } + panic!("configure_keepalive failed: {error}"); + } + } + + #[test] + #[cfg(target_os = "openbsd")] + fn test_openbsd_keepalive_cfg_path_compiles() { + let _ka = TcpKeepalive::new().with_time(Duration::from_secs(30)); + } + + #[test] + #[cfg(any( + target_os = "android", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "fuchsia", + target_os = "illumos", + target_os = "ios", + target_os = "visionos", + target_os = "linux", + target_os = "macos", + target_os = "netbsd", + target_os = "tvos", + target_os = "watchos", + target_os = "cygwin", + ))] + fn test_retry_keepalive_cfg_path_compiles() { + let _ka = TcpKeepalive::new() + .with_time(Duration::from_secs(30)) + .with_interval(Duration::from_secs(10)) + .with_retries(3); + } +} diff --git a/src/transport/socket.rs b/src/transport/socket.rs index f1f8d5c..54eb143 100644 --- a/src/transport/socket.rs +++ b/src/transport/socket.rs @@ -1,6 +1,8 @@ //! TCP Socket Configuration +#[cfg(target_os = "linux")] use std::collections::HashSet; +#[cfg(target_os = "linux")] use std::fs; use std::io::Result; use std::net::{SocketAddr, IpAddr}; @@ -44,6 +46,7 @@ pub fn configure_tcp_socket( pub fn configure_client_socket( stream: &TcpStream, keepalive_secs: u64, + #[cfg_attr(not(target_os = "linux"), allow(unused_variables))] ack_timeout_secs: u64, ) -> Result<()> { let socket = socket2::SockRef::from(stream); @@ -373,6 +376,7 @@ fn listening_inodes_for_port(addr: SocketAddr) -> HashSet { mod tests { use super::*; use std::io::ErrorKind; + use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::TcpListener; #[tokio::test] @@ -396,6 +400,115 @@ mod tests { panic!("configure_tcp_socket failed: {e}"); } } + + #[tokio::test] + async fn test_configure_client_socket() { + let listener = match TcpListener::bind("127.0.0.1:0").await { + Ok(l) => l, + Err(e) if e.kind() == ErrorKind::PermissionDenied => return, + Err(e) => panic!("bind failed: {e}"), + }; + let addr = match listener.local_addr() { + Ok(addr) => addr, + Err(e) => panic!("local_addr failed: {e}"), + }; + + let stream = match TcpStream::connect(addr).await { + Ok(s) => s, + Err(e) if e.kind() == ErrorKind::PermissionDenied => return, + Err(e) => panic!("connect failed: {e}"), + }; + + if let Err(e) = configure_client_socket(&stream, 30, 30) { + if e.kind() == ErrorKind::PermissionDenied { + return; + } + panic!("configure_client_socket failed: {e}"); + } + } + + #[tokio::test] + async fn test_configure_client_socket_zero_ack_timeout() { + let listener = match TcpListener::bind("127.0.0.1:0").await { + Ok(l) => l, + Err(e) if e.kind() == ErrorKind::PermissionDenied => return, + Err(e) => panic!("bind failed: {e}"), + }; + let addr = match listener.local_addr() { + Ok(addr) => addr, + Err(e) => panic!("local_addr failed: {e}"), + }; + + let stream = match TcpStream::connect(addr).await { + Ok(s) => s, + Err(e) if e.kind() == ErrorKind::PermissionDenied => return, + Err(e) => panic!("connect failed: {e}"), + }; + + if let Err(e) = configure_client_socket(&stream, 30, 0) { + if e.kind() == ErrorKind::PermissionDenied { + return; + } + panic!("configure_client_socket with zero ack timeout failed: {e}"); + } + } + + #[tokio::test] + async fn test_configure_client_socket_roundtrip_io() { + let listener = match TcpListener::bind("127.0.0.1:0").await { + Ok(l) => l, + Err(e) if e.kind() == ErrorKind::PermissionDenied => return, + Err(e) => panic!("bind failed: {e}"), + }; + let addr = match listener.local_addr() { + Ok(addr) => addr, + Err(e) => panic!("local_addr failed: {e}"), + }; + + let server_task = tokio::spawn(async move { + let (mut accepted, _) = match listener.accept().await { + Ok(v) => v, + Err(e) => panic!("accept failed: {e}"), + }; + let mut payload = [0u8; 4]; + if let Err(e) = accepted.read_exact(&mut payload).await { + panic!("server read_exact failed: {e}"); + } + if let Err(e) = accepted.write_all(b"pong").await { + panic!("server write_all failed: {e}"); + } + payload + }); + + let mut stream = match TcpStream::connect(addr).await { + Ok(s) => s, + Err(e) if e.kind() == ErrorKind::PermissionDenied => return, + Err(e) => panic!("connect failed: {e}"), + }; + + if let Err(e) = configure_client_socket(&stream, 30, 30) { + if e.kind() == ErrorKind::PermissionDenied { + return; + } + panic!("configure_client_socket failed: {e}"); + } + + if let Err(e) = stream.write_all(b"ping").await { + panic!("client write_all failed: {e}"); + } + + let mut reply = [0u8; 4]; + if let Err(e) = stream.read_exact(&mut reply).await { + panic!("client read_exact failed: {e}"); + } + assert_eq!(&reply, b"pong"); + + let server_seen = match server_task.await { + Ok(value) => value, + Err(e) => panic!("server task join failed: {e}"), + }; + assert_eq!(&server_seen, b"ping"); + } #[test] fn test_normalize_ip() {