Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
/nhclient
dist/

# Configurations
*.json

# AI Agents
CLAUDE.md
claude.md
55 changes: 40 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,11 @@ Nethopper enables users in a restricted network to access the internet through a
│ (SOCKS5 client) (Linux VM) (Bridge PC) │
│ │ │ │ │
│ │ SOCKS5 │ VLESS Reverse │ │
│ │ :1080 │ Tunnel :2083 │ │
│ ▼ │ │ │
│ ▼ │ Tunnel │ │
│ ┌────────────────────────────────┤ │ │
│ │ RESTRICTED NETWORK │◄───────────────────────┤ │
│ │ │ restricted interface │ │
│ │ Users connect to Server:1080 │ │ │
│ │ Users connect to server │ │ │
│ │ via SOCKS5 proxy │ │ │
│ └────────────────────────────────┤ │ │
│ │ │ │
Expand All @@ -36,27 +35,34 @@ Nethopper enables users in a restricted network to access the internet through a
└─────────────────────────────────────────────────────────────────────┘

Data flow:
1. App → SOCKS5 (:1080) → nhserver
2. nhserver (Portal) → VLESS reverse tunnel (:2083) → nhclient (Bridge)
1. App → SOCKS5 → nhserver
2. nhserver (Portal) → VLESS reverse tunnel → nhclient (Bridge)
3. nhclient → free interface → Internet
```

## Components

| Component | Binary | Platforms | Description |
|-----------|--------|-----------|-------------|
| Server | `nhserver` | Linux | Xray portal + SOCKS5 inbound, runs as systemd service |
| Server | `nhserver` | Linux | Xray portal + SOCKS5 inbound (standalone or via x-ui) |
| Client | `nhclient` | Linux, macOS, Windows | Xray bridge, connects to server, routes via free interface |

Xray-core is downloaded automatically on first install. Set `NETHOPPER_XRAY_PATH` to use a local binary instead:
### Server Modes

- **Standalone**: Downloads Xray, creates a systemd service, and manages everything independently.
- **x-ui integration**: Delegates inbound and Xray management to an existing [3x-ui](https://github.com/MHSanaei/3x-ui) panel. Nethopper creates SOCKS5 and VLESS inbounds via the x-ui API and adds portal/routing rules to x-ui's Xray config. No separate Xray binary or systemd service is needed.

If x-ui is detected on the system, the TUI will prompt you to choose the mode. In CLI mode, x-ui integration is used automatically unless `--standalone` is passed.

Ports are assigned randomly by default. You can override them with `--socks-port` and `--tunnel-port`.

In standalone mode, Xray-core is downloaded automatically on first install. Set `NETHOPPER_XRAY_PATH` to use a local binary instead:

```bash
sudo NETHOPPER_XRAY_PATH=/path/to/xray nhserver install
NETHOPPER_XRAY_PATH=/path/to/xray nhclient install
```

On the server, the binary is copied to `/usr/local/bin/xray` so the systemd service can access it regardless of where the original is located.

## Requirements

### Server (nhserver)
Expand All @@ -82,12 +88,19 @@ Both `nhserver` and `nhclient` can be used in two ways:
### 1. Install and Set Up the Server

```bash
# Standalone (or auto-detect x-ui)
sudo nhserver install

# Explicitly standalone even if x-ui is present
sudo nhserver install --standalone

