├── .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 |
88 |
89 |
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 | 
91 | 
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