From 427f28f5ad3213b27f48d79ba9d004d23aac47da Mon Sep 17 00:00:00 2001 From: v0 Date: Sun, 26 Apr 2026 22:05:33 +0000 Subject: [PATCH 1/2] add server_address field to inbound form and update project components Co-authored-by: Ehsan <1883051+ehsanking@users.noreply.github.com> --- app/api/inbounds/route.ts | 11 ++++++----- components/views/inbounds-view.tsx | 25 ++++++++++++++++++++++--- schema.sql | 1 + scripts/migrate-inbounds-v2.sql | 3 +++ 4 files changed, 32 insertions(+), 8 deletions(-) diff --git a/app/api/inbounds/route.ts b/app/api/inbounds/route.ts index 9b34524..52e7c10 100644 --- a/app/api/inbounds/route.ts +++ b/app/api/inbounds/route.ts @@ -24,6 +24,7 @@ export async function POST(req: Request) { name, protocol, port, + server_address, remark, // OpenVPN fields ovpn_protocol, @@ -60,8 +61,8 @@ export async function POST(req: Request) { } = body; // Validate required fields - if (!name || !protocol || !port) { - return NextResponse.json({ error: 'Name, protocol, and port are required' }, { status: 400 }); + if (!name || !protocol || !port || !server_address) { + return NextResponse.json({ error: 'Name, protocol, port and server address are required' }, { status: 400 }); } // Validate protocol @@ -84,9 +85,9 @@ export async function POST(req: Request) { } // Build SQL based on protocol type - const columns = ['name', 'protocol', 'port', 'remark', 'status']; - const values: any[] = [name, protocol, parseInt(port, 10), remark || '', 'active']; - const placeholders = ['?', '?', '?', '?', '?']; + const columns = ['name', 'protocol', 'port', 'server_address', 'remark', 'status']; + const values: any[] = [name, protocol, parseInt(port, 10), server_address, remark || '', 'active']; + const placeholders = ['?', '?', '?', '?', '?', '?']; // Add protocol-specific fields switch (protocol) { diff --git a/components/views/inbounds-view.tsx b/components/views/inbounds-view.tsx index d584ac6..7eeb46b 100644 --- a/components/views/inbounds-view.tsx +++ b/components/views/inbounds-view.tsx @@ -33,6 +33,7 @@ interface Inbound { name: string; protocol: string; port: number; + server_address: string; remark: string; status: string; created_at: string; @@ -52,6 +53,7 @@ interface InboundFormData { name: string; protocol: string; port: string; + server_address: string; remark: string; // OpenVPN ovpn_protocol: string; @@ -91,6 +93,7 @@ const initialFormData: InboundFormData = { name: '', protocol: 'openvpn', port: '', + server_address: '', remark: '', // OpenVPN defaults ovpn_protocol: 'udp', @@ -231,8 +234,8 @@ export default function InboundsView() { const handleCreate = async (e: React.FormEvent) => { e.preventDefault(); - if (!formData.name || !formData.port) { - return toast.error('Name and port are required'); + if (!formData.name || !formData.port || !formData.server_address) { + return toast.error('Name, server address and port are required'); } try { @@ -728,7 +731,7 @@ export default function InboundsView() {
{/* Basic Info */} -
+
+
+ + updateFormField('server_address', e.target.value)} + className="w-full bg-slate-50 border border-slate-100 rounded-xl px-4 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none transition-all" + placeholder="e.g. 185.12.34.56 or vpn.example.com" + /> +
+
+
setMainServerIp(e.target.value)} + placeholder="185.x.x.x" + className="w-full bg-white/10 border border-white/20 rounded-xl px-4 py-2.5 text-white placeholder:text-slate-500 focus:ring-2 focus:ring-emerald-500 focus:outline-none" + /> +
+
+ + setMainServerPort(e.target.value)} + placeholder="8443" + className="w-full bg-white/10 border border-white/20 rounded-xl px-4 py-2.5 text-white placeholder:text-slate-500 focus:ring-2 focus:ring-emerald-500 focus:outline-none" + /> +
+
+
+ + {/* Nodes Table */} +
+ {loading ? ( +
+ +

Loading nodes...

+
+ ) : nodes.length === 0 ? ( +
+ +
+

No tunnel nodes configured

+

Add your first node to enable multi-location VPN

+
+
+ ) : ( +
+ + + + + + + + + + + + + {nodes.map((node) => ( + + + + + + + + + ))} + +
LocationRemote IPTunnel TypePortStatusActions
+
+
+ {node.flag_emoji || COUNTRY_FLAGS[node.country_code || ''] || '🌍'} +
+
+

{node.name}

+

+ + {node.location} +

+
+
+
+ {node.remote_ip} + + + {node.tunnel_type} + + + {node.tunnel_port} + + + {node.status === 'online' ? : } + {node.status} + + +
+ + +
+
+
+ )} +
+ + {/* Add Node Modal */} + setShowAddModal(false)} + onSuccess={() => { + fetchNodes(); + setShowAddModal(false); + }} + /> + + {/* Command Modal */} + setShowCommandModal(false)} + node={selectedNode} + mainServerIp={mainServerIp} + mainServerPort={parseInt(mainServerPort, 10) || 8443} + onCopy={copyToClipboard} + copiedId={copiedId} + /> + + ); +} + +// Add Node Modal Component +function AddTunnelNodeModal({ + isOpen, + onClose, + onSuccess +}: { + isOpen: boolean; + onClose: () => void; + onSuccess: () => void; +}) { + const [formData, setFormData] = useState({ + name: '', + location: '', + country_code: '', + remote_ip: '', + tunnel_port: '443', + tunnel_type: getRecommendedTunnelType(), + local_forward_port: '10000', + sni_host: 'www.google.com', + }); + const [isSubmitting, setIsSubmitting] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!formData.name || !formData.location || !formData.remote_ip) { + toast.error('Please fill all required fields'); + return; + } + + setIsSubmitting(true); + try { + const res = await fetch('/api/tunnel-nodes', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + ...formData, + tunnel_port: parseInt(formData.tunnel_port, 10), + local_forward_port: parseInt(formData.local_forward_port, 10), + flag_emoji: COUNTRY_FLAGS[formData.country_code] || null, + }), + }); + + if (!res.ok) throw new Error('Failed to create node'); + + toast.success('Tunnel node created successfully'); + onSuccess(); + setFormData({ + name: '', + location: '', + country_code: '', + remote_ip: '', + tunnel_port: '443', + tunnel_type: getRecommendedTunnelType(), + local_forward_port: '10000', + sni_host: 'www.google.com', + }); + } catch { + toast.error('Failed to create tunnel node'); + } finally { + setIsSubmitting(false); + } + }; + + if (!isOpen) return null; + + return ( +
+ + + +
+
+
+