# x-ui integration with custom ports
sudo nhserver install --xui-user admin --xui-pass secret --socks-port 8080 --tunnel-port 3000
```

Or via TUI: run `sudo nhserver` and select **Install**.

This downloads Xray, generates config with a random UUID, creates a systemd service, and configures the firewall.
Standalone mode downloads Xray, generates config with a random UUID, creates a systemd service, and configures the firewall. x-ui mode creates inbounds and routing rules through the panel API instead.

### 2. Get the Connection String

Expand Down Expand Up @@ -127,19 +140,24 @@ Or via TUI: run `nhclient` and select **Run**.

### 6. Use the Proxy

Configure apps to use the SOCKS5 proxy at `<server-ip>:1080`.
Configure apps to use the SOCKS5 proxy at `<server-ip>:<socks-port>`. Check `sudo nhserver status` for the assigned port.

## CLI Reference

### Server Commands

```bash
sudo nhserver # Launch interactive TUI
sudo nhserver install # Download xray, create service, configure firewall
sudo nhserver install # Install (auto-detects x-ui)
sudo nhserver install --standalone # Force standalone mode
sudo nhserver install --xui-user admin --xui-pass secret # x-ui mode with credentials
sudo nhserver install --socks-port 8080 --tunnel-port 3000 # Custom ports
sudo nhserver configure # Update ports interactively
sudo nhserver configure --socks-port 8080 --tunnel-port 3000
sudo nhserver configure --xui-user admin --xui-pass secret # Required in x-ui mode
sudo nhserver status # Show status and connection string
sudo nhserver uninstall --force # Remove everything
sudo nhserver uninstall --keep-xui # Remove nethopper config but keep x-ui inbounds
```

### Client Commands
Expand All @@ -161,22 +179,29 @@ The `nh://` connection string encodes server details for easy sharing:
{
"v": 1,
"s": "192.168.1.100",
"p": 2083,
"sp": 1080,
"p": 54321,
"sp": 12345,
"u": "uuid-here"
}
```

## File Locations

### Server (Linux, root)
### Server — Standalone (Linux, root)
| File | Path |
|------|------|
| Xray binary | `/usr/local/bin/xray` |
| Server config | `/etc/nethopper/server.json` |
| Xray config | `/etc/nethopper/xray.json` |
| Systemd service | `/etc/systemd/system/nethopper.service` |

### Server — x-ui mode (Linux, root)
| File | Path |
|------|------|
| Server config | `/etc/nethopper/server.json` |

Xray binary and service are managed by x-ui. Inbounds and routing rules live in x-ui's database/config.

### Client (user-level)
| Platform | Binary | Config |
|----------|--------|--------|
Expand Down
123 changes: 106 additions & 17 deletions internal/actions/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,73 @@ import (
"strconv"
)

var portValidator = func(value string) error {
n, err := strconv.Atoi(value)
if err != nil || n < 1 || n > 65535 {
return fmt.Errorf("invalid port number")
}
return nil
}

// RegisterServerActions registers all server-side actions.
func RegisterServerActions() {
Register(&Action{
ID: ActionServerInstall,
Use: "install",
Short: "Install xray and configure server",
RequiresRoot: true,
Inputs: []InputField{
{
Name: "install-mode",
Label: "Installation mode",
Type: InputTypeSelect,
InteractiveOnly: true,
ShowIf: func(ctx *Context) bool {
return ctx.GetBool("xui-detected")
},
Options: []SelectOption{
{Label: "Configure via x-ui panel", Value: "xui"},
{Label: "Install standalone", Value: "standalone"},
},
},
{
Name: "standalone",
Label: "Install standalone (skip x-ui integration)",
Type: InputTypeBool,
},
{
Name: "xui-user",
Label: "x-ui panel username",
Type: InputTypeText,
Required: true,
ShowIf: func(ctx *Context) bool {
return ctx.GetString("install-mode") == "xui"
},
},
{
Name: "xui-pass",
Label: "x-ui panel password",
Type: InputTypePassword,
Required: true,
ShowIf: func(ctx *Context) bool {
return ctx.GetString("install-mode") == "xui"
},
},
{
Name: "socks-port",
Label: "SOCKS5 port",
Description: "Port for SOCKS5 inbound",
Type: InputTypeNumber,
Validate: portValidator,
},
{
Name: "tunnel-port",
Label: "Tunnel port",
Description: "Port for VLESS tunnel inbound",
Type: InputTypeNumber,
Validate: portValidator,
},
},
})

Register(&Action{
Expand All @@ -20,33 +80,37 @@ func RegisterServerActions() {
Short: "Configure server settings",
RequiresRoot: true,
Inputs: []InputField{
{
Name: "xui-user",
Label: "x-ui panel username",
Type: InputTypeText,
Required: true,
ShowIf: func(ctx *Context) bool {
return ctx.GetBool("xui-mode")
},
},
{
Name: "xui-pass",
Label: "x-ui panel password",
Type: InputTypePassword,
Required: true,
ShowIf: func(ctx *Context) bool {
return ctx.GetBool("xui-mode")
},
},
{
Name: "socks-port",
Label: "SOCKS5 port",
Description: "Port for SOCKS5 inbound",
Type: InputTypeNumber,
Default: "1080",
Validate: func(value string) error {
n, err := strconv.Atoi(value)
if err != nil || n < 1 || n > 65535 {
return fmt.Errorf("invalid port number")
}
return nil
},
Validate: portValidator,
},
{
Name: "tunnel-port",
Label: "Tunnel port",
Description: "Port for VLESS tunnel inbound",
Type: InputTypeNumber,
Default: "2083",
Validate: func(value string) error {
n, err := strconv.Atoi(value)
if err != nil || n < 1 || n > 65535 {
return fmt.Errorf("invalid port number")
}
return nil
},
Validate: portValidator,
},
},
})
Expand All @@ -62,8 +126,33 @@ func RegisterServerActions() {
Use: "uninstall",
Short: "Remove server installation",
RequiresRoot: true,
Inputs: []InputField{
{
Name: "xui-user",
Label: "x-ui panel username",
Type: InputTypeText,
Required: true,
ShowIf: func(ctx *Context) bool {
return ctx.GetBool("xui-mode")
},
},
{
Name: "xui-pass",
Label: "x-ui panel password",
Type: InputTypePassword,
Required: true,
ShowIf: func(ctx *Context) bool {
return ctx.GetBool("xui-mode")
},
},
{
Name: "keep-xui",
Label: "Keep x-ui configuration (only remove nethopper config)",
Type: InputTypeBool,
},
},
Confirm: &ConfirmConfig{
Message: "Remove xray, config, and systemd service?",
Message: "Remove nethopper server configuration?",
DefaultNo: true,
ForceFlag: "force",
},
Expand Down
12 changes: 9 additions & 3 deletions internal/config/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,15 @@ import (

// ServerConfig holds server-side configuration.
type ServerConfig struct {
SocksPort int `json:"socks_port"`
TunnelPort int `json:"tunnel_port"`
UUID string `json:"uuid"`
SocksPort int `json:"socks_port"`
TunnelPort int `json:"tunnel_port"`
UUID string `json:"uuid"`
XUIMode bool `json:"xui_mode,omitempty"`
XUISocksInboundID int `json:"xui_socks_inbound_id,omitempty"`
XUITunnelInboundID int `json:"xui_tunnel_inbound_id,omitempty"`
XUISocksTag string `json:"xui_socks_tag,omitempty"`
XUITunnelTag string `json:"xui_tunnel_tag,omitempty"`
XUIPortalTag string `json:"xui_portal_tag,omitempty"`
}

// ClientConfig holds client-side configuration.
Expand Down
21 changes: 21 additions & 0 deletions internal/handlers/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,27 @@ func copyFile(src, dst string) error {
return out.Close()
}

// randomAvailablePort finds a random available TCP port.
func randomAvailablePort() (int, error) {
l, err := net.Listen("tcp", ":0")
if err != nil {
return 0, err
}
port := l.Addr().(*net.TCPAddr).Port
l.Close()
return port, nil
}

// isPortAvailable checks if a specific TCP port is available for binding.
func isPortAvailable(port int) bool {
l, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
if err != nil {
return false
}
l.Close()
return true
}

// writeFile writes data to a file, creating parent directories as needed.
func writeFile(path string, data []byte) error {
if err := os.MkdirAll(filepath.Dir(path), 0750); err != nil {
Expand Down
Loading
Loading