├── .gitignore ├── src ├── udp │ ├── mod.rs │ └── udp_server.rs ├── omnip-web │ ├── src │ │ ├── vite-env.d.ts │ │ ├── App.css │ │ ├── main.tsx │ │ ├── Stats.tsx │ │ ├── Util.tsx │ │ ├── App.tsx │ │ ├── ProxyServer.tsx │ │ ├── assets │ │ │ └── react.svg │ │ └── QuicTunnel.tsx │ ├── dist │ │ ├── assets │ │ │ └── index-c38a9551.css │ │ ├── 404.html │ │ ├── index.html │ │ ├── vite.svg │ │ └── favicon.svg │ ├── public │ │ ├── 404.html │ │ ├── vite.svg │ │ └── favicon.svg │ ├── vite.config.ts │ ├── tsconfig.node.json │ ├── .gitignore │ ├── index.html │ ├── .eslintrc.cjs │ ├── tsconfig.json │ └── package.json ├── quic │ ├── mod.rs │ ├── quic_server.rs │ └── quic_client.rs ├── http │ ├── mod.rs │ ├── http_req.rs │ └── http_proxy_handler.rs ├── admin │ ├── mod.rs │ └── admin_server.rs ├── proxy_handler.rs ├── utils.rs ├── api.rs ├── server_info_bridge.rs ├── socks │ ├── mod.rs │ ├── socks_req.rs │ ├── socks_resp_parser.rs │ └── socks_proxy_handler.rs ├── bin │ └── omnip.rs ├── proxy_rule_manager.rs └── lib.rs ├── omnip1.jpg ├── omnip2.jpg ├── .cargo └── config.toml ├── Cargo.toml ├── README.md └── .github └── workflows └── main.yml /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /src/udp/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod udp_server; 2 | -------------------------------------------------------------------------------- /omnip1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neevek/omnip/HEAD/omnip1.jpg -------------------------------------------------------------------------------- /omnip2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neevek/omnip/HEAD/omnip2.jpg -------------------------------------------------------------------------------- /src/omnip-web/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/quic/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod quic_client; 2 | pub(crate) mod quic_server; 3 | -------------------------------------------------------------------------------- /src/omnip-web/dist/assets/index-c38a9551.css: -------------------------------------------------------------------------------- 1 | #root{margin:0 auto;padding:1rem;font-weight:100} 2 | -------------------------------------------------------------------------------- /src/omnip-web/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | margin: 0 auto; 3 | padding: 1rem; 4 | font-weight: 100; 5 | } 6 | -------------------------------------------------------------------------------- /src/http/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod http_proxy_handler; 2 | pub(crate) mod http_req; 3 | 4 | const INITIAL_HTTP_HEADER_SIZE: usize = 1024; 5 | const MAX_HTTP_HEADER_SIZE: usize = 1024 * 8; 6 | -------------------------------------------------------------------------------- /src/omnip-web/dist/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 404 Not Found 5 | 6 | 7 | 8 |
9 |

404 Not Found

10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/omnip-web/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 404 Not Found 5 | 6 | 7 | 8 |
9 |

404 Not Found