Add Tunnel Node

+

Configure a new location for DPI-resistant connection

+
+ +
+
+ + +
+
+ + setFormData(prev => ({ ...prev, name: e.target.value }))} + placeholder="e.g. Paris-01" + className="w-full bg-slate-50 border border-slate-100 rounded-xl px-4 py-2.5 text-sm focus:ring-2 focus:ring-emerald-500 focus:outline-none" + /> +
+
+ + setFormData(prev => ({ ...prev, location: e.target.value }))} + placeholder="e.g. Paris, France" + className="w-full bg-slate-50 border border-slate-100 rounded-xl px-4 py-2.5 text-sm focus:ring-2 focus:ring-emerald-500 focus:outline-none" + /> +
+
+ +
+
+ + setFormData(prev => ({ ...prev, remote_ip: e.target.value }))} + placeholder="185.x.x.x" + className="w-full bg-slate-50 border border-slate-100 rounded-xl px-4 py-2.5 text-sm font-mono focus:ring-2 focus:ring-emerald-500 focus:outline-none" + /> +
+
+ + +
+
+ +
+
+ + +

+ {getTunnelTypeDescription(formData.tunnel_type)} +

+
+
+ + setFormData(prev => ({ ...prev, tunnel_port: e.target.value }))} + placeholder="443" + className="w-full bg-slate-50 border border-slate-100 rounded-xl px-4 py-2.5 text-sm focus:ring-2 focus:ring-emerald-500 focus:outline-none" + /> +

Port 443 recommended for HTTPS mimicry

+
+
+ +
+
+ + setFormData(prev => ({ ...prev, local_forward_port: e.target.value }))} + placeholder="10000" + className="w-full bg-slate-50 border border-slate-100 rounded-xl px-4 py-2.5 text-sm focus:ring-2 focus:ring-emerald-500 focus:outline-none" + /> +
+
+ + setFormData(prev => ({ ...prev, sni_host: e.target.value }))} + placeholder="www.google.com" + className="w-full bg-slate-50 border border-slate-100 rounded-xl px-4 py-2.5 text-sm focus:ring-2 focus:ring-emerald-500 focus:outline-none" + /> +

Fake SNI to bypass DPI detection

+
+
+ +
+ +
+ +
+
+ ); +} + +// Command Modal Component +function CommandModal({ + isOpen, + onClose, + node, + mainServerIp, + mainServerPort, + onCopy, + copiedId, +}: { + isOpen: boolean; + onClose: () => void; + node: TunnelNode | null; + mainServerIp: string; + mainServerPort: number; + onCopy: (text: string, id: string) => void; + copiedId: string | null; +}) { + const [showSecret, setShowSecret] = useState(false); + const [activeTab, setActiveTab] = useState<'remote' | 'main'>('remote'); + + if (!isOpen || !node) return null; + + const remoteCommand = generateRemoteNodeCommand(node, mainServerIp || 'YOUR_MAIN_SERVER_IP', mainServerPort); + const mainCommand = generateMainServerCommand( + { ip: mainServerIp || 'YOUR_MAIN_SERVER_IP', port: mainServerPort }, + node.tunnel_type, + node.tunnel_secret, + node.local_forward_port + ); + + return ( +
+ + + +
+
+
+
+ {node.flag_emoji || COUNTRY_FLAGS[node.country_code || ''] || '🌍'} +
+
+

{node.name}

+

{node.location} - Setup Commands

+
+
+ +
+
+ +
+ {/* Warning if no main server IP */} + {!mainServerIp && ( +
+

+ Please enter your Main Server IP in the configuration section above to generate correct commands. +

+
+ )} + + {/* Tunnel Info */} +
+
+
+

Tunnel Type

+

{node.tunnel_type.toUpperCase()}

+
+
+

Remote IP

+

{node.remote_ip}

+
+
+

Port

+

{node.tunnel_port}

+
+
+
+
+
+

Tunnel Secret

+

+ {showSecret ? node.tunnel_secret : '••••••••••••••••••••'} +

+
+ +
+
+
+ + {/* Tabs */} +
+ + +
+ + {/* Command Display */} +
+
+
+                {activeTab === 'remote' ? remoteCommand : mainCommand}
+              
+
+ +
+ + {/* Instructions */} +
+

+ + Setup Instructions +

+
    +
  1. First, run the Main Server Command on your panel server to start the tunnel listener
  2. +
  3. Then, run the Remote Node Command on the remote server ({node.location})
  4. +
  5. The tunnel will establish automatically and traffic will flow through securely
  6. +
  7. Use the systemd service for auto-start on remote server reboot
  8. +