10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/omnip-web/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | base: '/web' 8 | }) 9 | -------------------------------------------------------------------------------- /src/omnip-web/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /src/omnip-web/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App.tsx' 4 | 5 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 6 | 7 | 8 | , 9 | ) 10 | -------------------------------------------------------------------------------- /src/omnip-web/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist-ssr 12 | *.local 13 | 14 | # Editor directories and files 15 | .vscode/* 16 | !.vscode/extensions.json 17 | .idea 18 | .DS_Store 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /src/omnip-web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Omnip 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/omnip-web/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { browser: true, es2020: true }, 3 | extends: [ 4 | 'eslint:recommended', 5 | 'plugin:@typescript-eslint/recommended', 6 | 'plugin:react-hooks/recommended', 7 | ], 8 | parser: '@typescript-eslint/parser', 9 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 10 | plugins: ['react-refresh'], 11 | rules: { 12 | 'react-refresh/only-export-components': 'warn', 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /src/omnip-web/dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Omnip 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | # works with NDK 28.0.13004108 2 | [target.aarch64-linux-android] 3 | ar = "/Users/neevek/Library/Android/ndk-bundle/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android-ar" 4 | linker = "/Users/neevek/Library/Android/ndk-bundle/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android35-clang" 5 | 6 | [target.armv7-linux-androideabi] 7 | ar = "/Users/neevek/Library/Android/ndk-bundle/toolchains/llvm/prebuilt/darwin-x86_64/bin/llvm-ar" 8 | linker = "/Users/neevek/Library/Android/ndk-bundle/toolchains/llvm/prebuilt/darwin-x86_64/bin/armv7a-linux-androideabi35-clang" 9 | -------------------------------------------------------------------------------- /src/omnip-web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 5 | "module": "ESNext", 6 | "skipLibCheck": true, 7 | 8 | /* Bundler mode */ 9 | "moduleResolution": "bundler", 10 | "allowImportingTsExtensions": true, 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "noEmit": true, 14 | "jsx": "react-jsx", 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true 21 | }, 22 | "include": ["src"], 23 | "references": [{ "path": "./tsconfig.node.json" }] 24 | } 25 | -------------------------------------------------------------------------------- /src/admin/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod admin_server; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[serde_with::skip_serializing_none] 5 | #[derive(Serialize, Deserialize)] 6 | pub struct JsonRequest { 7 | pub data: Option, 8 | } 9 | 10 | #[serde_with::skip_serializing_none] 11 | #[derive(Serialize, Deserialize, Debug)] 12 | pub struct JsonResponse { 13 | pub code: u16, 14 | pub msg: String, 15 | pub data: Option, 16 | } 17 | 18 | impl JsonResponse { 19 | pub fn succeed(data: Option) -> Self { 20 | JsonResponse:: { 21 | code: 0, 22 | msg: "".to_string(), 23 | data, 24 | } 25 | } 26 | 27 | pub fn fail(code: u16, msg: String) -> Self { 28 | JsonResponse:: { 29 | code, 30 | msg: msg.to_string(), 31 | data: None, 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/omnip-web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "omnip-web", 3 | "private": true, 4 | "version": "0.1.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@emotion/react": "^11.11.0", 14 | "@emotion/styled": "^11.11.0", 15 | "@mui/lab": "^5.0.0-alpha.129", 16 | "@mui/material": "^5.13.0", 17 | "react": "^18.3.1", 18 | "react-dom": "^18.3.1" 19 | }, 20 | "devDependencies": { 21 | "@types/react": "^18.3.23", 22 | "@types/react-dom": "^18.3.7", 23 | "@typescript-eslint/eslint-plugin": "^5.57.1", 24 | "@typescript-eslint/parser": "^5.57.1", 25 | "@vitejs/plugin-react": "^4.0.0", 26 | "eslint": "^8.38.0", 27 | "eslint-plugin-react-hooks": "^4.6.0", 28 | "eslint-plugin-react-refresh": "^0.3.4", 29 | "typescript": "^5.0.2", 30 | "vite": "^4.3.2" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/proxy_handler.rs: -------------------------------------------------------------------------------- 1 | use crate::{socks::SocksVersion, NetAddr, ProxyError}; 2 | use anyhow::Result; 3 | use async_trait::async_trait; 4 | use tokio::net::TcpStream; 5 | 6 | #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] 7 | pub(crate) enum OutboundType { 8 | HttpProxy, 9 | SocksProxy(SocksVersion), 10 | Direct, 11 | } 12 | 13 | pub enum ParseState<'a> { 14 | Pending, 15 | ContinueWithReply(Vec), 16 | FailWithReply((Vec, ProxyError)), 17 | ReceivedRequest(&'a NetAddr), 18 | } 19 | 20 | #[async_trait] 21 | pub(crate) trait ProxyHandler { 22 | fn parse(&mut self, data: &[u8]) -> ParseState; 23 | 24 | async fn reject(&self, inbound_stream: &mut TcpStream) -> Result<(), ProxyError>; 25 | 26 | async fn handle( 27 | &self, 28 | outbound_type: OutboundType, 29 | outbound_stream: &mut TcpStream, 30 | inbound_stream: &mut TcpStream, 31 | ) -> Result<(), ProxyError>; 32 | 33 | async fn handle_outbound_failure( 34 | &self, 35 | inbound_stream: &mut TcpStream, 36 | ) -> Result<(), ProxyError>; 37 | } 38 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use crate::ProxyError; 2 | use anyhow::{Context, Result}; 3 | use std::net::SocketAddr; 4 | use tokio::{io::AsyncReadExt, io::AsyncWriteExt, net::TcpStream}; 5 | 6 | pub async fn write_to_stream(stream: &mut TcpStream, buf: &[u8]) -> Result<(), ProxyError> { 7 | stream 8 | .write(buf) 9 | .await 10 | .context(format!( 11 | "failed to write to stream, addr: {:?}", 12 | get_peer_addr(stream) 13 | )) 14 | .map_err(ProxyError::Disconnected)?; 15 | Ok(()) 16 | } 17 | 18 | pub async fn read_from_stream(stream: &mut TcpStream, buf: &mut [u8]) -> Result { 19 | let size = stream 20 | .read(buf) 21 | .await 22 | .context(format!( 23 | "failed to read from stream, addr: {:?}", 24 | get_peer_addr(stream) 25 | )) 26 | .map_err(ProxyError::Disconnected)?; 27 | Ok(size) 28 | } 29 | 30 | pub fn get_peer_addr(st: &TcpStream) -> Result { 31 | st.peer_addr().map_err(|e| { 32 | log::error!("unexpected error: {e}"); 33 | ProxyError::GetPeerAddrFailed 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /src/omnip-web/src/Stats.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | import { Button, FormControl } from '@mui/material' 3 | import { fetchData, MessagePanel, MessageProps } from './Util' 4 | 5 | function App() { 6 | const [messages, setMessages] = useState([]); 7 | 8 | const loadStats = async () => { 9 | const stats = await fetchData("/api/stats"); 10 | if (stats) { 11 | const messages = [ 12 | { isError: false, text: "Total Tx (bytes): " + stats.total_tx_bytes }, 13 | { isError: false, text: "Total Rx (bytes): " + stats.total_rx_bytes }, 14 | { isError: false, text: "Ongoing Connections: " + stats.ongoing_connections }, 15 | { isError: false, text: "Total Connections: " + stats.total_connections }, 16 | ]; 17 | setMessages(messages); 18 | } 19 | } 20 | 21 | useEffect(() => { 22 | loadStats() 23 | }, []); 24 | 25 | return ( 26 | 27 | 0} messages={messages}/> 28 | 29 | 30 | ) 31 | } 32 | 33 | export default App 34 | -------------------------------------------------------------------------------- /src/omnip-web/src/Util.tsx: -------------------------------------------------------------------------------- 1 | import { Typography, Paper } from '@mui/material' 2 | 3 | export const fetchData = async (url: string) => { 4 | try { 5 | const resp = await fetch(url); 6 | const json = await resp.json(); 7 | return json.data; 8 | } catch (err) { 9 | console.log("failed to fetch data from url: " + url); 10 | } 11 | } 12 | 13 | export const postData = async (api: string, data: any) => { 14 | const requestOptions = { 15 | method: 'POST', 16 | headers: { 'Content-Type': 'application/json' }, 17 | body: JSON.stringify({ data: data }) 18 | }; 19 | return await fetch(api, requestOptions); 20 | } 21 | 22 | export interface MessageProps { 23 | isError: boolean, 24 | text: string 25 | } 26 | 27 | const Message = (props: MessageProps) => { 28 | return ( 29 | {props.text} 30 | ) 31 | } 32 | 33 | export const MessagePanel = (props: any) => { 34 | return ( 35 | props.visible && 36 | 37 | { 38 | props.messages.map((m: MessageProps, index: number) => 39 | 40 | ) 41 | } 42 | 43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /src/omnip-web/dist/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/omnip-web/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/api.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[async_trait::async_trait] 5 | pub trait Api: Send + Sync { 6 | fn set_prefer_upstream(&self, flag: bool); 7 | fn get_server_state(&self) -> ServerState; 8 | fn get_proxy_server_config(&self) -> ProxyServerConfig; 9 | fn get_quic_tunnel_config(&self) -> QuicTunnelConfig; 10 | fn get_server_stats(&self) -> ServerStats; 11 | async fn update_proxy_server_config(&self, config: ProxyServerConfig) -> Result<()>; 12 | async fn update_quic_tunnel_config(&self, config: QuicTunnelConfig) -> Result<()>; 13 | } 14 | 15 | #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)] 16 | pub struct ProxyServerConfig { 17 | pub server_addr: String, 18 | pub dot_server: String, 19 | pub name_servers: String, 20 | } 21 | 22 | #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)] 23 | pub struct QuicTunnelConfig { 24 | pub upstream_addr: String, 25 | pub cert: String, 26 | pub cipher: String, 27 | pub password: String, 28 | pub idle_timeout: u64, 29 | pub retry_interval: u64, 30 | pub hop_interval: u64, 31 | } 32 | 33 | #[derive(Serialize, Deserialize, Debug)] 34 | pub struct ServerState { 35 | pub prefer_upstream: bool, 36 | pub tunnel_state: String, 37 | } 38 | 39 | #[derive(Serialize, Deserialize, Debug)] 40 | pub struct ServerStats { 41 | pub total_rx_bytes: u64, 42 | pub total_tx_bytes: u64, 43 | pub total_connections: u32, 44 | pub ongoing_connections: u32, 45 | } 46 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "omnip" 3 | version = "0.7.6" 4 | edition = "2021" 5 | 6 | [lib] 7 | crate-type = ["dylib", "lib"] 8 | 9 | [dependencies] 10 | clap = { version = "4.5", features = ["derive"] } 11 | tokio = { version = "1.47", features = ["full"] } 12 | pretty_env_logger = "0.5" 13 | log = "0.4" 14 | chrono = "0.4" 15 | anyhow = "1.0" 16 | futures-util = "0.3" 17 | pin-utils = "0.1.0" 18 | num_cpus = "1.17" 19 | url = "2.5" 20 | regex = "1.11" 21 | notify = "6.1" 22 | rs-utilities = "0.4.3" 23 | # rs-utilities = { path = "../rs-utilities" } 24 | serde = { version = "1", features = ["derive"] } 25 | serde_json = "1" 26 | serde_with = "3" 27 | lazy_static = "1.5" 28 | async-trait = "0.1" 29 | byte-pool = { git = "https://github.com/neevek/byte-pool" } 30 | # rstun = { path = "../rstun" } 31 | rstun = { git = "https://github.com/neevek/rstun", tag = "v0.7.4" } 32 | hyper = { version = "0.14", features = ["full"]} 33 | http = "0.2" 34 | http-body = "0.4" 35 | mime_guess = "2.0" 36 | monolithica = { git = "https://github.com/neevek/monolithica" } 37 | base64 = "0.22" 38 | dashmap = "6" 39 | 40 | [dev-dependencies] 41 | jni = "0.21" 42 | android_logger = "0.15" 43 | 44 | [target.aarch64-linux-android.dependencies] 45 | jni = "0.21" 46 | android_logger = "0.15" 47 | 48 | [target.armv7-linux-androideabi.dependencies] 49 | jni = "0.21" 50 | android_logger = "0.15" 51 | 52 | [build-dependencies] 53 | monolithica = { git = "https://github.com/neevek/monolithica" } 54 | 55 | ######### build shared lib for android ########### 56 | # cargo build --target=aarch64-linux-android --release --lib 57 | 58 | [profile.release] 59 | opt-level = "z" 60 | strip = true 61 | lto ="fat" 62 | panic = "abort" 63 | -------------------------------------------------------------------------------- /src/quic/quic_server.rs: -------------------------------------------------------------------------------- 1 | use crate::QuicServerConfig; 2 | use anyhow::Result; 3 | use log::error; 4 | use std::net::SocketAddr; 5 | 6 | pub struct QuicServer { 7 | server: rstun::Server, 8 | } 9 | 10 | impl QuicServer { 11 | pub fn new(quic_server_config: QuicServerConfig) -> Self { 12 | log::info!( 13 | "quic server, tcp_upstream:{:?}, udp_upstream:{:?}", 14 | quic_server_config.tcp_upstream, 15 | quic_server_config.udp_upstream 16 | ); 17 | 18 | let config = rstun::ServerConfig { 19 | addr: quic_server_config.server_addr.to_string(), 20 | password: quic_server_config.common_cfg.password.to_string(), 21 | cert_path: quic_server_config.common_cfg.cert.to_string(), 22 | key_path: quic_server_config.common_cfg.key.to_string(), 23 | quic_timeout_ms: quic_server_config.common_cfg.quic_timeout_ms, 24 | tcp_timeout_ms: quic_server_config.common_cfg.tcp_timeout_ms, 25 | udp_timeout_ms: quic_server_config.common_cfg.udp_timeout_ms, 26 | default_tcp_upstream: quic_server_config.tcp_upstream, 27 | default_udp_upstream: quic_server_config.udp_upstream, 28 | dashboard_server: "".to_string(), 29 | dashboard_server_credential: "".to_string(), 30 | }; 31 | QuicServer { 32 | server: rstun::Server::new(config), 33 | } 34 | } 35 | 36 | pub fn bind(&mut self) -> Result { 37 | self.server.bind() 38 | } 39 | 40 | pub async fn serve(&self) { 41 | self.server 42 | .serve() 43 | .await 44 | .map_err(|e| error!("tunnel server failed: {e}")) 45 | .ok(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/omnip-web/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useState, SyntheticEvent } from 'react' 2 | import { Box, Tab, Typography } from '@mui/material' 3 | import TabContext from '@mui/lab/TabContext'; 4 | import TabList from '@mui/lab/TabList'; 5 | import TabPanel from '@mui/lab/TabPanel'; 6 | import { ThemeProvider, createTheme } from '@mui/material/styles'; 7 | import CssBaseline from '@mui/material/CssBaseline'; 8 | import ProxyServer from './ProxyServer' 9 | import QuicTunnel from './QuicTunnel' 10 | import Stats from './Stats' 11 | import './App.css' 12 | 13 | const darkTheme = createTheme({ 14 | typography: { 15 | fontSize: 11, 16 | h3: { 17 | fontStyle: 'italic', 18 | } 19 | }, 20 | palette: { 21 | mode: 'dark', 22 | }, 23 | }); 24 | 25 | function App() { 26 | const [value, setValue] = useState('1'); 27 | const handleChange = (_event: SyntheticEvent, newValue: string) => { 28 | setValue(newValue); 29 | }; 30 | 31 | return ( 32 | 33 | 34 | Omnip 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | ) 51 | } 52 | 53 | export default App 54 | -------------------------------------------------------------------------------- /src/server_info_bridge.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{Arc, Mutex}; 2 | 3 | use serde::Serialize; 4 | 5 | #[derive(Serialize)] 6 | pub(crate) struct ProxyTraffic { 7 | pub rx_bytes: u64, 8 | pub tx_bytes: u64, 9 | } 10 | 11 | #[derive(Serialize)] 12 | pub(crate) enum ServerStats { 13 | NewConnection, 14 | CloseConnection, 15 | Traffic(ProxyTraffic), 16 | } 17 | 18 | #[derive(Serialize)] 19 | #[allow(clippy::enum_variant_names)] 20 | pub(crate) enum ServerInfoType { 21 | ProxyDNSResolverType, 22 | ProxyServerState, 23 | ProxyTraffic, 24 | ProxyMessage, 25 | } 26 | 27 | #[derive(Serialize)] 28 | pub(crate) struct ServerInfo 29 | where 30 | T: ?Sized + Serialize, 31 | { 32 | pub info_type: ServerInfoType, 33 | pub data: Box, 34 | } 35 | 36 | impl ServerInfo 37 | where 38 | T: ?Sized + Serialize, 39 | { 40 | pub(crate) fn new(info_type: ServerInfoType, data: Box) -> Self { 41 | Self { info_type, data } 42 | } 43 | } 44 | 45 | #[derive(Clone)] 46 | #[allow(clippy::type_complexity)] 47 | pub(crate) struct ServerInfoBridge { 48 | listener: Option>>, 49 | } 50 | 51 | impl ServerInfoBridge { 52 | pub(crate) fn new() -> Self { 53 | ServerInfoBridge { listener: None } 54 | } 55 | 56 | pub(crate) fn set_listener(&mut self, listener: impl FnMut(&str) + 'static + Send + Sync) { 57 | self.listener = Some(Arc::new(Mutex::new(listener))); 58 | } 59 | 60 | pub(crate) fn has_listener(&self) -> bool { 61 | self.listener.is_some() 62 | } 63 | 64 | pub(crate) fn post_server_info(&self, data: &ServerInfo) 65 | where 66 | T: ?Sized + Serialize, 67 | { 68 | if let Some(ref listener) = self.listener { 69 | if let Ok(json) = serde_json::to_string(data) { 70 | listener.lock().unwrap()(json.as_str()); 71 | } 72 | } 73 | } 74 | 75 | pub(crate) fn post_server_log(&self, message: &str) { 76 | if let Some(ref listener) = self.listener { 77 | listener.lock().unwrap()(message); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/http/http_req.rs: -------------------------------------------------------------------------------- 1 | use crate::{http::INITIAL_HTTP_HEADER_SIZE, utils, Host, NetAddr, ProxyError, BUFFER_POOL}; 2 | use anyhow::anyhow; 3 | use anyhow::Result; 4 | use tokio::net::TcpStream; 5 | 6 | pub struct HttpReq {} 7 | 8 | impl HttpReq { 9 | pub async fn handshake( 10 | outbound_stream: &mut TcpStream, 11 | dst_addr: &NetAddr, 12 | ) -> Result, ProxyError> { 13 | let str_addr = match &dst_addr.host { 14 | Host::IP(ip) => ip.to_string(), 15 | Host::Domain(domain) => domain.to_string(), 16 | }; 17 | 18 | let mut buffer = BUFFER_POOL.alloc(INITIAL_HTTP_HEADER_SIZE); 19 | buffer.extend_from_slice("CONNECT ".as_bytes()); 20 | buffer.extend_from_slice(str_addr.as_bytes()); 21 | buffer.push(b':'); 22 | buffer.extend_from_slice(dst_addr.port.to_string().as_bytes()); 23 | buffer.extend_from_slice(" HTTP/1.1\r\n\r\n".as_bytes()); 24 | 25 | utils::write_to_stream(outbound_stream, buffer.as_ref()).await?; 26 | 27 | buffer.clear(); 28 | 29 | let partially_read_body_start_index; 30 | loop { 31 | let mut tmp_buffer = [0u8; 256]; 32 | let len = utils::read_from_stream(outbound_stream, &mut tmp_buffer).await?; 33 | if len == 0 { 34 | log::error!("failed to read request after retrying for 10 times"); 35 | return Err(ProxyError::BadRequest); 36 | } 37 | 38 | buffer.extend_from_slice(&tmp_buffer[..len]); 39 | let len = buffer.len(); 40 | if len > 4 { 41 | let start_index = len - 4; 42 | if &buffer[start_index..len] == b"\r\n\r\n" { 43 | partially_read_body_start_index = Some(len); 44 | break; 45 | } 46 | 47 | if let Some(index) = buffer.windows(4).position(|window| window == b"\r\n\r\n") { 48 | partially_read_body_start_index = Some(index + 4); 49 | break; 50 | } 51 | } 52 | } 53 | 54 | if let Some(index) = partially_read_body_start_index { 55 | if buffer[..].starts_with("HTTP/1.1 200 OK".as_bytes()) { 56 | if index <= buffer.len() { 57 | return Ok(Vec::with_capacity(0)); 58 | } else { 59 | return Ok(buffer[index..].into()); 60 | } 61 | } 62 | } 63 | 64 | Err(ProxyError::BadGateway(anyhow!( 65 | "invalid response from proxy server" 66 | ))) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/socks/mod.rs: -------------------------------------------------------------------------------- 1 | use log::error; 2 | use rs_utilities::ByteBuffer; 3 | use std::net::{IpAddr, SocketAddr}; 4 | pub(crate) mod socks_proxy_handler; 5 | pub(crate) mod socks_req; 6 | mod socks_resp_parser; 7 | 8 | pub type RespData = Vec; 9 | 10 | #[derive(PartialEq, Debug)] 11 | pub(crate) enum SocksError { 12 | GeneralError, 13 | V5ConnectionNotAllowed, 14 | V5NetworkUnreachable, 15 | V5HostUnreachable, 16 | V5ConnectionRefused, 17 | V5TTLExpired, 18 | V5CommandNotSupported, 19 | V5AddressTypeNotSupported, 20 | V5Unassigned, 21 | } 22 | 23 | #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] 24 | pub enum SocksVersion { 25 | V4, 26 | V5, 27 | } 28 | 29 | #[allow(unused)] 30 | pub(crate) enum SocksMethod { 31 | // I choose not to suport GSSAPI and basic Username/Password authentications. 32 | // For being secure, other protocols are supposed to be used instead of SocksV4/5. 33 | NoAuthentication, 34 | NoAcceptableMethods, 35 | } 36 | 37 | #[allow(unused)] 38 | pub(crate) enum RequestType { 39 | Connect, 40 | Bind, 41 | UDPAssociate, 42 | } 43 | 44 | #[allow(unused)] 45 | pub(crate) enum AddressType { 46 | Unknown, 47 | IPv4, 48 | DomainName, 49 | IPv6, 50 | } 51 | 52 | fn socks_reply(socks_version: SocksVersion, bnd_addr: SocketAddr, is_fail: bool) -> RespData { 53 | // <4-byte header> + <16-byte IP> + <2-byte port> 54 | const MAX_RESPONSE_LENGTH: usize = 22; 55 | let mut resp = ByteBuffer::::new(); 56 | match socks_version { 57 | SocksVersion::V4 => { 58 | if is_fail { 59 | resp.append("\x00\x5b".as_bytes()); 60 | } else { 61 | resp.append("\x00\x5a".as_bytes()); 62 | } 63 | resp.append(bnd_addr.port().to_be_bytes().as_ref()); 64 | 65 | match bnd_addr.ip() { 66 | IpAddr::V4(ip) => { 67 | resp.append(ip.octets().as_ref()); 68 | } 69 | IpAddr::V6(ip) => { 70 | error!("SOCKS4 doesn't support IPv6: {}", ip); 71 | resp.append("\x00\x00\x00\x00".as_bytes()); 72 | } 73 | } 74 | } 75 | SocksVersion::V5 => { 76 | if is_fail { 77 | resp.append("\x05\x01\x00".as_bytes()); 78 | } else { 79 | resp.append("\x05\x00\x00".as_bytes()); 80 | } 81 | 82 | match bnd_addr.ip() { 83 | IpAddr::V4(ip) => { 84 | resp.append_byte(0x01); 85 | resp.append(ip.octets().as_ref()); 86 | } 87 | IpAddr::V6(ip) => { 88 | resp.append_byte(0x04); 89 | resp.append(ip.octets().as_ref()); 90 | } 91 | } 92 | 93 | resp.append(bnd_addr.port().to_be_bytes().as_ref()); 94 | } 95 | } 96 | 97 | resp.as_bytes().into() 98 | } 99 | -------------------------------------------------------------------------------- /src/omnip-web/src/ProxyServer.tsx: -------------------------------------------------------------------------------- 1 | import { useState, BaseSyntheticEvent, useEffect } from 'react' 2 | import { Paper, ToggleButtonGroup, ToggleButton, Button, FormControl, TextField, Stack } from '@mui/material' 3 | import { fetchData, postData } from './Util' 4 | 5 | function ProxyServer() { 6 | const [proxyAddr, setProxyAddr] = useState(""); 7 | const [dotServer, setDotServer] = useState(""); 8 | const [nameServers, setNameServers] = useState(""); 9 | const [globalProxy, setGlobalProxy] = useState(false) 10 | 11 | const loadData = async () => { 12 | const serverState = await fetchData("/api/server_state"); 13 | if (serverState) { 14 | setGlobalProxy(serverState.prefer_upstream); 15 | } 16 | 17 | const serverConfig = await fetchData("/api/proxy_server_config"); 18 | if (serverConfig) { 19 | setProxyAddr(serverConfig.server_addr); 20 | setDotServer(serverConfig.dot_server); 21 | setNameServers(serverConfig.name_servers); 22 | } 23 | }; 24 | 25 | useEffect(() => { 26 | loadData() 27 | }, []); 28 | 29 | const handleChangeProxyMode = async ( 30 | _event: React.MouseEvent, 31 | mode: boolean, 32 | ) => { 33 | if (mode != null) { 34 | setGlobalProxy(mode); 35 | postData("/api/prefer_upstream", mode); 36 | } 37 | }; 38 | 39 | const handleApplyChanges = async (_event: BaseSyntheticEvent) => { 40 | const config = { 41 | server_addr: "", // this won't be updated 42 | dot_server: dotServer, 43 | name_servers: nameServers, 44 | }; 45 | 46 | await postData("/api/update_proxy_server_config", config); 47 | }; 48 | 49 | return ( 50 | 51 | 52 | 53 | setDotServer(e.target.value)} helperText="e.g. dns.google, dot.pub" /> 54 | setNameServers(e.target.value)} helperText="e.g. 8.8.8.8,1.1.1.1" /> 55 | 56 | 63 | Smart Proxy 64 | Global Proxy 65 | 66 | 67 | Smart Proxy is applicable only when QUIC tunnel is enabled. 68 | 69 | 70 | 71 | 72 | ) 73 | } 74 | 75 | export default ProxyServer 76 | -------------------------------------------------------------------------------- /src/quic/quic_client.rs: -------------------------------------------------------------------------------- 1 | use crate::QuicClientConfig; 2 | use anyhow::Result; 3 | use rstun::{TunnelConfig, TunnelMode, Upstream, UpstreamType}; 4 | use std::net::SocketAddr; 5 | 6 | pub struct QuicClient { 7 | client: rstun::Client, 8 | server_addr: String, 9 | } 10 | 11 | impl QuicClient { 12 | pub fn new(quic_client_config: QuicClientConfig) -> Self { 13 | let mut config = rstun::ClientConfig::default(); 14 | Self::set_config(&mut config, &quic_client_config); 15 | let server_addr = config.server_addr.clone(); 16 | QuicClient { 17 | client: rstun::Client::new(config), 18 | server_addr, 19 | } 20 | } 21 | 22 | pub async fn start_tcp_server(&mut self, addr: SocketAddr) -> Result { 23 | Ok(self.client.start_tcp_server(addr).await?.addr()) 24 | } 25 | 26 | pub async fn start_udp_server(&mut self, addr: SocketAddr) -> Result { 27 | Ok(self.client.start_udp_server(addr).await?.addr()) 28 | } 29 | 30 | pub fn connect_and_serve_async(&mut self) { 31 | self.client.connect_and_serve_async() 32 | } 33 | 34 | pub fn set_on_info_listener(&mut self, callback: impl FnMut(&str) + 'static + Send + Sync) { 35 | self.client.set_on_info_listener(callback); 36 | } 37 | 38 | pub fn set_enable_on_info_report(&mut self, enable: bool) { 39 | self.client.set_enable_on_info_report(enable); 40 | } 41 | 42 | pub fn stop(&self) { 43 | self.client.stop() 44 | } 45 | 46 | pub fn get_server_addr(&self) -> String { 47 | self.server_addr.clone() 48 | } 49 | 50 | pub fn get_state(&self) -> rstun::ClientState { 51 | self.client.get_state() 52 | } 53 | 54 | fn set_config(config: &mut rstun::ClientConfig, quic_client_config: &QuicClientConfig) { 55 | let mut tunnels = Vec::new(); 56 | if quic_client_config.local_tcp_server_addr.is_some() { 57 | tunnels.push(TunnelConfig { 58 | mode: TunnelMode::Out, 59 | local_server_addr: quic_client_config.local_tcp_server_addr, 60 | upstream: Upstream { 61 | upstream_addr: None, 62 | upstream_type: UpstreamType::Tcp, 63 | }, 64 | }); 65 | } 66 | 67 | if quic_client_config.local_udp_server_addr.is_some() { 68 | tunnels.push(TunnelConfig { 69 | mode: TunnelMode::Out, 70 | local_server_addr: quic_client_config.local_udp_server_addr, 71 | upstream: Upstream { 72 | upstream_addr: None, 73 | upstream_type: UpstreamType::Udp, 74 | }, 75 | }); 76 | } 77 | 78 | config.tunnels = tunnels; 79 | config.server_addr = quic_client_config.server_addr.to_string(); 80 | config.password = quic_client_config.common_cfg.password.clone(); 81 | config.cert_path = quic_client_config.common_cfg.cert.clone(); 82 | config.cipher = quic_client_config.common_cfg.cipher.clone(); 83 | config.quic_timeout_ms = quic_client_config.common_cfg.quic_timeout_ms; 84 | config.tcp_timeout_ms = quic_client_config.common_cfg.tcp_timeout_ms; 85 | config.udp_timeout_ms = quic_client_config.common_cfg.udp_timeout_ms; 86 | config.wait_before_retry_ms = quic_client_config.common_cfg.retry_interval_ms; 87 | config.hop_interval_ms = quic_client_config.common_cfg.hop_interval_ms; 88 | config.workers = quic_client_config.common_cfg.workers; 89 | config.dot_servers = quic_client_config.dot_servers.clone(); 90 | config.dns_servers = quic_client_config.name_servers.clone(); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/omnip-web/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/omnip-web/src/QuicTunnel.tsx: -------------------------------------------------------------------------------- 1 | import { useState, BaseSyntheticEvent, useEffect } from 'react' 2 | import { Paper, Button, FormControl, MenuItem, TextField, Stack } from '@mui/material' 3 | import { fetchData, postData, MessagePanel, MessageProps } from './Util' 4 | 5 | function QuicTunnel() { 6 | const [upstream_addr, setUpstream] = useState(""); 7 | const [password, setPassword] = useState(""); 8 | const [certPath, setCertPath] = useState(""); 9 | const [idleTimeout, setIdleTimeout] = useState(120000); 10 | const [retryInterval, setRetryInterval] = useState(5000); 11 | const [hopInterval, setHopInterval] = useState(5000); 12 | const [cipher, setCipher] = useState(""); 13 | const [tunnelState, setTunnelState] = useState("NotConnected"); 14 | const [messages, setMessages] = useState([]); 15 | 16 | const loadData = async () => { 17 | const serverState = await fetchData("/api/server_state"); 18 | if (serverState) { 19 | setTunnelState(serverState.tunnel_state); 20 | } 21 | 22 | const serverConfig = await fetchData("/api/quic_tunnel_config"); 23 | if (serverConfig) { 24 | setUpstream(serverConfig.upstream_addr); 25 | setPassword(serverConfig.password); 26 | setCertPath(serverConfig.cert); 27 | setIdleTimeout(serverConfig.idle_timeout); 28 | setRetryInterval(serverConfig.retry_interval); 29 | setHopInterval(serverConfig.hop_interval); 30 | setCipher(serverConfig.cipher); 31 | } 32 | }; 33 | 34 | useEffect(() => { 35 | loadData(); 36 | }, []); 37 | 38 | const updateMessage = (message: MessageProps) => { 39 | messages.push(message); 40 | setMessages([...messages]); 41 | } 42 | 43 | const updateQuicTunnelConfig = async (_event: BaseSyntheticEvent) => { 44 | const config = { 45 | upstream_addr, 46 | cert: certPath, 47 | cipher, 48 | password, 49 | idle_timeout: idleTimeout, 50 | retry_interval: retryInterval, 51 | hop_interval: hopInterval, 52 | }; 53 | 54 | postData("/api/update_quic_tunnel_config", config) 55 | .then((response) => response.json()) 56 | .then((data) => { 57 | if (data.code == 0) { 58 | updateMessage({ isError: false, text: "TunnelConfig updated!"}); 59 | } else { 60 | updateMessage({ isError: true, text: data.msg }); 61 | } 62 | 63 | loadData(); 64 | }) 65 | .catch((e) => { 66 | updateMessage({ isError: true, text: e.toString() }); 67 | }); 68 | }; 69 | 70 | return ( 71 | 72 | 73 | setUpstream(e.target.value)} helperText="e.g. example.com:3515, 1.2.3.4:3515" /> 74 | setPassword(e.target.value)}/> 75 | setCertPath(e.target.value)} helperText="leave this blank if connecting to the server using domain name"/> 76 | setIdleTimeout(parseInt(e.target.value))}/> 77 | setRetryInterval(parseInt(e.target.value))}/> 78 | setHopInterval(parseInt(e.target.value))}/> 79 | 80 | setCipher(e.target.value)} 85 | size='small' 86 | > 87 | chacha20-poly1305 88 | aes-256-gcm 89 | aes-128-gcm 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | {tunnelState} 98 | 0} messages={messages}/> 99 | 100 | 101 | 102 | ) 103 | } 104 | 105 | export default QuicTunnel 106 | -------------------------------------------------------------------------------- /src/udp/udp_server.rs: -------------------------------------------------------------------------------- 1 | use crate::{unspecified_socket_addr, BUFFER_POOL}; 2 | use anyhow::{Context, Result}; 3 | use dashmap::DashMap; 4 | use log::{debug, error, warn}; 5 | use std::sync::Mutex; 6 | use std::time::Duration; 7 | use std::{net::SocketAddr, sync::Arc}; 8 | use tokio::net::UdpSocket; 9 | 10 | pub const UDP_PACKET_SIZE: usize = 1500; 11 | 12 | pub struct UdpServer { 13 | state: Arc>, 14 | } 15 | 16 | #[derive(Debug, Clone)] 17 | struct State { 18 | serv_sock: Arc, 19 | sock_map: DashMap>, 20 | } 21 | 22 | impl UdpServer { 23 | pub async fn bind_and_start( 24 | server_addr: SocketAddr, 25 | upstream_addr: SocketAddr, 26 | use_sync: bool, 27 | udp_timeout_ms: u64, 28 | ) -> Result { 29 | let serv_sock = Arc::new(UdpSocket::bind(server_addr).await?); 30 | 31 | let state = Arc::new(Mutex::new(State { 32 | serv_sock: serv_sock.clone(), 33 | sock_map: DashMap::new(), 34 | })); 35 | let state_clone = state.clone(); 36 | 37 | let task = || async move { 38 | loop { 39 | let mut buf = BUFFER_POOL.alloc_and_fill(UDP_PACKET_SIZE); 40 | let state = state.clone(); 41 | match serv_sock.recv_from(&mut buf).await { 42 | Ok((size, addr)) => { 43 | tokio::spawn(async move { 44 | let state = state.lock().unwrap().clone(); 45 | let sock = 46 | Self::open_udp_socket(&state, addr, upstream_addr, udp_timeout_ms) 47 | .await?; 48 | sock.send(&buf[..size]).await.ok(); 49 | Ok::<(), anyhow::Error>(()) 50 | }); 51 | } 52 | Err(e) => { 53 | error!("failed to read from local udp socket, err: {e}"); 54 | } 55 | } 56 | } 57 | }; 58 | 59 | if use_sync { 60 | task().await; 61 | } else { 62 | tokio::spawn(task()); 63 | } 64 | 65 | Ok(Self { state: state_clone }) 66 | } 67 | 68 | async fn open_udp_socket( 69 | state: &State, 70 | inbound_addr: SocketAddr, 71 | outbound_addr: SocketAddr, 72 | udp_timeout_ms: u64, 73 | ) -> Result> { 74 | if let Some(s) = state.sock_map.get(&inbound_addr) { 75 | return Ok((*s).clone()); 76 | } 77 | 78 | let sock_map = state.sock_map.clone(); 79 | let serv_sock = state.serv_sock.clone(); 80 | 81 | let local_addr = unspecified_socket_addr(outbound_addr.is_ipv6()); 82 | let udp_socket = Arc::new(UdpSocket::bind(local_addr).await?); 83 | udp_socket.connect(outbound_addr).await?; 84 | let udp_socket_clone = udp_socket.clone(); 85 | sock_map.insert(inbound_addr, udp_socket.clone()); 86 | 87 | tokio::spawn(async move { 88 | debug!( 89 | "start udp session: {inbound_addr}, sockets: {}", 90 | sock_map.len() 91 | ); 92 | loop { 93 | let mut buf = BUFFER_POOL.alloc_and_fill(UDP_PACKET_SIZE); 94 | match tokio::time::timeout( 95 | Duration::from_millis(udp_timeout_ms), 96 | udp_socket.recv(&mut buf), 97 | ) 98 | .await 99 | { 100 | Ok(Ok(size)) => { 101 | unsafe { 102 | buf.set_len(size); 103 | } 104 | serv_sock.send_to(&buf[..size], inbound_addr).await.ok(); 105 | } 106 | e => { 107 | match e { 108 | Ok(Err(e)) => { 109 | warn!("failed read from udp socket, err: {e}"); 110 | } 111 | Err(_) => { 112 | // timedout 113 | } 114 | _ => unreachable!(""), 115 | } 116 | break; 117 | } 118 | } 119 | } 120 | 121 | sock_map.remove(&inbound_addr); 122 | debug!( 123 | "drop udp session({inbound_addr}), sockets: {}", 124 | sock_map.len() 125 | ); 126 | }); 127 | 128 | Ok(udp_socket_clone) 129 | } 130 | 131 | pub fn local_addr(&self) -> Result { 132 | self.state 133 | .lock() 134 | .unwrap() 135 | .serv_sock 136 | .local_addr() 137 | .context("") 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/socks/socks_req.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use log::error; 3 | use rs_utilities::ByteBuffer; 4 | use std::net::IpAddr; 5 | use tokio::net::TcpStream; 6 | 7 | use crate::{utils, Host, NetAddr, ProxyError}; 8 | 9 | use super::{ 10 | socks_resp_parser::{SocksRespParser, State}, 11 | SocksVersion, 12 | }; 13 | 14 | pub struct SocksReq {} 15 | 16 | impl SocksReq { 17 | pub async fn handshake( 18 | socks_version: SocksVersion, 19 | outbound_stream: &mut TcpStream, 20 | dst_addr: &NetAddr, 21 | ) -> Result<(), ProxyError> { 22 | let mut resp_parser = SocksRespParser::new(socks_version); 23 | let mut buf = [0u8; 512]; 24 | loop { 25 | match *resp_parser.state() { 26 | State::SelectMethod => { 27 | utils::write_to_stream(outbound_stream, "\x05\x01\x00".as_ref()).await?; 28 | } 29 | 30 | State::Connect => { 31 | let mut connect_command = ByteBuffer::<512>::new(); 32 | match *resp_parser.socks_version() { 33 | SocksVersion::V5 => { 34 | connect_command.append("\x05\x01\x00".as_ref()); 35 | match dst_addr.host { 36 | Host::IP(ip) => match ip { 37 | IpAddr::V4(ipv4) => { 38 | connect_command.append_byte(b'\x01'); 39 | connect_command.append(&ipv4.octets()); 40 | connect_command.append(&dst_addr.port.to_be_bytes()); 41 | } 42 | IpAddr::V6(ipv6) => { 43 | connect_command.append_byte(b'\x04'); 44 | connect_command.append(&ipv6.octets()); 45 | connect_command.append(&dst_addr.port.to_be_bytes()); 46 | } 47 | }, 48 | Host::Domain(ref domain) => { 49 | // domain name 50 | let domain_name = domain.as_bytes(); 51 | connect_command.append_byte(b'\x03'); 52 | connect_command.append_byte(domain_name.len() as u8); 53 | connect_command.append(domain_name); 54 | connect_command.append(&dst_addr.port.to_be_bytes()); 55 | } 56 | } 57 | } 58 | 59 | SocksVersion::V4 => { 60 | connect_command.append("\x04\x01".as_ref()); 61 | match dst_addr.host { 62 | Host::IP(IpAddr::V4(ipv4)) => { 63 | connect_command.append(&dst_addr.port.to_be_bytes()); 64 | connect_command.append(&ipv4.octets()); 65 | } 66 | // see https://www.openssh.com/txt/socks4a.protocol 67 | Host::IP(IpAddr::V6(ipv6)) => { 68 | let ipv6_str = ipv6.to_string(); 69 | connect_command.append(&dst_addr.port.to_be_bytes()); 70 | connect_command.append("\x00\x00\x00\x01\x00".as_bytes()); 71 | connect_command.append(ipv6_str.as_bytes()); 72 | connect_command.append_byte(0u8); 73 | } 74 | Host::Domain(ref domain) => { 75 | connect_command.append(&dst_addr.port.to_be_bytes()); 76 | connect_command.append("\x00\x00\x00\x01\x00".as_bytes()); 77 | connect_command.append(domain.as_bytes()); 78 | connect_command.append_byte(0u8); 79 | } 80 | } 81 | } 82 | } 83 | 84 | utils::write_to_stream(outbound_stream, connect_command.as_bytes()).await?; 85 | } 86 | _ => {} 87 | } 88 | 89 | let len = utils::read_from_stream(outbound_stream, &mut buf).await?; 90 | if len == 0 { 91 | log::error!("failed to read request after retrying for 10 times"); 92 | return Err(ProxyError::BadRequest); 93 | } 94 | 95 | if !resp_parser.advance(&buf[..len]) { 96 | error!( 97 | "connect failed: {:?}, dst_addr: {}", 98 | resp_parser.state(), 99 | dst_addr 100 | ); 101 | return Err(ProxyError::InternalError); 102 | } 103 | 104 | if resp_parser.state() == &State::NegotiationCompleted { 105 | break; 106 | } 107 | } 108 | 109 | Ok(()) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/omnip-web/dist/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/omnip-web/public/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/socks/socks_resp_parser.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::min; 2 | 3 | use log::{debug, error}; 4 | use rs_utilities::ByteBuffer; 5 | 6 | use super::{SocksError, SocksVersion}; 7 | 8 | #[derive(PartialEq, Debug)] 9 | pub(crate) enum State { 10 | SelectMethod, 11 | IdentifyingMethod, 12 | Connect, 13 | NegotiationCompleted, 14 | ErrorOccurred(SocksError), 15 | } 16 | 17 | pub(crate) struct SocksRespParser { 18 | socks_version: SocksVersion, 19 | state: State, 20 | buffer: ByteBuffer<512>, 21 | } 22 | 23 | impl SocksRespParser { 24 | pub fn new(socks_version: SocksVersion) -> Self { 25 | SocksRespParser { 26 | socks_version, 27 | state: match socks_version { 28 | SocksVersion::V5 => State::SelectMethod, 29 | SocksVersion::V4 => State::Connect, 30 | }, 31 | buffer: ByteBuffer::new(), 32 | } 33 | } 34 | 35 | pub fn socks_version(&self) -> &SocksVersion { 36 | &self.socks_version 37 | } 38 | 39 | pub fn advance(&mut self, buf: &[u8]) -> bool { 40 | if let State::ErrorOccurred(_) = self.state { 41 | return false; 42 | } 43 | 44 | if self.buffer.remaining() < buf.len() || !self.buffer.append(buf) { 45 | error!("unexpected large buffer: {}", self.buffer.len() + buf.len()); 46 | return self.fail_with_general_error(); 47 | } 48 | 49 | if self.state == State::SelectMethod { 50 | self.state = State::IdentifyingMethod; 51 | if self.buffer.len() != 2 { 52 | error!("unexpected socks response length: {}", self.buffer.len()); 53 | return self.fail_with_general_error(); 54 | } 55 | 56 | let bytes = self.buffer.as_bytes(); 57 | if self.socks_version == SocksVersion::V5 { 58 | if bytes != "\x05\x00".as_bytes() { 59 | error!("unexpected socks5 response code: {}", bytes[1]); 60 | return self.fail_with_general_error(); 61 | } 62 | self.state = State::Connect; 63 | self.buffer.clear(); 64 | return true; 65 | } 66 | } 67 | 68 | if self.state == State::Connect { 69 | let bytes = self.buffer.as_bytes(); 70 | let resp_size = bytes.len(); 71 | 72 | // exact 8 bytes for a Socks4 CONNECT response 73 | if self.socks_version == SocksVersion::V4 { 74 | if resp_size != 8 { 75 | error!("unexpected socks4 response length: {}", resp_size); 76 | return self.fail_with_general_error(); 77 | } 78 | 79 | if !bytes.starts_with("\x00\x5a".as_bytes()) { 80 | error!("unexpected socks4 response code: {}", bytes[1]); 81 | return self.fail_with_general_error(); 82 | } 83 | 84 | // the DSTPORT and DSTIP fields will be silently ignored 85 | 86 | self.state = State::NegotiationCompleted; 87 | return true; 88 | } 89 | 90 | // Socks5 91 | // at least 5 bytes is need to identify a valid Socks5 CONNECT response 92 | if resp_size > 4 { 93 | if !bytes.starts_with("\x05\x00\x00".as_bytes()) { 94 | self.state = match bytes[1] { 95 | 1u8 => State::ErrorOccurred(SocksError::GeneralError), 96 | 2u8 => State::ErrorOccurred(SocksError::V5ConnectionNotAllowed), 97 | 3u8 => State::ErrorOccurred(SocksError::V5NetworkUnreachable), 98 | 4u8 => State::ErrorOccurred(SocksError::V5HostUnreachable), 99 | 5u8 => State::ErrorOccurred(SocksError::V5ConnectionRefused), 100 | 6u8 => State::ErrorOccurred(SocksError::V5TTLExpired), 101 | 7u8 => State::ErrorOccurred(SocksError::V5CommandNotSupported), 102 | 8u8 => State::ErrorOccurred(SocksError::V5AddressTypeNotSupported), 103 | _ => State::ErrorOccurred(SocksError::V5Unassigned), 104 | }; 105 | debug!("error response: {:x?}", &bytes[..min(10, resp_size)]); 106 | return self.fail_with_general_error(); 107 | } 108 | 109 | // The BND.ADDR field indicates the IP address of the network interface 110 | // on the SOCKS server that was used to establish the outbound connection 111 | // to the destination host. This does not affect the connection between 112 | // the SOCKS client and the SOCKS server itself. The connection between 113 | // the SOCKS client and the SOCKS server remains the same. 114 | // The purpose of the BND.ADDR field is to allow the client to know the 115 | // specific network path that was used to establish the connection to the 116 | // destination host. This information may be useful for troubleshooting 117 | // or network analysis purposes. 118 | // 119 | // So we will simply drain and ignore the rest of the Socks5 response 120 | // so that we have a clean connection for later streaming 121 | 122 | let completed = match bytes[3] { 123 | 1u8 => { 124 | // <4-byte header> + <4-byte IP> + <2-byte port> 125 | resp_size == 4 + 4 + 2 126 | } 127 | 3u8 => { 128 | // domain name 129 | let domain_name_len = bytes[4]; 130 | // <4-byte header> + <1-byte domain length> + + <2-byte port> 131 | resp_size == (4 + 1 + domain_name_len + 2) as usize 132 | } 133 | 4u8 => { 134 | // ipv6 135 | // <4-byte header> + <16-byte IP> + <2-byte port> 136 | resp_size == 4 + 16 + 2 137 | } 138 | _ => false, 139 | }; 140 | 141 | if completed { 142 | self.state = State::NegotiationCompleted; 143 | } else { 144 | error!("unexpected socks5 response"); 145 | self.state = State::ErrorOccurred(SocksError::GeneralError); 146 | } 147 | 148 | return completed; 149 | } 150 | } 151 | 152 | false 153 | } 154 | 155 | fn fail_with_general_error(&mut self) -> bool { 156 | self.state = State::ErrorOccurred(SocksError::GeneralError); 157 | false 158 | } 159 | 160 | pub fn state(&self) -> &State { 161 | &self.state 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/bin/omnip.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use base64::prelude::*; 3 | use clap::builder::TypedValueParser as _; 4 | use clap::{builder::PossibleValuesParser, Parser}; 5 | use omnip::*; 6 | use rs_utilities::log_and_bail; 7 | use std::env; 8 | use url::Url; 9 | 10 | extern crate pretty_env_logger; 11 | 12 | fn main() -> Result<()> { 13 | let args = parse_args()?; 14 | if args.decode_base64 || print_args_as_base64(&args) { 15 | return Ok(()); 16 | } 17 | 18 | let log_filter = format!( 19 | "omnip={},rstun={},rs_utilities={}", 20 | args.loglevel, args.loglevel, args.loglevel 21 | ); 22 | rs_utilities::LogHelper::init_logger("omnip", log_filter.as_str()); 23 | 24 | let config = create_config( 25 | args.addr, 26 | args.upstream, 27 | args.dot_server, 28 | args.name_servers, 29 | args.proxy_rules_file, 30 | args.threads, 31 | args.watch_proxy_rules_change, 32 | args.tcp_nodelay, 33 | args.tcp_timeout_ms, 34 | args.udp_timeout_ms, 35 | )?; 36 | 37 | let common_quic_config = CommonQuicConfig { 38 | cert: args.cert, 39 | key: args.key, 40 | password: args.password, 41 | cipher: args.cipher, 42 | quic_timeout_ms: args.quic_timeout_ms, 43 | tcp_timeout_ms: args.tcp_timeout_ms, 44 | udp_timeout_ms: args.udp_timeout_ms, 45 | retry_interval_ms: args.retry_interval_ms, 46 | hop_interval_ms: args.hop_interval_ms, 47 | workers: args.threads, 48 | }; 49 | 50 | let mut server = Server::new(config, common_quic_config); 51 | server.run() 52 | } 53 | 54 | fn parse_args() -> Result { 55 | let args = OmnipArgs::parse(); 56 | if args.addr.starts_with("opp://") { 57 | match Url::parse(args.addr.as_str()) { 58 | Ok(url) => { 59 | let base64_args = url.host().context("invalid opp args")?.to_string(); 60 | let space_sep_args = String::from_utf8( 61 | BASE64_STANDARD 62 | .decode(base64_args) 63 | .context("invalid base64")?, 64 | )?; 65 | if args.decode_base64 { 66 | println!("{space_sep_args}"); 67 | // simply print the args and quit 68 | return Ok(args); 69 | } 70 | 71 | let parts: Vec = space_sep_args 72 | .split_whitespace() 73 | .map(String::from) 74 | .collect(); 75 | let mut vec_args = vec![String::from("")]; // empty string as the first arg (the programm name) 76 | vec_args.extend(parts); 77 | 78 | return Ok(OmnipArgs::parse_from(vec_args)); 79 | } 80 | _ => { 81 | log_and_bail!("invalid addr: {}", args.addr); 82 | } 83 | }; 84 | } 85 | Ok(args) 86 | } 87 | 88 | fn print_args_as_base64(args: &OmnipArgs) -> bool { 89 | if args.encode_base64 { 90 | let space_sep_args = env::args_os() 91 | .skip(1) 92 | .filter(|arg| arg != "-E" && arg != "--encode-base64") 93 | .map(|arg| { 94 | arg.into_string() 95 | .unwrap_or_else(|os_str| os_str.to_string_lossy().into_owned()) 96 | }) 97 | .collect::>() 98 | .join(" "); 99 | 100 | let base64_args = BASE64_STANDARD.encode(space_sep_args.as_bytes()); 101 | println!("opp://{base64_args}"); 102 | true 103 | } else { 104 | false 105 | } 106 | } 107 | 108 | #[derive(Parser, Debug)] 109 | #[command(author, version, about, long_about = None)] 110 | struct OmnipArgs { 111 | /// Server address [://][ip:]port 112 | /// for example: http://127.0.0.1:8000, http+quic://127.0.0.1:8000 113 | #[arg(short = 'a', long, verbatim_doc_comment, required = true)] 114 | addr: String, 115 | 116 | /// Upstream which the proxy server will relay traffic to based on proxy rules, 117 | /// [://]ip:port for example: http://127.0.0.1:8000, http+quic://127.0.0.1:8000 118 | #[arg(short = 'u', long, verbatim_doc_comment, default_value = "")] 119 | upstream: String, 120 | 121 | /// Path to the proxy rules file 122 | #[arg(short = 'r', long, default_value = "")] 123 | proxy_rules_file: String, 124 | 125 | /// Threads to run async tasks, default to number of cpu cores 126 | #[arg(short = 't', long, default_value = "0")] 127 | threads: usize, 128 | 129 | /// DoT (DNS-over-TLS) server, e.g. dns.google 130 | #[arg(long, default_value = "")] 131 | dot_server: String, 132 | 133 | /// comma saprated domain servers (E.g. 1.1.1.1,8.8.8.8), which will be used 134 | /// if no dot_server is specified, or system default if empty 135 | #[arg(long, verbatim_doc_comment, default_value = "")] 136 | name_servers: String, 137 | 138 | /// Applicable only for +quic protocols 139 | /// Path to the certificate file, if empty, a self-signed certificate 140 | /// with the domain "localhost" will be used 141 | #[arg(short = 'c', long, verbatim_doc_comment, default_value = "")] 142 | cert: String, 143 | 144 | /// Applicable only for +quic protocols 145 | /// Path to the key file, can be empty if no cert is provided 146 | #[arg(short = 'k', long, verbatim_doc_comment, default_value = "")] 147 | key: String, 148 | 149 | /// Applicable only for +quic protocols 150 | /// Password of the +quic server 151 | #[arg(short = 'p', long, verbatim_doc_comment, default_value = "")] 152 | password: String, 153 | 154 | /// Applicable only for +quic protocols 155 | /// Cipher for encryption 156 | #[arg(short = 'e', long, verbatim_doc_comment, default_value_t = String::from(rstun::SUPPORTED_CIPHER_SUITE_STRS[0]), 157 | value_parser = PossibleValuesParser::new(rstun::SUPPORTED_CIPHER_SUITE_STRS).map(|v| v.to_string()))] 158 | cipher: String, 159 | 160 | /// Applicable only for quic protocol as upstream 161 | /// Max idle timeout for the QUIC connections 162 | #[arg(short = 'i', long, verbatim_doc_comment, default_value = "120000")] 163 | quic_timeout_ms: u64, 164 | 165 | /// Read timeout in milliseconds for TCP connections 166 | #[arg(long, verbatim_doc_comment, default_value = "30000")] 167 | tcp_timeout_ms: u64, 168 | 169 | /// Read timeout in milliseconds for UDP connections 170 | #[arg(long, verbatim_doc_comment, default_value = "5000")] 171 | udp_timeout_ms: u64, 172 | 173 | /// Applicable only for quic protocol as upstream 174 | /// Max idle timeout for the QUIC connections 175 | #[arg(short = 'R', long, verbatim_doc_comment, default_value = "5000")] 176 | retry_interval_ms: u64, 177 | 178 | /// Applicable only for quic protocol as upstream 179 | /// Interval between hops in the QUIC connections 180 | #[arg(long, verbatim_doc_comment, default_value = "0")] 181 | hop_interval_ms: u64, 182 | 183 | /// Set TCP_NODELAY 184 | #[arg(long, action)] 185 | tcp_nodelay: bool, 186 | 187 | /// Reload proxy rules if updated 188 | #[arg(short = 'w', long, action)] 189 | watch_proxy_rules_change: bool, 190 | 191 | /// Log level 192 | #[arg(short = 'l', long, default_value_t = String::from("I"), 193 | value_parser = PossibleValuesParser::new(["T", "D", "I", "W", "E"]).map(|v| match v.as_str() { 194 | "T" => "trace", 195 | "D" => "debug", 196 | "I" => "info", 197 | "W" => "warn", 198 | "E" => "error", 199 | _ => "info", 200 | }.to_string()))] 201 | loglevel: String, 202 | 203 | /// Print the args as base64 string to be used in opp:// address, will be ignored if passing in 204 | /// as an opp:// address, which can combine all args as a single base64 string 205 | #[arg(short = 'E', long, verbatim_doc_comment, action)] 206 | encode_base64: bool, 207 | 208 | /// Decode and print the base64 encoded opp:// address 209 | #[arg(short = 'D', long, action)] 210 | decode_base64: bool, 211 | } 212 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | omnip - [tcp | udp | http proxy | socks proxy] over quic 2 | -------- 3 | 4 | An all-in-one proxy written in Rust. 5 | 6 | Features 7 | -------- 8 | 9 | 1. Supports [HTTP tunneling](https://en.wikipedia.org/wiki/HTTP_tunnel) and basic HTTP proxy. 10 | 2. Supports `CONNECT` command of both [SOCKS5](https://www.rfc-editor.org/rfc/rfc1928) and [SOCKS4](https://www.openssh.com/txt/socks4.protocol) with [SOCKS4a](https://www.openssh.com/txt/socks4a.protocol) extension. In the case of being a node in a proxy chain, the implementation always delays DNS resolution to the next node, only when acting as the last node will it resolve DNS. 11 | 3. Proxy chaining with the `--upstream` option. e.g. `--upstream http://ip:port` or `--upstream socks5://ip:port` to forward payload to another HTTP proxy or SOCKS proxy. 12 | 4. Proxy over [QUIC](https://quicwg.org/), i.e. `http+quic`, `socks5+quic` and `socks4+quic`, for example: 13 | * Start a QUIC server backed by an HTTP proxy on a remote server (HTTP proxy over QUIC): 14 | * `omnip -a http+quic://0.0.0.0:3515 -lD` 15 | * Start a local SOCKS5 proxy and forward all its payload to the HTTP proxy server through QUIC tunnel (everything is encrypted): 16 | * `omnip -a socks5://127.0.0.1:9000 --upstream http+quic://DOMAIN:3515 -lD` 17 | Note: The commands above will use auto-generated self-signed certificate for QUIC, which is for demonstration only. Domain name with certificate issued by trusted CA is recommended. For more details, see README of the [rstun](https://github.com/neevek/rstun) project, which omnip uses to implement proxy over QUIC. And remember to set a password for the server with the `-p` or `--password` option. 18 | 5. Supports plain tcp connections over QUIC, which can be used to expose a port of remote server through the QUIC tunnel, for example: 19 | * Start a QUIC server that forwards all its tcp payload to the local SSH port: 20 | * `omnip -a tcp+quic://0.0.0.0:3515 --upstream tcp://127.0.0.1:22 -lD` 21 | * Connect to the tunnel server and SSH into the remote server through the QUIC tunnel: 22 | * `omnip -a tcp://0.0.0.0:3721 --upstream tcp+quic://DOMAIN:3515 -lD` 23 | * `ssh -p 3721 user@127.0.0.1` 24 | 6. Supports plain udp tunneling over QUIC, for example: 25 | * Start a QUIC server that forwards all its udp payload to `1.1.1.1:53`: 26 | * `omnip -a udp+quic://0.0.0.0:3515 --upstream udp://1.1.1.1:53 -lD` 27 | * Connect to the tunnel server and resolve DNS via the tunnel: 28 | * `omnip -a udp://0.0.0.0:5353 --upstream udp+quic://DOMAIN:3515 -lD` 29 | * `dig @127.0.0.1 -p 5353 github.com` 30 | 7. Supports simple proxy rules, traffic will be relayed to upstream if the requested domain matches one of the proxy rules, this is for achieving *Smart Proxy* to control which domains should be forwarded through the tunnel, for example: 31 | * example.com 32 | * .example.com 33 | * ||example.com 34 | * ... 35 | 8. Supports DoT (DNS-over-TLS) or custom name servers, for example: `--dot-server dns.google`, `--name-servers 1.1.1.1,8.8.8.8`, if both are specified, DoT server takes precedence. 36 | 9. Simple Web UI can be accessed from the same port of the proxy server, DNS servers and tunnel connection can be configured through the Web UI. 37 | 38 | Examples 39 | -------- 40 | 41 | 1. Running omnip in its simplest form: 42 | 43 | ``` 44 | omnip -a 8000 45 | ``` 46 | 47 | omnip is bound to `127.0.0.1:8000`, running as a proxy server that supports HTTP, SOCKS5 and SOCKS4. The complete format for the `-a|--addr` option is `scheme://[ip|domain]:port`, so the following command is allowed, but it will be restricted to supporting SOCKS5 only, and the proxy server will be listening on all available network interfaces on the machine: 48 | 49 | ``` 50 | omnip -a socks5://0.0.0.0:8000 51 | ``` 52 | 53 | 2. Chaining proxy servers: 54 | 55 | `omnip -a socks5://127.0.0.1:8000 -u http://192.168.50.50:9000` 56 | 57 | omnip runs as a SOCKS5 proxy server, which forwards all the proxy requests to the upstream server specified with the `-u|--upstream` option, in this case it simply translates SOCKS5 proxy requests to HTTP proxy requests. the schemes of the upstream can be one of `http`, `socks5`, `socks4` and their QUIC counterparts `http+quic`, `socks5+quic` and `socks4+quic`. 58 | 59 | 3. Running omnip as QUIC secured proxy server: 60 | 61 | `omnip -a socks5+quic://0.0.0.0:8000 -p passward123` 62 | 63 | omnip runs as a QUIC secured proxy server, which is supposed to be used as an *upstream* by a normal proxy server through chaining, see above. In this case a temporary self-signed certificate is generated for the server and the server name of the certificate is always set to `localhost`, note this is for demo only. 64 | 65 | 4. Running omnip as QUIC secured proxy server, with custom self-signed certificate and its associated private key in PEM format: 66 | 67 | `omnip -a socks5+quic://0.0.0.0:8000 -p passward123 -c CERT_FILE -k KEY_FILE` 68 | 69 | Note: Normal omnip proxy server setting this QUIC server as upstream must provide the same self-signed certificate file. 70 | 71 | 5. Running omnip as QUIC secured proxy server, with certificate issued by trusted CA: 72 | 73 | `omnip -a socks5+quic://DOMAIN:8000 -p passward123 -c CERT_FILE -k KEY_FILE` 74 | 75 | Note: The server is running with DOMAIN and certificate issued by trusted CA, normal omnip proxy server setting this QUIC server as upstream must use the same DOMAIN, but NO certificate is needed in this case. 76 | 77 | 6. Chaining omnip proxy servers with QUIC in between: 78 | 79 | `omnip -a 0.0.0.0:9000 -u socks5+quic://DOMAIN:8000 -p passward123` 80 | 81 | Traffic going from `0.0.0.0:9000` to `DOMAIN:8000` will be secured by the QUIC tunnel in this case. 82 | 83 | 7. Running omnip proxy server with proxy rules: 84 | 85 | `omnip -a 0.0.0.0:9000 -u socks5+quic://DOMAIN:8000 -p passward123 -r PROXY_RULES_FILE` 86 | 87 | PROXY_RULES_FILE is a simple text file in which each line contains a simple rule for a domain. 88 | 89 | 90 | ![omnip](https://github.com/neevek/omnip/raw/master/omnip1.jpg) 91 | ![omnip](https://github.com/neevek/omnip/raw/master/omnip2.jpg) 92 | 93 | ``` 94 | Usage: omnip [OPTIONS] --addr 95 | 96 | Options: 97 | -a, --addr 98 | Server address [://][ip:]port 99 | for example: http://127.0.0.1:8000, http+quic://127.0.0.1:8000 100 | -u, --upstream 101 | Upstream which the proxy server will relay traffic to based on proxy rules, 102 | [://]ip:port for example: http://127.0.0.1:8000, http+quic://127.0.0.1:8000 [default: ] 103 | -r, --proxy-rules-file 104 | Path to the proxy rules file [default: ] 105 | -t, --threads 106 | Threads to run async tasks, default to number of cpu cores [default: 0] 107 | --dot-server 108 | DoT (DNS-over-TLS) server, e.g. dns.google [default: ] 109 | --name-servers 110 | comma saprated domain servers (E.g. 1.1.1.1,8.8.8.8), which will be used 111 | if no dot_server is specified, or system default if empty [default: ] 112 | -c, --cert 113 | Applicable only for +quic protocols 114 | Path to the certificate file, if empty, a self-signed certificate 115 | with the domain "localhost" will be used [default: ] 116 | -k, --key 117 | Applicable only for +quic protocols 118 | Path to the key file, can be empty if no cert is provided [default: ] 119 | -p, --password 120 | Applicable only for +quic protocols 121 | Password of the +quic server [default: ] 122 | -e, --cipher 123 | Applicable only for +quic protocols 124 | Cipher for encryption [default: chacha20-poly1305] [possible values: chacha20-poly1305, aes-256-gcm, aes-128-gcm] 125 | -i, --quic-timeout-ms 126 | Applicable only for quic protocol as upstream 127 | Max idle timeout for the QUIC connections [default: 120000] 128 | --tcp-timeout-ms 129 | Read timeout in milliseconds for TCP connections [default: 30000] 130 | --udp-timeout-ms 131 | Read timeout in milliseconds for UDP connections [default: 5000] 132 | -R, --retry-interval-ms 133 | Applicable only for quic protocol as upstream 134 | Max idle timeout for the QUIC connections [default: 5000] 135 | --hop-interval-ms 136 | Applicable only for quic protocol as upstream 137 | Interval between hops in the QUIC connections [default: 180000] 138 | --tcp-nodelay 139 | Set TCP_NODELAY 140 | -w, --watch-proxy-rules-change 141 | Reload proxy rules if updated 142 | -l, --loglevel 143 | Log level [default: I] [possible values: T, D, I, W, E] 144 | -E, --encode-base64 145 | Print the args as base64 string to be used in opp:// address, will be ignored if passing in 146 | as an opp:// address, which can combine all args as a single base64 string 147 | -D, --decode-base64 148 | Decode and print the base64 encoded opp:// address 149 | -h, --help 150 | Print help 151 | -V, --version 152 | Print version 153 | ``` 154 | 155 | License 156 | ------- 157 | 158 | This Source Code Form is subject to the terms of the Mozilla Public 159 | License, v. 2.0. If a copy of the MPL was not distributed with this 160 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 161 | -------------------------------------------------------------------------------- /src/socks/socks_proxy_handler.rs: -------------------------------------------------------------------------------- 1 | use super::{socks_reply, socks_req::SocksReq, SocksVersion}; 2 | use crate::{ 3 | http::http_req::HttpReq, 4 | proxy_handler::{OutboundType, ParseState, ProxyHandler}, 5 | utils, NetAddr, ProxyError, 6 | }; 7 | use anyhow::Context; 8 | use anyhow::{anyhow, Result}; 9 | use async_trait::async_trait; 10 | use log::{debug, error}; 11 | use std::{ 12 | cmp::min, 13 | net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, 14 | }; 15 | use tokio::{io::AsyncWriteExt, net::TcpStream}; 16 | 17 | const SOCKS_MAX_RETRY_COUNT: u8 = 20; 18 | 19 | #[derive(PartialEq, Debug)] 20 | pub(crate) enum InternalParseState { 21 | SelectMethod, 22 | SelectedMethod, 23 | Connected, 24 | } 25 | 26 | pub struct SocksProxyHandler { 27 | socks_version: SocksVersion, 28 | bnd_addr: SocketAddr, 29 | state: InternalParseState, 30 | target_addr: Option, 31 | retry_count: u8, 32 | } 33 | 34 | impl SocksProxyHandler { 35 | pub fn new(socks_version: SocksVersion, bnd_addr: SocketAddr) -> Self { 36 | SocksProxyHandler { 37 | socks_version, 38 | bnd_addr, 39 | state: match socks_version { 40 | SocksVersion::V5 => InternalParseState::SelectMethod, 41 | // for SOCKS4, there's no method selection step, so initial step is SelectedMethod 42 | SocksVersion::V4 => InternalParseState::SelectedMethod, 43 | }, 44 | target_addr: None, 45 | retry_count: 0, 46 | } 47 | } 48 | 49 | fn fail_with_resp(&self) -> ParseState { 50 | ParseState::FailWithReply(( 51 | socks_reply(self.socks_version, self.bnd_addr, true), 52 | ProxyError::BadRequest, 53 | )) 54 | } 55 | 56 | async fn reply_connected( 57 | &self, 58 | inbound_stream: &mut TcpStream, 59 | connected: bool, 60 | ) -> Result<(), ProxyError> { 61 | utils::write_to_stream( 62 | inbound_stream, 63 | &socks_reply(self.socks_version, self.bnd_addr, !connected), 64 | ) 65 | .await 66 | } 67 | 68 | /// see https://www.openssh.com/txt/socks4a.protocol 69 | fn parse_socks4a_domain(data: &[u8]) -> Option { 70 | let data = &data[8..]; 71 | let userid_end = data.iter().position(|&b| b == 0u8)?; 72 | if userid_end + 1 < data.len() { 73 | let data = &data[(userid_end + 1)..]; 74 | let domain_end = data.iter().position(|&b| b == 0u8)?; 75 | Some(std::str::from_utf8(&data[..domain_end]).ok()?.to_string()) 76 | } else { 77 | error!("failed to parse domain name for socks4a connection"); 78 | None 79 | } 80 | } 81 | 82 | fn extract_target_addr(&self) -> Result<&NetAddr, ProxyError> { 83 | match self.target_addr { 84 | Some(ref target_addr) => Ok(target_addr), 85 | None => Err(ProxyError::BadGateway(anyhow!( 86 | "failed to parse SOCKS request, no target address" 87 | ))), 88 | } 89 | } 90 | } 91 | 92 | #[async_trait] 93 | impl ProxyHandler for SocksProxyHandler { 94 | fn parse(&mut self, data: &[u8]) -> ParseState { 95 | if self.state == InternalParseState::Connected { 96 | error!("invalid state, never call parse again!"); 97 | return self.fail_with_resp(); 98 | } 99 | 100 | if data.is_empty() { 101 | return self.fail_with_resp(); 102 | } 103 | 104 | let data_len = data.len(); 105 | if self.state == InternalParseState::SelectMethod { 106 | if data_len < 3 || (data[1] + 2) as usize != data_len { 107 | error!("unexpected socks request length: {}", data_len); 108 | return self.fail_with_resp(); 109 | } 110 | 111 | if !data[2..].iter().any(|method| *method == 0x00) { 112 | error!("only \"No Authentication\" is supported for SOCKS5"); 113 | return self.fail_with_resp(); 114 | } 115 | 116 | self.state = InternalParseState::SelectedMethod; 117 | return ParseState::ContinueWithReply("\x05\x00".into()); 118 | } 119 | 120 | if self.state == InternalParseState::SelectedMethod { 121 | if self.socks_version == SocksVersion::V4 { 122 | if data_len < 8 { 123 | return self.fail_with_resp(); 124 | } 125 | // supports CONNECT only 126 | if !data.starts_with("\x04\x01".as_bytes()) { 127 | debug!("error request: {:x?}", &data[..min(10, data_len)]); 128 | return self.fail_with_resp(); 129 | } 130 | 131 | if &data[4..8] == "\x00\x00\x00\x01".as_bytes() { 132 | self.target_addr = Some(match Self::parse_socks4a_domain(data) { 133 | Some(domain) => { 134 | NetAddr::from_domain(domain, ((data[2] as u16) << 8) | data[3] as u16) 135 | } 136 | None => return self.fail_with_resp(), 137 | }); 138 | } else { 139 | self.target_addr = Some(NetAddr::from_ip( 140 | IpAddr::V4(Ipv4Addr::new(data[4], data[5], data[6], data[7])), 141 | ((data[2] as u16) << 8) | data[3] as u16, 142 | )); 143 | } 144 | 145 | self.state = InternalParseState::Connected; 146 | return ParseState::ReceivedRequest(self.target_addr.as_ref().unwrap()); 147 | } 148 | 149 | // Socks5 150 | // at least 5 data is need to identify a valid Socks5 CONNECT fail_with_response 151 | if data_len <= 4 { 152 | return self.fail_with_resp(); 153 | } 154 | 155 | if !data.starts_with("\x05\x01\x00".as_bytes()) { 156 | debug!("error request: {:x?}", &data[..min(10, data_len)]); 157 | return self.fail_with_resp(); 158 | } 159 | 160 | let target_addr = match data[3] { 161 | 1u8 => { 162 | // <4-byte header> + <4-byte IP> + <2-byte port> 163 | if data_len == 4 + 4 + 2 { 164 | Some(NetAddr::from_ip( 165 | IpAddr::V4(Ipv4Addr::new(data[4], data[5], data[6], data[7])), 166 | ((data[8] as u16) << 8) | data[9] as u16, 167 | )) 168 | } else { 169 | None 170 | } 171 | } 172 | 3u8 => { 173 | // // domain name 174 | // // <4-byte header> + <1-byte domain length> + + <2-byte port> 175 | let domain_len = data[4] as usize; 176 | if domain_len > 0 && data_len == 4 + 1 + domain_len + 2 { 177 | let domain_start = 5; 178 | if let Ok(domain) = 179 | std::str::from_utf8(&data[domain_start..(domain_start + domain_len)]) 180 | { 181 | let port_index = domain_start + domain_len; 182 | Some(NetAddr::new( 183 | domain, 184 | ((data[port_index] as u16) << 8) | data[port_index + 1] as u16, 185 | )) 186 | } else { 187 | None 188 | } 189 | } else { 190 | None 191 | } 192 | } 193 | 4u8 => { 194 | // // ipv6 195 | // // <4-byte header> + <16-byte IP> + <2-byte port> 196 | if data_len == 4 + 16 + 2 { 197 | Some(NetAddr::from_ip( 198 | IpAddr::V6(Ipv6Addr::new( 199 | ((data[4] as u16) << 8) | data[5] as u16, 200 | ((data[6] as u16) << 8) | data[7] as u16, 201 | ((data[8] as u16) << 8) | data[9] as u16, 202 | ((data[10] as u16) << 8) | data[11] as u16, 203 | ((data[12] as u16) << 8) | data[13] as u16, 204 | ((data[14] as u16) << 8) | data[15] as u16, 205 | ((data[16] as u16) << 8) | data[17] as u16, 206 | ((data[18] as u16) << 8) | data[19] as u16, 207 | )), 208 | ((data[20] as u16) << 8) | data[21] as u16, 209 | )) 210 | } else { 211 | None 212 | } 213 | } 214 | _ => None, 215 | }; 216 | 217 | if target_addr.is_none() { 218 | error!("unexpected socks5 request"); 219 | return self.fail_with_resp(); 220 | } 221 | 222 | self.target_addr = target_addr; 223 | self.state = InternalParseState::Connected; 224 | return ParseState::ReceivedRequest(self.target_addr.as_ref().unwrap()); 225 | } 226 | 227 | self.retry_count += 1; 228 | 229 | if self.retry_count >= SOCKS_MAX_RETRY_COUNT { 230 | error!( 231 | "unexpected socks request failed with retry count: {}", 232 | self.retry_count 233 | ); 234 | self.fail_with_resp() 235 | } else { 236 | ParseState::Pending 237 | } 238 | } 239 | 240 | async fn handle( 241 | &self, 242 | outbound_type: OutboundType, 243 | outbound_stream: &mut TcpStream, 244 | inbound_stream: &mut TcpStream, 245 | ) -> Result<(), ProxyError> { 246 | match outbound_type { 247 | OutboundType::Direct => self.reply_connected(inbound_stream, true).await, 248 | 249 | OutboundType::HttpProxy => { 250 | HttpReq::handshake(outbound_stream, self.extract_target_addr()?).await?; 251 | self.reply_connected(inbound_stream, true).await 252 | } 253 | 254 | OutboundType::SocksProxy(ver) => { 255 | SocksReq::handshake(ver, outbound_stream, self.extract_target_addr()?).await?; 256 | self.reply_connected(inbound_stream, true).await 257 | } 258 | } 259 | } 260 | 261 | async fn handle_outbound_failure( 262 | &self, 263 | inbound_stream: &mut TcpStream, 264 | ) -> Result<(), ProxyError> { 265 | self.reply_connected(inbound_stream, false).await 266 | } 267 | 268 | async fn reject(&self, inbound_stream: &mut TcpStream) -> Result<(), ProxyError> { 269 | // we will still politely return OK to the client, and drop the connection 270 | self.reply_connected(inbound_stream, true).await?; 271 | inbound_stream 272 | .flush() 273 | .await 274 | .context("stream is disconnected while flussing") 275 | .map_err(ProxyError::Disconnected) 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /src/admin/admin_server.rs: -------------------------------------------------------------------------------- 1 | use super::{JsonRequest, JsonResponse}; 2 | use crate::Api; 3 | use anyhow::Result; 4 | use http::request::Parts; 5 | use http::response; 6 | use hyper::body; 7 | use hyper::{body::Body, body::Bytes, server::conn::Http, service::Service, Request, Response}; 8 | use lazy_static::lazy_static; 9 | use log::{error, info}; 10 | use monolithica::{Asset, AssetIndexer}; 11 | use serde::de::DeserializeOwned; 12 | use serde::Serialize; 13 | use std::collections::HashMap; 14 | use std::future::Future; 15 | use std::sync::Arc; 16 | use std::{net::SocketAddr, pin::Pin}; 17 | use tokio::net::TcpListener; 18 | 19 | static WEB_RESOURCE_INDEX: &str = include_str!(concat!(env!("OUT_DIR"), "/omnip-web.idx")); 20 | static WEB_RESOURCE_DATA: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/omnip-web.blob")); 21 | 22 | const HEADER_KEY_CONTENT_TYPE: &str = "Content-Type"; 23 | const CONTENT_TYPE_JSON: &str = "application/json"; 24 | 25 | static HTTP_CODE_OK: u16 = 200; 26 | 27 | static SERVICE_ERR_CODE_INVALID_PAYLOAD: u16 = 100; 28 | static SERVICE_ERR_CODE_INVALID_RESPONSE: u16 = 101; 29 | static SERVICE_ERR_CODE_METHOD_NOT_SUPPORTED: u16 = 102; 30 | 31 | type RequestHandler = fn(api: Arc, head: Parts, body: Bytes) -> Result; 32 | type RequestHandlersMap = HashMap<&'static str, RequestHandler>; 33 | 34 | lazy_static! { 35 | static ref ASSET_INDEXER: AssetIndexer<'static> = AssetIndexer::new(WEB_RESOURCE_INDEX); 36 | static ref REQUEST_HANDLERS_MAP: RequestHandlersMap = { 37 | let mut map = HashMap::<&'static str, RequestHandler>::new(); 38 | map.insert("/api/server_state", AdminServer::server_state); 39 | map.insert("/api/prefer_upstream", AdminServer::prefer_upstream); 40 | map.insert("/api/proxy_server_config", AdminServer::proxy_server_config); 41 | map.insert("/api/quic_tunnel_config", AdminServer::quic_tunnel_config); 42 | map.insert( 43 | "/api/update_quic_tunnel_config", 44 | AdminServer::update_quic_tunnel_config, 45 | ); 46 | map.insert( 47 | "/api/update_proxy_server_config", 48 | AdminServer::update_proxy_server_config, 49 | ); 50 | map.insert("/api/stats", AdminServer::server_stats); 51 | map 52 | }; 53 | } 54 | 55 | pub struct DashboardServer {} 56 | 57 | impl DashboardServer { 58 | pub fn new() -> Self { 59 | Self {} 60 | } 61 | 62 | pub async fn bind(&self, addr: SocketAddr) -> Result { 63 | let listener = TcpListener::bind(addr).await.inspect_err(|_| { 64 | error!("failed to bind dashboard server on address: {addr}"); 65 | })?; 66 | 67 | let addr = listener.local_addr().unwrap(); 68 | info!("dashboard server bound to: {addr}"); 69 | Ok(listener) 70 | } 71 | 72 | pub async fn serve_async(&self, listener: TcpListener, api: Arc) { 73 | tokio::spawn(async move { 74 | loop { 75 | match listener.accept().await { 76 | Ok((stream, _)) => { 77 | let api = api.clone(); 78 | tokio::task::spawn(async move { 79 | if let Err(err) = Http::new() 80 | .serve_connection(stream, AdminServer::new(api)) 81 | .await 82 | { 83 | println!("Failed to serve connection: {:?}", err); 84 | } 85 | }); 86 | } 87 | 88 | Err(e) => error!("access server failed, err: {e}"), 89 | } 90 | } 91 | }); 92 | } 93 | } 94 | 95 | struct AdminServer { 96 | api: Arc, 97 | } 98 | 99 | impl AdminServer { 100 | pub fn new(api: Arc) -> Self { 101 | AdminServer { api } 102 | } 103 | 104 | async fn handle_request(api: Arc, req: Request) -> Option> { 105 | let path = req.uri().path(); 106 | match REQUEST_HANDLERS_MAP.get(path) { 107 | Some(handler) => { 108 | let (parts, req_body) = req.into_parts(); 109 | let bytes = body::to_bytes(req_body).await.ok()?; 110 | let resp_body = match handler(api.clone(), parts, bytes) { 111 | Ok(resp) => resp, 112 | Err(e) => Self::convert_resp_to_body(e).unwrap(), 113 | }; 114 | 115 | Some( 116 | Response::builder() 117 | .status(HTTP_CODE_OK) 118 | .header(HEADER_KEY_CONTENT_TYPE, CONTENT_TYPE_JSON) 119 | .body(resp_body) 120 | .unwrap(), 121 | ) 122 | } 123 | 124 | None => None, 125 | } 126 | } 127 | 128 | fn not_supported() -> JsonResponse { 129 | JsonResponse::fail( 130 | SERVICE_ERR_CODE_METHOD_NOT_SUPPORTED, 131 | "Method not supported".to_string(), 132 | ) 133 | } 134 | 135 | fn convert_req_to_json( 136 | bytes: Bytes, 137 | ) -> Result, JsonResponse> { 138 | serde_json::from_slice::>(&bytes) 139 | .map_err(|e| JsonResponse::fail(SERVICE_ERR_CODE_INVALID_PAYLOAD, e.to_string())) 140 | } 141 | 142 | fn convert_resp_to_body(resp: JsonResponse) -> Result { 143 | let bytes = serde_json::to_vec(&resp) 144 | .map_err(|e| JsonResponse::fail(SERVICE_ERR_CODE_INVALID_RESPONSE, e.to_string()))?; 145 | Ok(Body::from(bytes)) 146 | } 147 | 148 | fn server_state(api: Arc, head: Parts, _: Bytes) -> Result { 149 | let body = match head.method { 150 | http::Method::GET => Self::convert_resp_to_body( 151 | JsonResponse::::succeed(Some(api.get_server_state())), 152 | )?, 153 | _ => return Err(Self::not_supported()), 154 | }; 155 | 156 | Ok(body) 157 | } 158 | 159 | fn proxy_server_config(api: Arc, head: Parts, _: Bytes) -> Result { 160 | let body = match head.method { 161 | http::Method::GET => { 162 | Self::convert_resp_to_body(JsonResponse::::succeed( 163 | Some(api.get_proxy_server_config()), 164 | ))? 165 | } 166 | _ => return Err(Self::not_supported()), 167 | }; 168 | 169 | Ok(body) 170 | } 171 | 172 | fn quic_tunnel_config(api: Arc, head: Parts, _: Bytes) -> Result { 173 | let body = match head.method { 174 | http::Method::GET => { 175 | Self::convert_resp_to_body(JsonResponse::::succeed( 176 | Some(api.get_quic_tunnel_config()), 177 | ))? 178 | } 179 | _ => return Err(Self::not_supported()), 180 | }; 181 | 182 | Ok(body) 183 | } 184 | 185 | fn server_stats(api: Arc, head: Parts, _: Bytes) -> Result { 186 | let body = match head.method { 187 | http::Method::GET => Self::convert_resp_to_body( 188 | JsonResponse::::succeed(Some(api.get_server_stats())), 189 | )?, 190 | _ => return Err(Self::not_supported()), 191 | }; 192 | 193 | Ok(body) 194 | } 195 | 196 | fn prefer_upstream(api: Arc, head: Parts, body: Bytes) -> Result { 197 | let body = match head.method { 198 | http::Method::POST => { 199 | let req = Self::convert_req_to_json::(body)?; 200 | api.set_prefer_upstream(req.data.unwrap()); 201 | Self::convert_resp_to_body(::succeed(None))? 202 | } 203 | _ => return Err(Self::not_supported()), 204 | }; 205 | 206 | Ok(body) 207 | } 208 | 209 | fn update_proxy_server_config( 210 | api: Arc, 211 | head: Parts, 212 | body: Bytes, 213 | ) -> Result { 214 | let body = match head.method { 215 | http::Method::POST => { 216 | let req = Self::convert_req_to_json::(body)?; 217 | tokio::spawn(async move { 218 | api.update_proxy_server_config(req.data.unwrap()).await.ok(); 219 | }); 220 | Self::convert_resp_to_body(::succeed(None))? 221 | } 222 | _ => return Err(Self::not_supported()), 223 | }; 224 | 225 | Ok(body) 226 | } 227 | 228 | fn update_quic_tunnel_config( 229 | api: Arc, 230 | head: Parts, 231 | body: Bytes, 232 | ) -> Result { 233 | let body = match head.method { 234 | http::Method::POST => { 235 | let req = Self::convert_req_to_json::(body)?; 236 | tokio::spawn(async move { 237 | api.update_quic_tunnel_config(req.data.unwrap()).await.ok(); 238 | }); 239 | Self::convert_resp_to_body(::succeed(None))? 240 | } 241 | _ => return Err(Self::not_supported()), 242 | }; 243 | 244 | Ok(body) 245 | } 246 | } 247 | 248 | impl Service> for AdminServer { 249 | type Response = Response; 250 | type Error = hyper::Error; 251 | type Future = Pin> + Send>>; 252 | 253 | fn call(&mut self, req: Request) -> Self::Future { 254 | fn static_html_resp( 255 | builder: response::Builder, 256 | Asset { offset, len, .. }: &Asset, 257 | ) -> Response { 258 | let offset = *offset as usize; 259 | let len = *len as usize; 260 | let content = &WEB_RESOURCE_DATA[offset..(offset + len)]; 261 | builder.body(Body::from(content)).unwrap() 262 | } 263 | 264 | let api = self.api.clone(); 265 | let res = async { 266 | let path = req.uri().path(); 267 | let path = if ["/", "/web", "/web/"].iter().any(|&p| p == path) { 268 | "index.html" 269 | } else { 270 | &path[5..] // skip /web/ 271 | }; 272 | 273 | let asset = ASSET_INDEXER.locate_asset(path); 274 | Ok(match asset { 275 | Some(asset) => { 276 | let mime = if !asset.mime.is_empty() { 277 | &asset.mime 278 | } else { 279 | "application/octet-stream" 280 | }; 281 | 282 | let builder = Response::builder() 283 | .status(200) 284 | .header(HEADER_KEY_CONTENT_TYPE, mime); 285 | static_html_resp(builder, asset) 286 | } 287 | None => match Self::handle_request(api, req).await { 288 | Some(res) => res, 289 | None => { 290 | let builder = Response::builder() 291 | .status(404) 292 | .header(HEADER_KEY_CONTENT_TYPE, "text/html"); 293 | let asset = ASSET_INDEXER.locate_asset("404.html").unwrap(); 294 | static_html_resp(builder, asset) 295 | } 296 | }, 297 | }) 298 | }; 299 | 300 | Box::pin(res) 301 | } 302 | 303 | fn poll_ready( 304 | &mut self, 305 | _: &mut std::task::Context<'_>, 306 | ) -> std::task::Poll> { 307 | std::task::Poll::Ready(Ok(())) 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v[0-9]+.[0-9]+.[0-9]+' 7 | 8 | jobs: 9 | linux-gnu-x86_64: 10 | name: Linux gnu x86_64 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: actions-rs/toolchain@v1 15 | with: 16 | toolchain: stable 17 | target: x86_64-unknown-linux-gnu 18 | override: true 19 | - run: rustup update && cargo build --all-features --release && mv target/release/omnip target/release/omnip-linux-gnu-x86_64 20 | - name: Release 21 | uses: softprops/action-gh-release@v1 22 | if: startsWith(github.ref, 'refs/tags/') 23 | with: 24 | files: | 25 | target/release/omnip-linux-gnu-x86_64 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | 29 | linux-musl-x86_64: 30 | name: Linux musl x86_64 31 | runs-on: ubuntu-latest 32 | steps: 33 | - uses: actions/checkout@v2 34 | - uses: actions-rs/toolchain@v1 35 | with: 36 | toolchain: stable 37 | target: x86_64-unknown-linux-musl 38 | override: true 39 | - run: sudo apt-get -y install musl-tools && rustup target add x86_64-unknown-linux-musl && rustup update 40 | - run: cargo install cross --git https://github.com/cross-rs/cross 41 | - run: cross build --all-features --release --target x86_64-unknown-linux-musl 42 | - run: mv target/x86_64-unknown-linux-musl/release/omnip target/x86_64-unknown-linux-musl/release/omnip-linux-musl-x86_64 43 | - name: Release 44 | uses: softprops/action-gh-release@v1 45 | if: startsWith(github.ref, 'refs/tags/') 46 | with: 47 | files: | 48 | target/x86_64-unknown-linux-musl/release/omnip-linux-musl-x86_64 49 | env: 50 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 51 | 52 | windows-x86_64: 53 | name: Windows msvc x86_64 54 | runs-on: windows-latest 55 | defaults: 56 | run: 57 | shell: bash 58 | steps: 59 | - uses: actions/checkout@v2 60 | - uses: actions-rs/toolchain@v1 61 | with: 62 | toolchain: stable 63 | target: x86_64-pc-windows-msvc 64 | override: true 65 | - run: cargo build --all-features --release && mv target/release/omnip.exe target/release/omnip-windows-x86_64.exe 66 | - name: Release 67 | uses: softprops/action-gh-release@v1 68 | if: startsWith(github.ref, 'refs/tags/') 69 | with: 70 | files: | 71 | target/release/omnip-windows-x86_64.exe 72 | env: 73 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 74 | 75 | windows-arm64: 76 | name: Windows msvc ARM64 77 | runs-on: windows-latest 78 | defaults: 79 | run: 80 | shell: bash 81 | steps: 82 | - uses: actions/checkout@v2 83 | - uses: actions-rs/toolchain@v1 84 | with: 85 | toolchain: stable 86 | target: aarch64-pc-windows-msvc 87 | override: true 88 | - run: cargo build --target aarch64-pc-windows-msvc --all-features --release && mv target/aarch64-pc-windows-msvc/release/omnip.exe target/release/omnip-windows-arm64.exe 89 | - name: Release 90 | uses: softprops/action-gh-release@v1 91 | if: startsWith(github.ref, 'refs/tags/') 92 | with: 93 | files: | 94 | target/release/omnip-windows-arm64.exe 95 | env: 96 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 97 | 98 | darwin-x86_64: 99 | name: Darwin x86_64 100 | runs-on: macos-latest 101 | steps: 102 | - uses: actions/checkout@v2 103 | - uses: actions-rs/toolchain@v1 104 | with: 105 | toolchain: stable 106 | target: x86_64-apple-darwin 107 | override: true 108 | - run: rustup target add x86_64-apple-darwin && cargo build --all-features --release --target x86_64-apple-darwin && mv target/x86_64-apple-darwin/release/omnip target/x86_64-apple-darwin/release/omnip-darwin-x86_64 109 | - name: Release 110 | uses: softprops/action-gh-release@v1 111 | if: startsWith(github.ref, 'refs/tags/') 112 | with: 113 | files: | 114 | target/x86_64-apple-darwin/release/omnip-darwin-x86_64 115 | env: 116 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 117 | 118 | darwin-aarch64: 119 | name: Darwin Aarch64 120 | runs-on: macos-latest 121 | steps: 122 | - uses: actions/checkout@v2 123 | - uses: actions-rs/toolchain@v1 124 | with: 125 | toolchain: stable 126 | target: aarch64-apple-darwin 127 | override: true 128 | - run: rustup target add aarch64-apple-darwin && cargo build --all-features --release --target aarch64-apple-darwin && mv target/aarch64-apple-darwin/release/omnip target/aarch64-apple-darwin/release/omnip-darwin-aarch64 129 | - name: Release 130 | uses: softprops/action-gh-release@v1 131 | if: startsWith(github.ref, 'refs/tags/') 132 | with: 133 | files: | 134 | target/aarch64-apple-darwin/release/omnip-darwin-aarch64 135 | env: 136 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 137 | 138 | linux-gnueabihf-armv7: 139 | name: Linux gnueabihf ARMv7 140 | runs-on: ubuntu-latest 141 | steps: 142 | - uses: actions/checkout@v2 143 | - uses: actions-rs/toolchain@v1 144 | with: 145 | toolchain: stable 146 | target: armv7-unknown-linux-gnueabihf 147 | override: true 148 | - run: rustup target add armv7-unknown-linux-gnueabihf && rustup update && cargo install cross --git https://github.com/cross-rs/cross && cross build --all-features --release --target armv7-unknown-linux-gnueabihf && mv target/armv7-unknown-linux-gnueabihf/release/omnip target/armv7-unknown-linux-gnueabihf/release/omnip-linux-gnueabihf-armv7 149 | - name: Release 150 | uses: softprops/action-gh-release@v1 151 | if: startsWith(github.ref, 'refs/tags/') 152 | with: 153 | files: | 154 | target/armv7-unknown-linux-gnueabihf/release/omnip-linux-gnueabihf-armv7 155 | env: 156 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 157 | 158 | linux-musleabihf-armv7: 159 | name: Linux musleabihf ARMv7 160 | runs-on: ubuntu-latest 161 | steps: 162 | - uses: actions/checkout@v2 163 | - uses: actions-rs/toolchain@v1 164 | with: 165 | toolchain: stable 166 | target: armv7-unknown-linux-musleabihf 167 | override: true 168 | - run: rustup target add armv7-unknown-linux-musleabihf && rustup update && cargo install cross --git https://github.com/cross-rs/cross && cross build --all-features --release --target armv7-unknown-linux-musleabihf && mv target/armv7-unknown-linux-musleabihf/release/omnip target/armv7-unknown-linux-musleabihf/release/omnip-linux-musleabihf-armv7 169 | - name: Release 170 | uses: softprops/action-gh-release@v1 171 | if: startsWith(github.ref, 'refs/tags/') 172 | with: 173 | files: | 174 | target/armv7-unknown-linux-musleabihf/release/omnip-linux-musleabihf-armv7 175 | env: 176 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 177 | 178 | linux-musleabi-armv7: 179 | name: Linux musleabi ARMv7 180 | runs-on: ubuntu-latest 181 | steps: 182 | - uses: actions/checkout@v2 183 | - uses: actions-rs/toolchain@v1 184 | with: 185 | toolchain: stable 186 | target: armv7-unknown-linux-musleabi 187 | override: true 188 | - run: rustup target add armv7-unknown-linux-musleabi && rustup update && cargo install cross --git https://github.com/cross-rs/cross && cross build --all-features --release --target armv7-unknown-linux-musleabi && mv target/armv7-unknown-linux-musleabi/release/omnip target/armv7-unknown-linux-musleabi/release/omnip-linux-musleabi-armv7 189 | - name: Release 190 | uses: softprops/action-gh-release@v1 191 | if: startsWith(github.ref, 'refs/tags/') 192 | with: 193 | files: | 194 | target/armv7-unknown-linux-musleabi/release/omnip-linux-musleabi-armv7 195 | env: 196 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 197 | 198 | linux-gnueabi-armv7: 199 | name: Linux gnueabi ARMv7 200 | runs-on: ubuntu-latest 201 | steps: 202 | - uses: actions/checkout@v2 203 | - uses: actions-rs/toolchain@v1 204 | with: 205 | toolchain: stable 206 | target: armv7-unknown-linux-gnueabi 207 | override: true 208 | - run: rustup target add armv7-unknown-linux-gnueabi && rustup update && cargo install cross --git https://github.com/cross-rs/cross && cross build --all-features --release --target armv7-unknown-linux-gnueabi && mv target/armv7-unknown-linux-gnueabi/release/omnip target/armv7-unknown-linux-gnueabi/release/omnip-linux-gnueabi-armv7 209 | - name: Release 210 | uses: softprops/action-gh-release@v1 211 | if: startsWith(github.ref, 'refs/tags/') 212 | with: 213 | files: | 214 | target/armv7-unknown-linux-gnueabi/release/omnip-linux-gnueabi-armv7 215 | env: 216 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 217 | 218 | linux-musleabi-armv5: 219 | name: Linux musleabi ARMv5 220 | runs-on: ubuntu-latest 221 | steps: 222 | - uses: actions/checkout@v2 223 | - uses: actions-rs/toolchain@v1 224 | with: 225 | toolchain: stable 226 | target: arm-unknown-linux-musleabi 227 | override: true 228 | - run: rustup target add arm-unknown-linux-musleabi && rustup update && cargo install cross --git https://github.com/cross-rs/cross && cross build --all-features --release --target arm-unknown-linux-musleabi && mv target/arm-unknown-linux-musleabi/release/omnip target/arm-unknown-linux-musleabi/release/omnip-linux-musleabi-armv5 229 | - name: Release 230 | uses: softprops/action-gh-release@v1 231 | if: startsWith(github.ref, 'refs/tags/') 232 | with: 233 | files: | 234 | target/arm-unknown-linux-musleabi/release/omnip-linux-musleabi-armv5 235 | env: 236 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 237 | 238 | linux-gnu-aarch64: 239 | name: Linux gnu Aarch64 240 | runs-on: ubuntu-latest 241 | steps: 242 | - uses: actions/checkout@v2 243 | - uses: actions-rs/toolchain@v1 244 | with: 245 | toolchain: stable 246 | target: aarch64-unknown-linux-gnu 247 | override: true 248 | - run: rustup target add aarch64-unknown-linux-gnu && rustup update && cargo install cross --git https://github.com/cross-rs/cross && cross build --all-features --release --target aarch64-unknown-linux-gnu && mv target/aarch64-unknown-linux-gnu/release/omnip target/aarch64-unknown-linux-gnu/release/omnip-linux-gnu-aarch64 249 | - name: Release 250 | uses: softprops/action-gh-release@v1 251 | if: startsWith(github.ref, 'refs/tags/') 252 | with: 253 | files: | 254 | target/aarch64-unknown-linux-gnu/release/omnip-linux-gnu-aarch64 255 | env: 256 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 257 | 258 | linux-musl-aarch64: 259 | name: Linux musl Aarch64 260 | runs-on: ubuntu-latest 261 | steps: 262 | - uses: actions/checkout@v2 263 | - uses: actions-rs/toolchain@v1 264 | with: 265 | toolchain: stable 266 | target: aarch64-unknown-linux-musl 267 | override: true 268 | - run: sudo apt-get -y install musl-tools && rustup target add aarch64-unknown-linux-musl && rustup update 269 | - run: cargo install cross --git https://github.com/cross-rs/cross 270 | - run: cross build --all-features --release --target aarch64-unknown-linux-musl 271 | - run: mv target/aarch64-unknown-linux-musl/release/omnip target/aarch64-unknown-linux-musl/release/omnip-linux-musl-aarch64 272 | - name: Release 273 | uses: softprops/action-gh-release@v1 274 | if: startsWith(github.ref, 'refs/tags/') 275 | with: 276 | files: | 277 | target/aarch64-unknown-linux-musl/release/omnip-linux-musl-aarch64 278 | env: 279 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 280 | -------------------------------------------------------------------------------- /src/proxy_rule_manager.rs: -------------------------------------------------------------------------------- 1 | /// some simple rules are supported: 2 | /// example.com 3 | /// .example.com 4 | /// ||example.com 5 | /// @@||example.com 6 | /// @@|example.com 7 | /// %%||example.com 8 | use std::fs::File; 9 | use std::io::{BufRead, BufReader}; 10 | use std::sync::{Arc, RwLock}; 11 | 12 | use log::{debug, info, warn}; 13 | use regex::Regex; 14 | use std::net::IpAddr; 15 | use std::str::FromStr; 16 | 17 | type FnMatch = Box bool>; 18 | const SORT_MATCH_RULES_COUNT_THRESHOLD: usize = 10; 19 | const SORT_MATCH_RULES_INDEX_THRESHOLD: usize = 10; 20 | 21 | pub struct ProxyRule { 22 | str_rule: String, 23 | fn_matches: FnMatch, 24 | match_count: RwLock, 25 | } 26 | 27 | pub enum MatchResult { 28 | Direct, 29 | Proxy, 30 | Reject, 31 | } 32 | 33 | impl ProxyRule { 34 | pub fn new(str_rule: &str, fn_matches: FnMatch) -> Self { 35 | ProxyRule { 36 | str_rule: str_rule.to_string(), 37 | fn_matches, 38 | match_count: RwLock::new(0), 39 | } 40 | } 41 | 42 | pub fn is_same_rule(&self, str_rule: &str) -> bool { 43 | self.str_rule == str_rule 44 | } 45 | 46 | pub fn matches(&self, host: &str, port: u16) -> bool { 47 | if self.fn_matches.as_ref()(host, port) { 48 | *self.match_count.write().unwrap() += 1; 49 | return true; 50 | } 51 | false 52 | } 53 | } 54 | 55 | #[derive(Clone)] 56 | pub struct ProxyRuleManager { 57 | inner: Arc>, 58 | } 59 | unsafe impl Send for ProxyRuleManager {} 60 | unsafe impl Sync for ProxyRuleManager {} 61 | 62 | pub struct ProxyRuleManagerInner { 63 | match_rules: Vec, 64 | exception_rules: Vec, 65 | reject_rules: Vec, 66 | } 67 | 68 | impl ProxyRuleManager { 69 | pub fn new() -> Self { 70 | Self { 71 | inner: Arc::new(RwLock::new(ProxyRuleManagerInner { 72 | match_rules: Vec::new(), 73 | exception_rules: Vec::new(), 74 | reject_rules: Vec::new(), 75 | })), 76 | } 77 | } 78 | 79 | pub fn add_rules_by_file(&mut self, file_path: &str) -> usize { 80 | let mut count = 0; 81 | if let Ok(file) = File::open(file_path) { 82 | BufReader::new(file).lines().for_each(|line| { 83 | if let Ok(line) = line { 84 | if self.add_rule(line.as_str()) { 85 | count += 1; 86 | } 87 | } 88 | }); 89 | } 90 | 91 | info!("added {} rules", count); 92 | count 93 | } 94 | 95 | pub fn add_rule(&mut self, rule: &str) -> bool { 96 | let mut prm = self.inner.write().unwrap(); 97 | if Self::has_rule(&prm.match_rules, rule) { 98 | warn!("duplicated rule: {}", rule); 99 | return true; 100 | } 101 | if let Some(fn_matches) = Self::parse_proxy_rule(rule) { 102 | prm.match_rules.push(ProxyRule::new(rule, fn_matches)); 103 | return true; 104 | } 105 | if Self::has_rule(&prm.exception_rules, rule) { 106 | warn!("duplicated exception rule: {}", rule); 107 | return true; 108 | } 109 | if let Some(fn_matches) = Self::parse_exception_rule(rule) { 110 | prm.exception_rules.push(ProxyRule::new(rule, fn_matches)); 111 | return true; 112 | } 113 | if Self::has_rule(&prm.reject_rules, rule) { 114 | warn!("duplicated reject rule: {}", rule); 115 | return true; 116 | } 117 | if let Some(fn_matches) = Self::parse_reject_rule(rule) { 118 | prm.reject_rules.push(ProxyRule::new(rule, fn_matches)); 119 | return true; 120 | } 121 | false 122 | } 123 | 124 | pub fn clear_all(&mut self) { 125 | let mut prm = self.inner.write().unwrap(); 126 | prm.match_rules.clear(); 127 | prm.exception_rules.clear(); 128 | } 129 | 130 | pub fn matches(&mut self, host: &str, port: u16) -> MatchResult { 131 | let prm = self.inner.read().unwrap(); 132 | let should_rejct = self.do_match(&prm.reject_rules, host, port); 133 | if should_rejct { 134 | debug!("rejected! {host}:{port}"); 135 | return MatchResult::Reject; 136 | } 137 | 138 | let should_proxy = self.do_match(&prm.match_rules, host, port); 139 | if !should_proxy { 140 | return MatchResult::Direct; 141 | } 142 | 143 | let matched_except_rule = self.do_match(&prm.exception_rules, host, port); 144 | if matched_except_rule { 145 | MatchResult::Direct 146 | } else { 147 | debug!("matched! {host}:{port}"); 148 | MatchResult::Proxy 149 | } 150 | } 151 | 152 | pub fn parse_proxy_rule(rule: &str) -> Option { 153 | if rule.is_empty() { 154 | return None; 155 | } 156 | 157 | // IPv6 may start with "::", but we will simply ignore it here 158 | if rule.chars().nth(0).unwrap().is_numeric() { 159 | // Handle CIDR notation first 160 | if let Some((ip_str, prefix_len)) = rule.split_once('/') { 161 | if let (Ok(ip), Ok(prefix_len)) = 162 | (IpAddr::from_str(ip_str), prefix_len.parse::()) 163 | { 164 | let cidr = IpCidr::new(ip, prefix_len); 165 | return Some(Box::new(move |host, _port| { 166 | if host.is_empty() || !host.chars().nth(0).unwrap().is_numeric() { 167 | return false; 168 | } 169 | if let Ok(host_ip) = IpAddr::from_str(host) { 170 | return cidr.contains(&host_ip); 171 | } 172 | false 173 | })); 174 | } 175 | } 176 | 177 | // Handle direct IP addresses 178 | if let Ok(ip) = IpAddr::from_str(rule) { 179 | return Some(Box::new(move |host, _port| { 180 | if host.is_empty() || !host.chars().nth(0).unwrap().is_numeric() { 181 | return false; 182 | } 183 | if let Ok(host_ip) = IpAddr::from_str(host) { 184 | return host_ip == ip; 185 | } 186 | false 187 | })); 188 | } 189 | } 190 | 191 | let rule_len = rule.len(); 192 | let bytes = rule.as_bytes(); 193 | 194 | if rule_len > 2 && bytes[0] == b'/' && bytes[rule.len() - 1] == b'/' { 195 | let re = Regex::new(&rule[1..rule_len - 1]).ok()?; 196 | return Some(Box::new(move |host, _port| re.is_match(host))); 197 | } 198 | 199 | let ch = bytes[0].to_ascii_lowercase() as char; 200 | if ch != '|' && ch != '.' && !ch.is_ascii_digit() && !ch.is_ascii_lowercase() { 201 | return None; 202 | } 203 | 204 | let mut rule = rule; 205 | let mut requires_443_port = false; 206 | let mut fuzzy_match = false; 207 | 208 | // matches against domain 209 | if rule.starts_with("||") { 210 | rule = &rule[2..]; 211 | fuzzy_match = true; 212 | } else if rule.starts_with('.') { 213 | rule = &rule[1..]; 214 | fuzzy_match = true; 215 | } else if rule.starts_with("|https://") { 216 | rule = &rule[9..]; 217 | requires_443_port = true; 218 | } else if rule.starts_with("|http://") { 219 | rule = &rule[8..]; 220 | } 221 | 222 | Some(Self::build_rule(rule, requires_443_port, fuzzy_match)) 223 | } 224 | 225 | pub fn parse_exception_rule(rule: &str) -> Option { 226 | if rule.is_empty() || rule.as_bytes()[0] != b'@' { 227 | return None; 228 | } 229 | 230 | let mut rule = rule; 231 | let mut requires_443_port = false; 232 | let mut fuzzy_match = false; 233 | 234 | // matches against domain 235 | if rule.starts_with("@@|https://") { 236 | rule = &rule[11..]; 237 | requires_443_port = true; 238 | } else if rule.starts_with("@@|http://") { 239 | rule = &rule[10..]; 240 | } else if rule.starts_with("@@||") { 241 | rule = &rule[4..]; 242 | fuzzy_match = true; 243 | } 244 | 245 | Some(Self::build_rule(rule, requires_443_port, fuzzy_match)) 246 | } 247 | 248 | pub fn parse_reject_rule(rule: &str) -> Option { 249 | if rule.is_empty() || rule.as_bytes()[0] != b'%' { 250 | return None; 251 | } 252 | 253 | let mut rule = rule; 254 | let mut requires_443_port = false; 255 | let mut fuzzy_match = false; 256 | 257 | // matches against domain 258 | if rule.starts_with("%%|https://") { 259 | rule = &rule[11..]; 260 | requires_443_port = true; 261 | } else if rule.starts_with("%%|http://") { 262 | rule = &rule[10..]; 263 | } else if rule.starts_with("%%||") { 264 | rule = &rule[4..]; 265 | fuzzy_match = true; 266 | } 267 | 268 | Some(Self::build_rule(rule, requires_443_port, fuzzy_match)) 269 | } 270 | 271 | fn build_rule(rule: &str, requires_443_port: bool, fuzzy_match: bool) -> FnMatch { 272 | let rule_copy = rule.to_string(); 273 | Box::new(move |host, port| { 274 | if requires_443_port && port != 443 { 275 | return false; 276 | } 277 | if let Some(pos) = host.find(rule_copy.as_str()) { 278 | return pos == 0 || (fuzzy_match && host.as_bytes()[pos - 1] == b'.'); 279 | } 280 | false 281 | }) 282 | } 283 | 284 | fn has_rule(rules: &[ProxyRule], rule: &str) -> bool { 285 | rules.iter().any(|r| r.is_same_rule(rule)) 286 | } 287 | 288 | fn do_match(&self, rules: &[ProxyRule], host: &str, port: u16) -> bool { 289 | rules 290 | .iter() 291 | .enumerate() 292 | .find(|(_, rule)| rule.matches(host, port)) 293 | .is_some_and(|(index, rule)| { 294 | // sort the rules if it runs with tokio runtime (of course it does) 295 | if tokio::runtime::Handle::try_current().is_ok() 296 | && index >= SORT_MATCH_RULES_INDEX_THRESHOLD 297 | && *rule.match_count.read().unwrap() >= SORT_MATCH_RULES_COUNT_THRESHOLD 298 | { 299 | let prm = self.clone(); 300 | tokio::spawn(async move { 301 | prm.sort_rules(); 302 | }); 303 | info!("sort the rule for: {host}:{port}, current index:{index}"); 304 | } 305 | true 306 | }) 307 | } 308 | 309 | fn sort_rules(&self) { 310 | let mut prm = self.inner.write().unwrap(); 311 | prm.match_rules.sort_by(|a, b| { 312 | (*b.match_count.read().unwrap()).cmp(&(*a.match_count.read().unwrap())) 313 | }); 314 | prm.exception_rules.sort_by(|a, b| { 315 | (*b.match_count.read().unwrap()).cmp(&(*a.match_count.read().unwrap())) 316 | }); 317 | } 318 | } 319 | 320 | impl Default for ProxyRuleManager { 321 | fn default() -> Self { 322 | Self::new() 323 | } 324 | } 325 | 326 | struct IpCidr { 327 | ip: IpAddr, 328 | prefix_len: u8, 329 | } 330 | 331 | impl IpCidr { 332 | fn new(ip: IpAddr, prefix_len: u8) -> Self { 333 | Self { ip, prefix_len } 334 | } 335 | 336 | fn contains(&self, ip: &IpAddr) -> bool { 337 | match (self.ip, ip) { 338 | (IpAddr::V4(network), IpAddr::V4(ip)) => { 339 | let mask = !((1u32 << (32 - self.prefix_len)) - 1); 340 | (u32::from(network) & mask) == (u32::from(*ip) & mask) 341 | } 342 | (IpAddr::V6(network), IpAddr::V6(ip)) => { 343 | let mask = !((1u128 << (128 - self.prefix_len)) - 1); 344 | (u128::from(network) & mask) == (u128::from(*ip) & mask) 345 | } 346 | _ => false, 347 | } 348 | } 349 | } 350 | -------------------------------------------------------------------------------- /src/http/http_proxy_handler.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | proxy_handler::{OutboundType, ParseState, ProxyHandler}, 3 | socks::socks_req::SocksReq, 4 | utils, NetAddr, PooledBuffer, ProxyError, BUFFER_POOL, 5 | }; 6 | use anyhow::Context; 7 | use async_trait::async_trait; 8 | use log::{debug, error}; 9 | use rs_utilities::unwrap_or_return; 10 | use tokio::{io::AsyncWriteExt, net::TcpStream}; 11 | use url::Url; 12 | 13 | use super::{INITIAL_HTTP_HEADER_SIZE, MAX_HTTP_HEADER_SIZE}; 14 | 15 | const HTTP_RESP_200: &[u8] = b"HTTP/1.1 200 OK\r\n\r\n"; 16 | const HTTP_RESP_400: &[u8] = b"HTTP/1.1 400 Bad Request\r\n\r\n"; 17 | const HTTP_RESP_413: &[u8] = b"HTTP/1.1 413 Payload Too Large\r\n\r\n"; 18 | const HTTP_RESP_502: &[u8] = b"HTTP/1.1 502 Bad Gateway\r\n\r\n"; 19 | 20 | pub struct HttpProxyHandler<'a> { 21 | http_request: Option>, 22 | buffer: Option>, 23 | parse_done: bool, 24 | } 25 | 26 | impl HttpProxyHandler<'_> { 27 | pub fn new() -> Self { 28 | HttpProxyHandler { 29 | http_request: None, 30 | buffer: None, 31 | parse_done: false, 32 | } 33 | } 34 | 35 | async fn exchange_http_payloads( 36 | http_request: &HttpRequest<'_>, 37 | outbound_stream: &mut TcpStream, 38 | inbound_stream: &mut TcpStream, 39 | ) -> Result<(), ProxyError> { 40 | if http_request.is_connect_request() { 41 | utils::write_to_stream(inbound_stream, HTTP_RESP_200).await?; 42 | } else { 43 | let header = std::str::from_utf8(http_request.header()) 44 | .context("failed to convert header as UTF-8 string") 45 | .map_err(|_| ProxyError::BadRequest)? 46 | .replace("Proxy-Connection", "Connection") 47 | .replace("proxy-connection", "Connection"); 48 | utils::write_to_stream(outbound_stream, header.as_bytes()).await?; 49 | } 50 | 51 | let body = http_request.body(); 52 | if !body.is_empty() { 53 | utils::write_to_stream(outbound_stream, body).await?; 54 | } 55 | 56 | Ok(()) 57 | } 58 | } 59 | 60 | #[async_trait] 61 | impl ProxyHandler for HttpProxyHandler<'_> { 62 | fn parse(&mut self, data: &[u8]) -> ParseState { 63 | if self.parse_done { 64 | error!("invalid state, never call parse again!"); 65 | return ParseState::FailWithReply((HTTP_RESP_400.into(), ProxyError::BadRequest)); 66 | } 67 | 68 | if data.is_empty() { 69 | return ParseState::FailWithReply((HTTP_RESP_400.into(), ProxyError::BadRequest)); 70 | } 71 | 72 | if self.buffer.is_none() { 73 | self.buffer = Some(BUFFER_POOL.alloc(INITIAL_HTTP_HEADER_SIZE)); 74 | } 75 | 76 | let buffer = self.buffer.as_mut().unwrap(); 77 | if buffer.len() + data.len() > MAX_HTTP_HEADER_SIZE { 78 | return ParseState::FailWithReply((HTTP_RESP_413.into(), ProxyError::PayloadTooLarge)); 79 | } 80 | 81 | buffer.extend_from_slice(data); 82 | 83 | let request_text = unwrap_or_return!(find_http_request_text(buffer), ParseState::Pending); 84 | let request_text = unwrap_or_return!( 85 | std::str::from_utf8(request_text).ok(), 86 | ParseState::FailWithReply((HTTP_RESP_400.into(), ProxyError::BadRequest)) 87 | ); 88 | 89 | let parts: Vec<&str> = request_text.split("\r\n").collect(); 90 | if parts.is_empty() { 91 | return ParseState::FailWithReply((HTTP_RESP_400.into(), ProxyError::BadRequest)); 92 | } 93 | 94 | let request_text_len = request_text.len(); 95 | let mut method = ""; 96 | let mut url = ""; 97 | let mut version = ""; 98 | let mut headers = vec![]; 99 | for (index, part) in parts.iter().enumerate() { 100 | if index == 0 { 101 | let parts: Vec<&str> = part.split(' ').collect(); 102 | if parts.len() != 3 { 103 | error!("invalid http request"); 104 | return ParseState::FailWithReply(( 105 | HTTP_RESP_400.into(), 106 | ProxyError::BadRequest, 107 | )); 108 | } 109 | 110 | method = parts[0]; 111 | url = parts[1]; 112 | version = parts[2]; 113 | } else if let Some(colon_pos) = part.find(':') { 114 | let k = part[0..colon_pos].trim().to_lowercase(); 115 | let v = part[(colon_pos + 1)..].trim().to_string(); 116 | headers.push((k, v)); 117 | } 118 | } 119 | 120 | let method = method.to_string(); 121 | let url = url.to_string(); 122 | let version = version.to_string(); 123 | let buffer = self.buffer.take().unwrap(); 124 | 125 | self.parse_done = true; 126 | self.http_request = 127 | HttpRequest::build(method, url, version, headers, request_text_len, buffer); 128 | 129 | match self.http_request { 130 | Some(ref http_request) => ParseState::ReceivedRequest(&http_request.outbound_addr), 131 | None => ParseState::FailWithReply((HTTP_RESP_400.into(), ProxyError::BadRequest)), 132 | } 133 | } 134 | 135 | async fn handle( 136 | &self, 137 | outbound_type: OutboundType, 138 | outbound_stream: &mut TcpStream, 139 | inbound_stream: &mut TcpStream, 140 | ) -> Result<(), ProxyError> { 141 | let http_request = unwrap_or_return!(&self.http_request, Err(ProxyError::BadRequest)); 142 | match outbound_type { 143 | OutboundType::Direct => { 144 | Self::exchange_http_payloads(http_request, outbound_stream, inbound_stream).await 145 | } 146 | 147 | OutboundType::HttpProxy => { 148 | // simply forward the complete http proxy request 149 | utils::write_to_stream(outbound_stream, http_request.payload()).await 150 | } 151 | 152 | OutboundType::SocksProxy(ver) => { 153 | SocksReq::handshake(ver, outbound_stream, &http_request.outbound_addr).await?; 154 | Self::exchange_http_payloads(http_request, outbound_stream, inbound_stream).await 155 | } 156 | } 157 | } 158 | 159 | async fn handle_outbound_failure( 160 | &self, 161 | inbound_stream: &mut TcpStream, 162 | ) -> Result<(), ProxyError> { 163 | utils::write_to_stream(inbound_stream, HTTP_RESP_502).await 164 | } 165 | 166 | async fn reject(&self, inbound_stream: &mut TcpStream) -> Result<(), ProxyError> { 167 | let http_request = unwrap_or_return!(&self.http_request, Err(ProxyError::BadRequest)); 168 | if http_request.is_connect_request() { 169 | // we will still politely return OK to the client, and drop the connection 170 | utils::write_to_stream(inbound_stream, HTTP_RESP_200).await?; 171 | inbound_stream 172 | .flush() 173 | .await 174 | .context("stream is disconnected while flushing") 175 | .map_err(ProxyError::Disconnected) 176 | } else { 177 | Ok(()) 178 | } 179 | } 180 | } 181 | 182 | fn find_http_request_text(data: &[u8]) -> Option<&[u8]> { 183 | let len = data.len(); 184 | if len > 4 { 185 | let start_index = len - 4; 186 | if &data[start_index..len] == b"\r\n\r\n" { 187 | return Some(&data[..len]); 188 | } 189 | 190 | if let Some(start_index) = data.windows(4).position(|window| window == b"\r\n\r\n") { 191 | return Some(&data[..(start_index + 4)]); 192 | } 193 | } 194 | 195 | None 196 | } 197 | 198 | #[derive(Debug)] 199 | pub(crate) struct HttpRequest<'a> { 200 | pub method: String, 201 | _url: String, 202 | _version: String, 203 | _headers: Vec<(String, String)>, 204 | pub header_len: usize, 205 | pub buffer: PooledBuffer<'a>, 206 | pub outbound_addr: NetAddr, 207 | pub header_start_pos: usize, 208 | } 209 | 210 | impl<'a> HttpRequest<'a> { 211 | pub fn build( 212 | method: String, 213 | url: String, 214 | version: String, 215 | headers: Vec<(String, String)>, 216 | header_len: usize, 217 | mut buffer: PooledBuffer<'a>, 218 | ) -> Option { 219 | let (outbound_addr, header_start_pos) = Self::derive_outbound_addr_and_header_start_pos( 220 | &method, 221 | &url, 222 | &headers, 223 | header_len, 224 | &mut buffer, 225 | )?; 226 | 227 | Some(Self { 228 | method, 229 | _url: url, 230 | _version: version, 231 | _headers: headers, 232 | header_len, 233 | buffer, 234 | outbound_addr, 235 | header_start_pos, 236 | }) 237 | } 238 | 239 | fn derive_outbound_addr_and_header_start_pos( 240 | method: &str, 241 | url: &str, 242 | headers: &Vec<(String, String)>, 243 | header_len: usize, 244 | buffer: &mut PooledBuffer<'a>, 245 | ) -> Option<(NetAddr, usize)> { 246 | let mut addr = None; 247 | let is_connect_request = method.starts_with("CONNECT"); 248 | let mut has_host = false; 249 | if is_connect_request { 250 | // url is the address, host:port is assumed 251 | addr = Some(url); 252 | } else if let Some(host) = Self::find_host(headers) { 253 | // host is the address, host:port is assumed 254 | addr = Some(host); 255 | has_host = true; 256 | } 257 | 258 | // strip the scheme://host part of the url, for example: 259 | // GET http://example.com/test/index.html 260 | // to 261 | // GET /test/index.html 262 | let mut start_pos = 0; 263 | if has_host && url.starts_with("http") { 264 | let method_len = method.len(); 265 | let skip_len = if url.starts_with("https") { 266 | method_len + 9 // 9 for the string " https://" 267 | } else { 268 | method_len + 8 // 8 for the string " http://" 269 | }; 270 | 271 | let header_text = &mut buffer[..header_len]; 272 | if let Some(mut slash_pos) = header_text 273 | .iter() 274 | .skip(skip_len) 275 | .position(|&c| c == b'/' || c == b' ') 276 | { 277 | slash_pos += skip_len; 278 | if header_text[slash_pos] == b'/' { 279 | start_pos = slash_pos - method_len - 1; // - 1 for the space after METHOD 280 | header_text[start_pos..(start_pos + method_len)] 281 | .copy_from_slice(method.as_bytes()); 282 | header_text[slash_pos - 1] = b' '; 283 | } 284 | } 285 | } 286 | 287 | if let Some(addr) = addr { 288 | let ipv6_end_bracket_pos = addr.rfind(']').unwrap_or(0); 289 | let mut port = None; 290 | let mut host_start_pos = 0; 291 | let mut host_end_pos; 292 | if let Some(pos) = addr[ipv6_end_bracket_pos..].find(':') { 293 | port = addr[(pos + 1)..].parse().ok(); 294 | host_end_pos = pos; 295 | } else { 296 | host_end_pos = addr.len(); 297 | } 298 | 299 | if ipv6_end_bracket_pos > 0 { 300 | // exclude the square brackets 301 | host_start_pos = 1; 302 | host_end_pos = ipv6_end_bracket_pos; 303 | } 304 | let host = &addr[host_start_pos..host_end_pos]; 305 | 306 | if port.is_none() { 307 | port = if url.starts_with("https") { 308 | Some(443) 309 | } else { 310 | Some(80) 311 | } 312 | } 313 | 314 | return Some((NetAddr::new(host, port.unwrap()), start_pos)); 315 | } 316 | 317 | debug!("will parse url first: {}", url); 318 | let url = Url::parse(url); 319 | if let Ok(url) = url { 320 | if url.scheme().starts_with("http") { 321 | let mut host = url.host_str()?; 322 | if host.is_empty() { 323 | error!("invalid request: {}", url); 324 | return None; 325 | } 326 | if host.as_bytes().first().unwrap_or(&b' ') == &b'[' { 327 | host = &host[1..(host.len() - 1)]; 328 | } 329 | let mut port = url.port().unwrap_or(0); 330 | if port == 0 { 331 | port = if url.scheme().starts_with("https") { 332 | 443 333 | } else { 334 | 80 335 | } 336 | } 337 | 338 | return Some((NetAddr::new(host, port), start_pos)); 339 | } 340 | } 341 | 342 | error!("invalid request"); 343 | None 344 | } 345 | 346 | fn find_host(headers: &Vec<(String, String)>) -> Option<&str> { 347 | for (k, v) in headers { 348 | if k == "Host" || k == "host" { 349 | return Some(v); 350 | } 351 | } 352 | None 353 | } 354 | 355 | pub fn is_connect_request(&self) -> bool { 356 | self.method.starts_with("CONNECT") 357 | } 358 | 359 | /// return the complete HTTP header text, including the trailing \r\n\r\n 360 | pub fn header(&self) -> &[u8] { 361 | &self.buffer[self.header_start_pos..self.header_len] 362 | } 363 | 364 | /// return the HTTP body 365 | pub fn body(&self) -> &[u8] { 366 | &self.buffer[self.header_len..] 367 | } 368 | 369 | /// return the entire request payload 370 | pub fn payload(&self) -> &[u8] { 371 | &self.buffer[self.header_start_pos..] 372 | } 373 | } 374 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod admin; 2 | mod api; 3 | mod http; 4 | mod proxy_handler; 5 | mod proxy_rule_manager; 6 | mod quic; 7 | mod server; 8 | mod server_info_bridge; 9 | mod socks; 10 | mod udp; 11 | mod utils; 12 | 13 | use anyhow::Context; 14 | use anyhow::{bail, Result}; 15 | pub use api::Api; 16 | use byte_pool::{Block, BytePool}; 17 | use lazy_static::lazy_static; 18 | use log::warn; 19 | pub use proxy_rule_manager::ProxyRuleManager; 20 | pub use quic::quic_client::QuicClient; 21 | pub use quic::quic_server::QuicServer; 22 | use rs_utilities::log_and_bail; 23 | use serde::{Deserialize, Serialize}; 24 | pub use server::Server; 25 | use std::fmt::{Display, Formatter}; 26 | use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; 27 | use std::str::FromStr; 28 | use url::Url; 29 | 30 | const UNSPECIFIED_V4: SocketAddr = SocketAddr::new(std::net::IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0); 31 | 32 | const INTERNAL_DOMAIN_SURRFIX: [&str; 6] = [ 33 | ".home", 34 | ".lan", 35 | ".corp", 36 | ".intranet", 37 | ".private", 38 | "localhost", 39 | ]; 40 | 41 | lazy_static! { 42 | static ref BUFFER_POOL: BytePool::> = BytePool::>::new(); 43 | } 44 | 45 | type PooledBuffer<'a> = Block<'a>; 46 | 47 | #[derive(Debug)] 48 | pub enum ProxyError { 49 | ConnectionRefused, 50 | IPv6NotSupported, // not supported by Socks4 51 | InternalError, 52 | GetPeerAddrFailed, 53 | BadRequest, 54 | Timeout, 55 | PayloadTooLarge, 56 | BadGateway(anyhow::Error), 57 | Disconnected(anyhow::Error), 58 | } 59 | 60 | #[derive(PartialEq, Serialize, Deserialize, Debug, Clone)] 61 | pub enum Host { 62 | IP(IpAddr), 63 | Domain(String), 64 | } 65 | 66 | impl Display for Host { 67 | fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result { 68 | match self { 69 | Host::IP(ip) => formatter.write_fmt(format_args!("{}", ip)), 70 | Host::Domain(domain) => formatter.write_fmt(format_args!("{}", domain)), 71 | } 72 | } 73 | } 74 | 75 | #[derive(PartialEq, Serialize, Deserialize, Debug, Clone)] 76 | pub struct NetAddr { 77 | pub host: Host, 78 | pub port: u16, 79 | } 80 | 81 | impl NetAddr { 82 | pub fn new(host: &str, port: u16) -> Self { 83 | // IPv6 assumed if square brackets are found 84 | let host = if host.find('[').is_some() && host.rfind(']').is_some() { 85 | &host[1..(host.len() - 1)] 86 | } else { 87 | host 88 | }; 89 | NetAddr { 90 | host: match host.parse::() { 91 | Ok(ip) => Host::IP(ip), 92 | _ => Host::Domain(host.to_string()), 93 | }, 94 | port, 95 | } 96 | } 97 | 98 | pub fn from_domain(domain: String, port: u16) -> Self { 99 | NetAddr { 100 | host: Host::Domain(domain), 101 | port, 102 | } 103 | } 104 | 105 | pub fn from_ip(ip: IpAddr, port: u16) -> Self { 106 | NetAddr { 107 | host: Host::IP(ip), 108 | port, 109 | } 110 | } 111 | 112 | pub fn from_socket_addr(addr: SocketAddr) -> Self { 113 | NetAddr { 114 | host: Host::IP(addr.ip()), 115 | port: addr.port(), 116 | } 117 | } 118 | 119 | pub fn is_domain(&self) -> bool { 120 | matches!(self.host, Host::Domain(_)) 121 | } 122 | 123 | pub fn is_ip(&self) -> bool { 124 | !self.is_domain() 125 | } 126 | 127 | pub fn is_ipv6(&self) -> bool { 128 | match self.host { 129 | Host::IP(ip) => ip.is_ipv6(), 130 | _ => false, 131 | } 132 | } 133 | 134 | pub fn unwrap_domain(&self) -> &str { 135 | if let Host::Domain(ref domain) = self.host { 136 | domain.as_str() 137 | } else { 138 | panic!("not a domain") 139 | } 140 | } 141 | 142 | pub fn unwrap_ip(&self) -> IpAddr { 143 | if let Host::IP(ref ip) = self.host { 144 | *ip 145 | } else { 146 | panic!("not an IP") 147 | } 148 | } 149 | 150 | pub fn is_loopback(&self) -> bool { 151 | if let Host::IP(ref ip) = self.host { 152 | ip.is_loopback() 153 | } else { 154 | false 155 | } 156 | } 157 | 158 | pub fn is_internal_ip(&self) -> bool { 159 | if let Host::IP(ref ip) = self.host { 160 | match ip { 161 | IpAddr::V4(ip) => ip.is_loopback() || ip.is_private(), 162 | IpAddr::V6(ip) => ip.is_loopback(), 163 | } 164 | } else { 165 | false 166 | } 167 | } 168 | 169 | pub fn is_internal_domain(&self) -> bool { 170 | if let Host::Domain(ref domain) = self.host { 171 | INTERNAL_DOMAIN_SURRFIX 172 | .iter() 173 | .any(|suffix| domain.ends_with(suffix)) 174 | } else { 175 | false 176 | } 177 | } 178 | 179 | pub fn to_socket_addr(&self) -> Option { 180 | match self.host { 181 | Host::IP(ip) => Some(SocketAddr::new(ip, self.port)), 182 | _ => { 183 | warn!("{self} is not an IP address"); 184 | None 185 | } 186 | } 187 | } 188 | } 189 | 190 | impl std::fmt::Display for NetAddr { 191 | fn fmt(&self, formatter: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { 192 | match self.host { 193 | Host::IP(ip) if ip.is_ipv6() => { 194 | formatter.write_fmt(format_args!("[{}]:{}", self.host, self.port)) 195 | } 196 | _ => formatter.write_fmt(format_args!("{}:{}", self.host, self.port)), 197 | } 198 | } 199 | } 200 | 201 | impl FromStr for NetAddr { 202 | type Err = anyhow::Error; 203 | fn from_str(s: &str) -> Result { 204 | let colon_pos = s.rfind(':').context("port required")?; 205 | let port = s[(colon_pos + 1)..] 206 | .parse::() 207 | .context("invalid port")?; 208 | 209 | if let Some(pos) = s.rfind(']') { 210 | if pos + 1 != colon_pos { 211 | bail!("invalid ipv6 address: {s}"); 212 | } 213 | } 214 | 215 | Ok(NetAddr::new(&s[..colon_pos], port)) 216 | } 217 | } 218 | 219 | #[serde_with::skip_serializing_none] 220 | #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] 221 | pub enum ProtoType { 222 | Http, 223 | Socks5, 224 | Socks4, 225 | Tcp, 226 | Udp, 227 | } 228 | 229 | impl ProtoType { 230 | pub fn format_as_string(&self, combine_layer_proto: bool) -> String { 231 | match self { 232 | ProtoType::Http => { 233 | if combine_layer_proto { 234 | "http+quic" 235 | } else { 236 | "http" 237 | } 238 | } 239 | ProtoType::Socks5 => { 240 | if combine_layer_proto { 241 | "socks5+quic" 242 | } else { 243 | "socks5" 244 | } 245 | } 246 | ProtoType::Socks4 => { 247 | if combine_layer_proto { 248 | "socks4+quic" 249 | } else { 250 | "socks4" 251 | } 252 | } 253 | ProtoType::Tcp => { 254 | if combine_layer_proto { 255 | "tcp+quic" 256 | } else { 257 | "tcp" 258 | } 259 | } 260 | ProtoType::Udp => { 261 | if combine_layer_proto { 262 | "udp+quic" 263 | } else { 264 | "udp" 265 | } 266 | } 267 | } 268 | .to_string() 269 | } 270 | } 271 | 272 | impl std::fmt::Display for ProtoType { 273 | fn fmt(&self, formatter: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { 274 | let msg = match self { 275 | ProtoType::Http => "HTTP", 276 | ProtoType::Socks5 => "SOCKS5", 277 | ProtoType::Socks4 => "SOCKS4", 278 | ProtoType::Tcp => "TCP", 279 | ProtoType::Udp => "UDP", 280 | }; 281 | formatter.write_fmt(format_args!("{}", msg)) 282 | } 283 | } 284 | 285 | #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] 286 | pub enum QuicProtoType { 287 | HttpOverQuic, 288 | Socks5OverQuic, 289 | Socks4OverQuic, 290 | TcpOverQuic, 291 | UdpOverQuic, 292 | } 293 | 294 | #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] 295 | pub struct ServerAddr { 296 | pub proto: Option, 297 | pub net_addr: NetAddr, 298 | pub is_quic_proto: bool, 299 | } 300 | 301 | impl Display for ServerAddr { 302 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 303 | match &self.proto { 304 | Some(pt) => f.write_fmt(format_args!( 305 | "{}://{}", 306 | pt.format_as_string(self.is_quic_proto), 307 | self.net_addr 308 | )), 309 | None => f.write_fmt(format_args!("{}", self.net_addr)), 310 | } 311 | } 312 | } 313 | 314 | #[serde_with::skip_serializing_none] 315 | #[derive(Serialize, Deserialize, Debug)] 316 | pub struct Config { 317 | server_addr: ServerAddr, 318 | upstream_addr: Option, 319 | pub proxy_rules_file: String, 320 | pub workers: usize, 321 | pub dot_server: String, 322 | pub name_servers: String, 323 | pub watch_proxy_rules_change: bool, 324 | pub tcp_nodelay: bool, 325 | pub tcp_timeout_ms: u64, 326 | pub udp_timeout_ms: u64, 327 | } 328 | 329 | #[derive(Debug)] 330 | pub struct QuicServerConfig { 331 | pub server_addr: SocketAddr, 332 | pub tcp_upstream: Option, 333 | pub udp_upstream: Option, 334 | pub common_cfg: CommonQuicConfig, 335 | } 336 | 337 | #[derive(Debug, Clone)] 338 | pub struct QuicClientConfig { 339 | pub server_addr: NetAddr, 340 | pub local_tcp_server_addr: Option, 341 | pub local_udp_server_addr: Option, 342 | pub common_cfg: CommonQuicConfig, 343 | pub dot_servers: Vec, 344 | pub name_servers: Vec, 345 | } 346 | 347 | #[derive(Debug, Clone)] 348 | pub struct AddressMapping { 349 | pub server_addr: NetAddr, 350 | pub local_tcp_server_addr: Option, 351 | pub local_udp_server_addr: Option, 352 | pub common_cfg: CommonQuicConfig, 353 | } 354 | 355 | #[derive(Serialize, Deserialize, Debug, Clone)] 356 | pub struct CommonQuicConfig { 357 | pub cert: String, 358 | pub key: String, 359 | pub cipher: String, 360 | pub password: String, 361 | pub quic_timeout_ms: u64, 362 | pub tcp_timeout_ms: u64, 363 | pub udp_timeout_ms: u64, 364 | pub retry_interval_ms: u64, 365 | pub hop_interval_ms: u64, 366 | pub workers: usize, 367 | } 368 | 369 | pub fn local_socket_addr(ipv6: bool) -> SocketAddr { 370 | if ipv6 { 371 | SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 0) 372 | } else { 373 | SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0) 374 | } 375 | } 376 | 377 | pub fn unspecified_socket_addr(ipv6: bool) -> SocketAddr { 378 | if ipv6 { 379 | SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0) 380 | } else { 381 | SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0) 382 | } 383 | } 384 | 385 | pub fn parse_socket_addr(addr: &str) -> Option { 386 | let mut addr = addr.to_string(); 387 | let mut start_pos = 0; 388 | if let Some(ipv6_end_bracket_pos) = addr.find(']') { 389 | start_pos = ipv6_end_bracket_pos + 1; 390 | } 391 | if addr[start_pos..].find(':').is_none() { 392 | addr = format!("127.0.0.1:{}", addr); 393 | } 394 | addr.parse().ok() 395 | } 396 | 397 | #[rustfmt::skip] 398 | pub fn parse_server_addr(addr: &str) -> Result> { 399 | if addr.trim().is_empty() { 400 | return Ok(None); 401 | } 402 | 403 | let supported_protocols: &[(Option, Option, &str)] = &[ 404 | (None, None, "unspecified"), 405 | (Some(ProtoType::Http), None, "http"), 406 | (Some(ProtoType::Socks5), None, "socks5"), 407 | (Some(ProtoType::Socks4), None, "socks4"), 408 | (Some(ProtoType::Tcp), None, "tcp"), 409 | (Some(ProtoType::Udp), None, "udp"), 410 | (Some(ProtoType::Http), Some(QuicProtoType::HttpOverQuic), "http+quic"), 411 | (Some(ProtoType::Socks5), Some(QuicProtoType::Socks5OverQuic), "socks5+quic"), 412 | (Some(ProtoType::Socks4), Some(QuicProtoType::Socks4OverQuic), "socks4+quic"), 413 | (Some(ProtoType::Tcp), Some(QuicProtoType::TcpOverQuic), "tcp+quic"), 414 | (Some(ProtoType::Udp), Some(QuicProtoType::UdpOverQuic), "udp+quic"), 415 | ]; 416 | 417 | let addr = format_addr(addr, "127.0.0.1")?; 418 | let url = match Url::parse(addr.as_str()) { 419 | Ok(url) => url, 420 | _ => { 421 | log_and_bail!("invalid protocol: {addr}"); 422 | } 423 | }; 424 | 425 | if !url.has_host() { 426 | log_and_bail!("invalid address: {addr}"); 427 | } 428 | 429 | let mut serv_proto = None; 430 | let mut quic_proto = None; 431 | supported_protocols.iter().for_each(|v| { 432 | if url.scheme() == v.2 { 433 | serv_proto = v.0.clone(); 434 | quic_proto = v.1.clone(); 435 | } 436 | }); 437 | 438 | if url.scheme() != "unspecified" && serv_proto.is_none() { 439 | log_and_bail!("invalid scheme: {}", url.scheme()); 440 | } 441 | 442 | let net_addr = NetAddr::new( 443 | url.host().unwrap().to_string().as_str(), 444 | url.port_or_known_default().unwrap(), 445 | ); 446 | 447 | Ok(Some(ServerAddr{ 448 | proto: serv_proto, 449 | net_addr, 450 | is_quic_proto: quic_proto.is_some(), 451 | })) 452 | } 453 | 454 | fn format_addr(addr: &str, default_ip: &str) -> Result { 455 | let addr = if addr.contains("://") { 456 | addr.to_string() 457 | } else if addr.starts_with('[') && !addr.contains("]:") { 458 | log_and_bail!("Server address must contain a port, e.g. [::1]:3515"); 459 | } else if addr.contains('.') && !addr.contains(':') { 460 | log_and_bail!("Server address must contain a port, e.g. 127.0.0.1:3515"); 461 | } else if !addr.contains(':') { 462 | format!("unspecified://{default_ip}:{addr}") 463 | } else { 464 | format!("unspecified://{addr}") 465 | }; 466 | Ok(addr) 467 | } 468 | 469 | #[rustfmt::skip] 470 | #[allow(clippy::too_many_arguments)] 471 | pub fn create_config( 472 | server_addr: String, 473 | upstream_addr: String, 474 | dot_server: String, 475 | name_servers: String, 476 | proxy_rules_file: String, 477 | workers: usize, 478 | watch_proxy_rules_change: bool, 479 | tcp_nodelay: bool, 480 | mut tcp_timeout_ms: u64, 481 | mut udp_timeout_ms: u64, 482 | ) -> Result { 483 | let server_addr = match parse_server_addr(&server_addr)? { 484 | Some(server_addr) => server_addr, 485 | None => { 486 | log_and_bail!("invalid server address: {server_addr}"); 487 | } 488 | }; 489 | 490 | let upstream_addr = parse_server_addr(&upstream_addr)?; 491 | 492 | if tcp_timeout_ms == 0 { 493 | tcp_timeout_ms = 30000; 494 | } 495 | if udp_timeout_ms == 0 { 496 | udp_timeout_ms = 5000; 497 | } 498 | 499 | #[allow(warnings)] 500 | if let Some(upstream_addr) = &upstream_addr { 501 | if server_addr.is_quic_proto && upstream_addr.is_quic_proto { 502 | log_and_bail!( 503 | "QUIC server and QUIC upstream cannot be chained: {} -> {upstream_addr}", 504 | server_addr.net_addr.to_socket_addr().unwrap() 505 | ); 506 | } 507 | 508 | if upstream_addr.net_addr.is_domain() && !upstream_addr.is_quic_proto { 509 | log_and_bail!("only IP address is allowed for upstream with non-quic protocols, invalid upstream: {upstream_addr}"); 510 | } 511 | } 512 | 513 | match (server_addr.proto.clone(), upstream_addr.as_ref().and_then(|u| u.proto.clone())) { 514 | (None, Some(ProtoType::Http)) | 515 | (None, Some(ProtoType::Socks5)) | 516 | (None, Some(ProtoType::Socks4)) | 517 | (Some(ProtoType::Http), Some(ProtoType::Http)) | 518 | (Some(ProtoType::Http), Some(ProtoType::Socks5)) | 519 | (Some(ProtoType::Http), Some(ProtoType::Socks4)) | 520 | (Some(ProtoType::Http), None) | 521 | (Some(ProtoType::Socks5), Some(ProtoType::Http)) | 522 | (Some(ProtoType::Socks5), Some(ProtoType::Socks5)) | 523 | (Some(ProtoType::Socks5), Some(ProtoType::Socks4)) | 524 | (Some(ProtoType::Socks5), None) | 525 | (Some(ProtoType::Socks4), Some(ProtoType::Http)) | 526 | (Some(ProtoType::Socks4), Some(ProtoType::Socks5)) | 527 | (Some(ProtoType::Socks4), Some(ProtoType::Socks4)) | 528 | (Some(ProtoType::Socks4), None) | 529 | (Some(ProtoType::Tcp), Some(ProtoType::Tcp)) | 530 | (Some(ProtoType::Udp), Some(ProtoType::Udp)) | 531 | (None, None) => {} 532 | _ => { 533 | log_and_bail!("proto chaining not supported: {server_addr} → {:?}", 534 | upstream_addr.as_ref().map_or("".to_string(), |u| u.net_addr.to_string())); 535 | } 536 | } 537 | 538 | let worker_threads = if workers > 0 { 539 | workers 540 | } else { 541 | num_cpus::get() 542 | }; 543 | 544 | Ok(Config { 545 | server_addr, 546 | upstream_addr, 547 | proxy_rules_file, 548 | workers: worker_threads, 549 | dot_server, 550 | name_servers, 551 | watch_proxy_rules_change, 552 | tcp_nodelay, 553 | tcp_timeout_ms, 554 | udp_timeout_ms 555 | }) 556 | } 557 | 558 | #[cfg(target_os = "android")] 559 | #[allow(non_snake_case)] 560 | pub mod android { 561 | extern crate jni; 562 | 563 | use jni::sys::{jlong, jstring}; 564 | 565 | use self::jni::objects::{JClass, JString}; 566 | use self::jni::sys::{jboolean, jint, JNI_TRUE}; 567 | use self::jni::JNIEnv; 568 | use super::*; 569 | use log::error; 570 | use std::sync::Arc; 571 | use std::thread; 572 | 573 | #[no_mangle] 574 | pub unsafe extern "C" fn Java_net_neevek_omnip_Omnip_nativeInitLogger( 575 | mut env: JNIEnv, 576 | _: JClass, 577 | jlogLevel: JString, 578 | ) -> jboolean { 579 | let log_level = match get_string(&mut env, &jlogLevel).as_str() { 580 | "T" => "trace", 581 | "D" => "debug", 582 | "I" => "info", 583 | "W" => "warn", 584 | "E" => "error", 585 | _ => "info", 586 | }; 587 | let log_filter = format!( 588 | "omnip={},rstun={},rs_utilities={}", 589 | log_level, log_level, log_level 590 | ); 591 | rs_utilities::LogHelper::init_logger("omnip", log_filter.as_str()); 592 | return JNI_TRUE; 593 | } 594 | 595 | #[no_mangle] 596 | pub unsafe extern "C" fn Java_net_neevek_omnip_Omnip_nativeCreate( 597 | mut env: JNIEnv, 598 | _: JClass, 599 | jaddr: JString, 600 | jupstream: JString, 601 | jdotServer: JString, 602 | jnameServers: JString, 603 | jproxyRulesFile: JString, 604 | jcert: JString, 605 | jkey: JString, 606 | jcipher: JString, 607 | jpassword: JString, 608 | jquicTimeoutMs: jint, 609 | jretryIntervalMs: jint, 610 | jhopIntervalMs: jint, 611 | jworkers: jint, 612 | jtcpNoDelay: jboolean, 613 | jtcpTimeoutMs: jlong, 614 | judpTimeoutMs: jlong, 615 | ) -> jlong { 616 | if jaddr.is_null() { 617 | return 0; 618 | } 619 | 620 | let addr = get_string(&mut env, &jaddr); 621 | let upstream = get_string(&mut env, &jupstream); 622 | let dot_server = get_string(&mut env, &jdotServer); 623 | let name_servers = get_string(&mut env, &jnameServers); 624 | let proxy_rules_file = get_string(&mut env, &jproxyRulesFile); 625 | let cert = get_string(&mut env, &jcert); 626 | let key = get_string(&mut env, &jkey); 627 | let cipher = get_string(&mut env, &jcipher); 628 | let password = get_string(&mut env, &jpassword); 629 | 630 | let config = match create_config( 631 | addr, 632 | upstream, 633 | dot_server, 634 | name_servers, 635 | proxy_rules_file, 636 | jworkers as usize, 637 | false, 638 | jtcpNoDelay != 0, 639 | jtcpTimeoutMs as u64, 640 | judpTimeoutMs as u64, 641 | ) { 642 | Ok(config) => config, 643 | Err(e) => { 644 | error!("failed to create config: {}", e); 645 | return 0; 646 | } 647 | }; 648 | 649 | let common_quic_config = CommonQuicConfig { 650 | cert, 651 | key, 652 | password, 653 | cipher, 654 | quic_timeout_ms: jquicTimeoutMs as u64, 655 | retry_interval_ms: jretryIntervalMs as u64, 656 | hop_interval_ms: jhopIntervalMs as u64, 657 | workers: jworkers as usize, 658 | tcp_timeout_ms: jtcpTimeoutMs as u64, 659 | udp_timeout_ms: judpTimeoutMs as u64, 660 | }; 661 | 662 | Box::into_raw(Box::new(Server::new(config, common_quic_config))) as jlong 663 | } 664 | 665 | #[no_mangle] 666 | pub unsafe extern "C" fn Java_net_neevek_omnip_Omnip_nativeStart( 667 | _env: JNIEnv, 668 | _: JClass, 669 | server_ptr: jlong, 670 | ) { 671 | if server_ptr == 0 { 672 | return; 673 | } 674 | 675 | let server = &mut *(server_ptr as *mut Arc); 676 | if server.has_scheduled_start() { 677 | return; 678 | } 679 | 680 | server.set_scheduled_start(); 681 | thread::spawn(move || { 682 | let server = &mut *(server_ptr as *mut Arc); 683 | server.run().ok(); 684 | }); 685 | } 686 | 687 | #[no_mangle] 688 | pub unsafe extern "C" fn Java_net_neevek_omnip_Omnip_nativeGetState( 689 | env: JNIEnv, 690 | _: JClass, 691 | server_ptr: jlong, 692 | ) -> jstring { 693 | if server_ptr == 0 { 694 | return env.new_string("").unwrap().into_raw(); 695 | } 696 | 697 | let server = &mut *(server_ptr as *mut Arc); 698 | env.new_string(server.get_state().to_string()) 699 | .unwrap() 700 | .into_raw() 701 | } 702 | 703 | #[no_mangle] 704 | pub unsafe extern "C" fn Java_net_neevek_omnip_Omnip_nativeStop( 705 | _env: JNIEnv, 706 | _: JClass, 707 | server_ptr: jlong, 708 | ) { 709 | if server_ptr != 0 { 710 | let _boxed_server = Box::from_raw(server_ptr as *mut Arc); 711 | } 712 | } 713 | 714 | #[no_mangle] 715 | pub unsafe extern "C" fn Java_net_neevek_omnip_Omnip_nativeSetEnableOnInfoReport( 716 | env: JNIEnv, 717 | jobj: JClass, 718 | server_ptr: jlong, 719 | enable: jboolean, 720 | ) { 721 | if server_ptr == 0 { 722 | return; 723 | } 724 | 725 | let server = &mut *(server_ptr as *mut Arc); 726 | let bool_enable = enable == 1; 727 | if bool_enable && !server.has_on_info_listener() { 728 | let jvm = env.get_java_vm().unwrap(); 729 | let jobj_global_ref = env.new_global_ref(jobj).unwrap(); 730 | server.set_on_info_listener(move |data: &str| { 731 | let mut env = jvm.attach_current_thread().unwrap(); 732 | if let Ok(s) = env.new_string(data) { 733 | env.call_method( 734 | &jobj_global_ref, 735 | "onInfo", 736 | "(Ljava/lang/String;)V", 737 | &[(&s).into()], 738 | ) 739 | .unwrap(); 740 | } 741 | }); 742 | } 743 | 744 | server.set_enable_on_info_report(bool_enable); 745 | } 746 | 747 | fn get_string(env: &mut JNIEnv, jstr: &JString) -> String { 748 | if !jstr.is_null() { 749 | env.get_string(&jstr).unwrap().to_string_lossy().to_string() 750 | } else { 751 | String::from("") 752 | } 753 | } 754 | } 755 | --------------------------------------------------------------------------------