+
+
+
+
+ ); +} diff --git a/lib/tunnel-commands.ts b/lib/tunnel-commands.ts new file mode 100644 index 0000000..62cfb81 --- /dev/null +++ b/lib/tunnel-commands.ts @@ -0,0 +1,182 @@ +/** + * DPI-Resistant Tunnel Command Generator + * + * Generates commands for establishing secure tunnels between nodes + * Using Gost (Go Simple Tunnel) for maximum DPI bypass capability + * + * Tunnel Types: + * - WSS: WebSocket over TLS (looks like HTTPS traffic) + * - gRPC: HTTP/2 based (looks like Google services) + * - QUIC: UDP-based encrypted protocol + * - H2: HTTP/2 tunnel + */ + +export interface TunnelNode { + id: number; + name: string; + location: string; + country_code?: string; + flag_emoji?: string; + remote_ip: string; + tunnel_port: number; + tunnel_type: 'wss' | 'grpc' | 'quic' | 'h2'; + tunnel_secret: string; + local_forward_port: number; + sni_host: string; + status: string; +} + +export interface MainServerConfig { + ip: string; + port: number; +} + +/** + * Generate the command to run on the MAIN server (where panel is installed) + * This creates the listener that remote nodes will connect to + */ +export function generateMainServerCommand( + mainServer: MainServerConfig, + tunnelType: string, + tunnelSecret: string, + localForwardPort: number +): string { + const authHeader = Buffer.from(`admin:${tunnelSecret}`).toString('base64'); + + switch (tunnelType) { + case 'wss': + return `# Install Gost on Main Server +curl -L https://github.com/ginuerzh/gost/releases/download/v2.11.5/gost-linux-amd64-2.11.5.gz -o gost.gz && gunzip gost.gz && chmod +x gost && mv gost /usr/local/bin/ + +# Run Tunnel Listener (WSS - Looks like HTTPS) +gost -L "relay+wss://:${mainServer.port}?auth=${authHeader}&path=/ws&cert=/etc/ssl/certs/server.crt&key=/etc/ssl/private/server.key"`; + + case 'grpc': + return `# Install Gost on Main Server +curl -L https://github.com/ginuerzh/gost/releases/download/v2.11.5/gost-linux-amd64-2.11.5.gz -o gost.gz && gunzip gost.gz && chmod +x gost && mv gost /usr/local/bin/ + +# Run Tunnel Listener (gRPC - Looks like Google services) +gost -L "relay+grpc://:${mainServer.port}?auth=${authHeader}"`; + + case 'quic': + return `# Install Gost on Main Server +curl -L https://github.com/ginuerzh/gost/releases/download/v2.11.5/gost-linux-amd64-2.11.5.gz -o gost.gz && gunzip gost.gz && chmod +x gost && mv gost /usr/local/bin/ + +# Run Tunnel Listener (QUIC - UDP encrypted) +gost -L "relay+quic://:${mainServer.port}?auth=${authHeader}"`; + + case 'h2': + return `# Install Gost on Main Server +curl -L https://github.com/ginuerzh/gost/releases/download/v2.11.5/gost-linux-amd64-2.11.5.gz -o gost.gz && gunzip gost.gz && chmod +x gost && mv gost /usr/local/bin/ + +# Run Tunnel Listener (HTTP/2) +gost -L "relay+h2://:${mainServer.port}?auth=${authHeader}"`; + + default: + return `# Unknown tunnel type: ${tunnelType}`; + } +} + +/** + * Generate the command to run on the REMOTE node server + * This connects back to the main server and creates the tunnel + */ +export function generateRemoteNodeCommand( + node: TunnelNode, + mainServerIp: string, + mainServerListenPort: number +): string { + const authHeader = Buffer.from(`admin:${node.tunnel_secret}`).toString('base64'); + + const installCmd = `# ============================================ +# Tunnel Setup for: ${node.name} (${node.location}) +# ============================================ + +# Step 1: Install Gost +curl -L https://github.com/ginuerzh/gost/releases/download/v2.11.5/gost-linux-amd64-2.11.5.gz -o gost.gz +gunzip gost.gz +chmod +x gost +mv gost /usr/local/bin/ + +`; + + let tunnelCmd = ''; + + switch (node.tunnel_type) { + case 'wss': + tunnelCmd = `# Step 2: Start Tunnel (WSS - DPI Resistant) +# This will forward local port ${node.local_forward_port} through the tunnel +gost -L "tcp://:${node.local_forward_port}" -F "relay+wss://${mainServerIp}:${mainServerListenPort}?auth=${authHeader}&path=/ws&serverName=${node.sni_host}"`; + break; + + case 'grpc': + tunnelCmd = `# Step 2: Start Tunnel (gRPC - Looks like Google traffic) +gost -L "tcp://:${node.local_forward_port}" -F "relay+grpc://${mainServerIp}:${mainServerListenPort}?auth=${authHeader}"`; + break; + + case 'quic': + tunnelCmd = `# Step 2: Start Tunnel (QUIC - UDP based) +gost -L "tcp://:${node.local_forward_port}" -F "relay+quic://${mainServerIp}:${mainServerListenPort}?auth=${authHeader}"`; + break; + + case 'h2': + tunnelCmd = `# Step 2: Start Tunnel (HTTP/2) +gost -L "tcp://:${node.local_forward_port}" -F "relay+h2://${mainServerIp}:${mainServerListenPort}?auth=${authHeader}"`; + break; + } + + const systemdService = ` + +# ============================================ +# Optional: Create Systemd Service for Auto-Start +# ============================================ +cat > /etc/systemd/system/gost-tunnel.service << 'EOF' +[Unit] +Description=Gost Tunnel to Main Server +After=network.target + +[Service] +Type=simple +ExecStart=/usr/local/bin/gost -L "tcp://:${node.local_forward_port}" -F "relay+${node.tunnel_type}://${mainServerIp}:${mainServerListenPort}?auth=${authHeader}${node.tunnel_type === 'wss' ? `&path=/ws&serverName=${node.sni_host}` : ''}" +Restart=always +RestartSec=5 + +[Install] +WantedBy=multi-user.target +EOF + +systemctl daemon-reload +systemctl enable gost-tunnel +systemctl start gost-tunnel + +# Check status +systemctl status gost-tunnel`; + + return installCmd + tunnelCmd + systemdService; +} + +/** + * Get human-readable description of tunnel type + */ +export function getTunnelTypeDescription(type: string): string { + switch (type) { + case 'wss': + return 'WebSocket over TLS - Appears as normal HTTPS traffic, best for bypassing DPI'; + case 'grpc': + return 'gRPC over HTTP/2 - Mimics Google services traffic'; + case 'quic': + return 'QUIC Protocol - Fast UDP-based encrypted tunnel'; + case 'h2': + return 'HTTP/2 Tunnel - Standard HTTP/2 based connection'; + default: + return 'Unknown tunnel type'; + } +} + +/** + * Get recommended tunnel type based on conditions + */ +export function getRecommendedTunnelType(): 'wss' | 'grpc' | 'quic' | 'h2' { + // WSS is generally the most DPI-resistant for Iranian firewall + return 'wss'; +} diff --git a/panel.sqlite b/panel.sqlite index e45ab6771a06c33eb9de54fc7c4a26e754bf2879..9ce367c052c474ccca3e1d27c9210609212066b7 100644 GIT binary patch delta 641 zcmZ`$PiqrF6wfwmc1_aa7MmbN7{ri-hGI=~Q7_YU)j*ntWGlT*C%cn$%64bjnYhWx z_Tt4$We&NDcvTP&g5K-dZ&1PSQCBvGl;VR2kN4j1&*Pn3s3-4koUIy0B9T~$@?R{y ztyC4`^TCJ9+)WiGvOhAX>5uwv-PO+Zs`_1pUyNHt_0?!u%ReqCr+2C6kP&r8NDMuX zxX5D;;fM)OePW?IT zlr|oeD`^l#-^Bw&j@c2tJ`1A0Ac*>tgGFVvHg)2&Ktv~7ptT$2Ub6|d&8Kj$X@Skn zYD_xa6l0&rvxQtf)J(q7KAu(&^3h^%RPz?85c974{NpZOuQI| z;rf8FflJnHc3hMfnR}W~JcoLN$*(14?Y@h-K!*eeqDQa*R%6HPT5zWwv&XNHKeKn_ xr<9f#Ohk@Efct%Fquy8$4z?Q4B-Eh1Os0}U-