netcloak 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +107 -0
- data/bin/netcloak +12 -0
- data/lib/netcloak/version.rb +3 -0
- data/lib/netcloak.rb +488 -0
- metadata +116 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 450227d25f5ae1b6f518be99fcb4076d40a63a0cf28e32e55f5aa6d9c70c43b1
|
4
|
+
data.tar.gz: 34060bcf9d6d8228ea759d9613e42537bd3bfd52cb84c5a6614b22d440f65756
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f80a4da6e2a1ab18362427a3af1f780cf02975282fffd5878d1ab707a5c9263d95b1d4e273af772a0da9eb27e0f2ab1af192cad6b943421b8cc16b4ca280303f
|
7
|
+
data.tar.gz: 535f7fa7835badda8265a6ca6af1abd8b6e58e97881a55241ff5842433af2067e1c93fd1b1278a71e817af3b540ff7606f0e41d7044ce9b2acbbe07bcfac2728
|
data/README.md
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
# NetCloak
|
2
|
+
|
3
|
+
NetCloak is a terminal-based user interface (TUI) wrapper for OpenVPN, providing a seamless and intuitive way to manage and monitor your VPN connections. Designed for efficiency, NetCloak enables users to select, connect, and monitor OpenVPN tunnels in real time with an interactive interface inspired by btop++.
|
4
|
+
|
5
|
+
## Features
|
6
|
+
|
7
|
+
- **TUI-Based VPN Management**: Easily select and start OpenVPN configurations from a simple terminal interface.
|
8
|
+
- **Real-Time Monitoring**: Track VPN connection status, latency, and tunnel uptime dynamically.
|
9
|
+
- **Interactive Controls**:
|
10
|
+
- **[R]** Reconnect
|
11
|
+
- **[D]** Disconnect
|
12
|
+
- **[Q]** Quit
|
13
|
+
- **Latency Visualization**: Displays latency trends in a bar graph format for quick insights.
|
14
|
+
- **Minimal Dependencies**: Runs using Curses and OpenVPN, keeping the setup lightweight.
|
15
|
+
- **Future Proxy Support**: Proxy chains functionality will be added in future updates.
|
16
|
+
|
17
|
+
## Installation
|
18
|
+
|
19
|
+
### Prerequisites
|
20
|
+
|
21
|
+
Ensure you have the following installed on your system:
|
22
|
+
|
23
|
+
- **OpenVPN**
|
24
|
+
- **Ruby** (latest version recommended)
|
25
|
+
- **Curses** gem for Ruby
|
26
|
+
|
27
|
+
### Install OpenVPN
|
28
|
+
|
29
|
+
#### Debian-based (Ubuntu, Debian, etc.)
|
30
|
+
```sh
|
31
|
+
sudo apt install openvpn -y
|
32
|
+
```
|
33
|
+
|
34
|
+
#### Fedora
|
35
|
+
```sh
|
36
|
+
sudo dnf install openvpn -y
|
37
|
+
```
|
38
|
+
|
39
|
+
#### CentOS
|
40
|
+
```sh
|
41
|
+
sudo yum install openvpn -y
|
42
|
+
```
|
43
|
+
|
44
|
+
#### Arch Linux
|
45
|
+
```sh
|
46
|
+
sudo pacman -S openvpn --noconfirm
|
47
|
+
```
|
48
|
+
|
49
|
+
#### openSUSE
|
50
|
+
```sh
|
51
|
+
sudo zypper install openvpn
|
52
|
+
```
|
53
|
+
|
54
|
+
### Setup
|
55
|
+
|
56
|
+
```sh
|
57
|
+
# Clone the repository
|
58
|
+
git clone https://github.com/cilliapwndev/NetCloak.git
|
59
|
+
cd netcloak
|
60
|
+
|
61
|
+
# Install dependencies
|
62
|
+
gem install curses
|
63
|
+
```
|
64
|
+
|
65
|
+
## Usage
|
66
|
+
|
67
|
+
1. **Navigate to your OpenVPN configuration directory**
|
68
|
+
```sh
|
69
|
+
cd /path/to/ovpn/configs
|
70
|
+
```
|
71
|
+
2. **Run NetCloak**
|
72
|
+
```sh
|
73
|
+
ruby netcloak.rb
|
74
|
+
```
|
75
|
+
3. **Select an OpenVPN configuration file**
|
76
|
+
4. **Monitor VPN performance**
|
77
|
+
5. **Use interactive keys to manage the connection**
|
78
|
+
|
79
|
+
## Demo
|
80
|
+
|
81
|
+
<img src="https://i.imgur.com/UvHNBof.gif"/>
|
82
|
+
|
83
|
+
## Roadmap
|
84
|
+
|
85
|
+
- Add support for WireGuard integration
|
86
|
+
- Improve UI with additional statistics
|
87
|
+
- Implement configuration profiles for quick VPN switching
|
88
|
+
- Enhance security features with automatic kill-switch
|
89
|
+
- In the future, NetCloak will be available as a Ruby gem for easier installation and updates.
|
90
|
+
- Future updates will include proxy chains support.
|
91
|
+
|
92
|
+
## Contributing
|
93
|
+
|
94
|
+
We welcome contributions! Feel free to open issues or submit pull requests to improve NetCloak.
|
95
|
+
|
96
|
+
## License
|
97
|
+
|
98
|
+
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
99
|
+
|
100
|
+
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
101
|
+
|
102
|
+
You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
|
103
|
+
|
104
|
+
---
|
105
|
+
|
106
|
+
**Stay anonymous, stay secure with NetCloak.**
|
107
|
+
|
data/bin/netcloak
ADDED
data/lib/netcloak.rb
ADDED
@@ -0,0 +1,488 @@
|
|
1
|
+
require 'curses'
|
2
|
+
require 'open3'
|
3
|
+
|
4
|
+
require_relative 'netcloak/version'
|
5
|
+
|
6
|
+
module NetCloak
|
7
|
+
class Error < StandardError; end
|
8
|
+
|
9
|
+
class NetCloak
|
10
|
+
COLORS = {
|
11
|
+
primary: 1,
|
12
|
+
success: 2,
|
13
|
+
danger: 3,
|
14
|
+
warning: 4,
|
15
|
+
accent: 5
|
16
|
+
}
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
@vpn_pid = nil
|
20
|
+
@selected_ovpn = nil
|
21
|
+
@latencies = []
|
22
|
+
@start_time = Time.now
|
23
|
+
@running = true
|
24
|
+
init_ui
|
25
|
+
end
|
26
|
+
|
27
|
+
def init_ui
|
28
|
+
Curses.init_screen
|
29
|
+
Curses.curs_set(0) # Hide cursor
|
30
|
+
Curses.start_color
|
31
|
+
Curses.noecho
|
32
|
+
Curses.stdscr.keypad(true)
|
33
|
+
Curses.stdscr.nodelay = true
|
34
|
+
|
35
|
+
# Initialize color pairs
|
36
|
+
Curses.init_pair(COLORS[:primary], Curses::COLOR_CYAN, Curses::COLOR_BLACK)
|
37
|
+
Curses.init_pair(COLORS[:success], Curses::COLOR_GREEN, Curses::COLOR_BLACK)
|
38
|
+
Curses.init_pair(COLORS[:danger], Curses::COLOR_RED, Curses::COLOR_BLACK)
|
39
|
+
Curses.init_pair(COLORS[:warning], Curses::COLOR_YELLOW, Curses::COLOR_BLACK)
|
40
|
+
Curses.init_pair(COLORS[:accent], Curses::COLOR_MAGENTA, Curses::COLOR_BLACK)
|
41
|
+
end
|
42
|
+
|
43
|
+
def show_connecting_screen
|
44
|
+
win = Curses.stdscr
|
45
|
+
win.clear
|
46
|
+
print_header("Connecting to VPN...")
|
47
|
+
|
48
|
+
win.setpos(5, (Curses.cols - 20) / 2)
|
49
|
+
win.attron(Curses.color_pair(COLORS[:primary])) do
|
50
|
+
win.addstr("Establishing connection")
|
51
|
+
end
|
52
|
+
|
53
|
+
3.times do |i|
|
54
|
+
win.setpos(7, (Curses.cols - 3) / 2)
|
55
|
+
win.addstr("#{'.' * (i + 1)} ")
|
56
|
+
win.refresh
|
57
|
+
sleep 0.5
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def list_ovpn_files
|
62
|
+
files = Dir.glob("*.ovpn")
|
63
|
+
if files.empty?
|
64
|
+
show_error("No OpenVPN (.ovpn) files found!")
|
65
|
+
exit(1)
|
66
|
+
end
|
67
|
+
files
|
68
|
+
end
|
69
|
+
|
70
|
+
def choose_ovpn_file(files)
|
71
|
+
menu_win = Curses::Window.new(Curses.lines, Curses.cols, 0, 0)
|
72
|
+
menu_win.keypad = true
|
73
|
+
|
74
|
+
selection = 0
|
75
|
+
top_line = 0
|
76
|
+
|
77
|
+
loop do
|
78
|
+
# Clear only the menu area (lines 3 to bottom)
|
79
|
+
menu_win.setpos(3, 0)
|
80
|
+
(3..Curses.lines-1).each { menu_win.addstr(" " * Curses.cols) }
|
81
|
+
|
82
|
+
print_header_to_window(menu_win, "Select VPN Configuration")
|
83
|
+
|
84
|
+
visible_start = top_line
|
85
|
+
visible_end = [top_line + Curses.lines - 8, files.size - 1].min
|
86
|
+
|
87
|
+
(visible_start..visible_end).each do |index|
|
88
|
+
menu_win.setpos(4 + index - top_line, 4)
|
89
|
+
if index == selection
|
90
|
+
menu_win.attron(Curses.color_pair(COLORS[:accent]) | Curses::A_BOLD) { menu_win.addstr("> #{files[index]}") }
|
91
|
+
else
|
92
|
+
menu_win.addstr(" #{files[index]}")
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
menu_win.setpos(Curses.lines - 3, 4)
|
97
|
+
menu_win.addstr("↑/↓: Select ENTER: Confirm Q: Quit")
|
98
|
+
|
99
|
+
menu_win.refresh
|
100
|
+
|
101
|
+
case menu_win.getch
|
102
|
+
when Curses::Key::UP
|
103
|
+
if selection > 0
|
104
|
+
selection -= 1
|
105
|
+
top_line = selection if selection < top_line
|
106
|
+
end
|
107
|
+
when Curses::Key::DOWN
|
108
|
+
if selection < files.size - 1
|
109
|
+
selection += 1
|
110
|
+
top_line = selection - (Curses.lines - 8) + 1 if selection >= top_line + (Curses.lines - 8)
|
111
|
+
end
|
112
|
+
when 10 then return files[selection] # Enter
|
113
|
+
when 'q' then @running = false; return nil
|
114
|
+
end
|
115
|
+
end
|
116
|
+
ensure
|
117
|
+
menu_win.close if menu_win
|
118
|
+
end
|
119
|
+
|
120
|
+
def print_header_to_window(win, title)
|
121
|
+
cols = Curses.cols
|
122
|
+
|
123
|
+
win.setpos(1, 0)
|
124
|
+
win.attron(Curses.color_pair(COLORS[:primary]) | Curses::A_BOLD) do
|
125
|
+
win.addstr(" " * cols)
|
126
|
+
win.setpos(1, (cols - title.length) / 2)
|
127
|
+
win.addstr(title)
|
128
|
+
end
|
129
|
+
|
130
|
+
win.setpos(2, 0)
|
131
|
+
win.attron(Curses.color_pair(COLORS[:primary])) do
|
132
|
+
win.addstr("─" * cols)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def start_vpn(ovpn_file)
|
137
|
+
show_connecting_screen
|
138
|
+
|
139
|
+
log_file = "/tmp/openvpn_#{Time.now.to_i}.log"
|
140
|
+
@vpn_pid = `sudo openvpn --config #{ovpn_file} > #{log_file} 2>&1 & echo $!`.strip.to_i
|
141
|
+
|
142
|
+
win = Curses.stdscr
|
143
|
+
connected = false
|
144
|
+
|
145
|
+
30.times do |i|
|
146
|
+
break unless @running
|
147
|
+
|
148
|
+
win.setpos(9, (Curses.cols - 20) / 2)
|
149
|
+
win.addstr("Attempt #{i+1}/30" + " " * 10)
|
150
|
+
|
151
|
+
if File.exist?(log_file)
|
152
|
+
log_content = File.read(log_file)
|
153
|
+
|
154
|
+
if log_content.include?("Initialization Sequence Completed")
|
155
|
+
connected = true
|
156
|
+
break
|
157
|
+
elsif log_content.include?("ERROR")
|
158
|
+
show_error("Connection failed! Check #{log_file}")
|
159
|
+
return false
|
160
|
+
end
|
161
|
+
|
162
|
+
last_line = log_content.lines.last.to_s.strip
|
163
|
+
win.setpos(11, (Curses.cols - [last_line.length, Curses.cols].min) / 2)
|
164
|
+
win.addstr(last_line[0..Curses.cols-1] + " " * 10)
|
165
|
+
end
|
166
|
+
|
167
|
+
win.refresh
|
168
|
+
sleep 1
|
169
|
+
end
|
170
|
+
|
171
|
+
if connected
|
172
|
+
win.setpos(13, (Curses.cols - 20) / 2)
|
173
|
+
win.attron(Curses.color_pair(COLORS[:success])) do
|
174
|
+
win.addstr("Connected successfully!")
|
175
|
+
end
|
176
|
+
win.refresh
|
177
|
+
sleep 1
|
178
|
+
true
|
179
|
+
else
|
180
|
+
show_error("Connection timeout after 30 seconds") if @running
|
181
|
+
false
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def vpn_connected?
|
186
|
+
return false unless @vpn_pid
|
187
|
+
system("ps -p #{@vpn_pid} > /dev/null") &&
|
188
|
+
(system("ip a show tun0 > /dev/null") || system("ip a show tap0 > /dev/null"))
|
189
|
+
end
|
190
|
+
|
191
|
+
def get_latency
|
192
|
+
result = `ping -c 1 8.8.8.8 2>&1`
|
193
|
+
if result =~ /rtt min\/avg\/max\/mdev = ([\d.]+)\/([\d.]+)\/([\d.]+)\/([\d.]+) ms/
|
194
|
+
[$1.to_f, $2.to_f, $3.to_f]
|
195
|
+
else
|
196
|
+
nil
|
197
|
+
end
|
198
|
+
rescue
|
199
|
+
nil
|
200
|
+
end
|
201
|
+
|
202
|
+
def draw_graph(win, data, width, height, y, x)
|
203
|
+
valid_data = data.compact.select { |v| v.is_a?(Numeric) }
|
204
|
+
return if valid_data.empty?
|
205
|
+
|
206
|
+
max_value = valid_data.max.to_f
|
207
|
+
min_value = valid_data.min.to_f
|
208
|
+
range = (max_value - min_value) > 0 ? (max_value - min_value) : 1.0
|
209
|
+
|
210
|
+
scaled_data = valid_data.last(width - 4).map do |value|
|
211
|
+
((height - 1) - (((value - min_value) / range) * (height - 1))).to_i
|
212
|
+
end
|
213
|
+
|
214
|
+
scaled_data.each_with_index do |y_pos, x_pos|
|
215
|
+
next unless y_pos.between?(0, height-1)
|
216
|
+
win.setpos(y + height - 1 - y_pos, x + x_pos)
|
217
|
+
win.attron(Curses.color_pair(COLORS[:accent])) { win.addstr("▄") }
|
218
|
+
end
|
219
|
+
|
220
|
+
win.setpos(y + height, x)
|
221
|
+
win.addstr("#{min_value.round}ms")
|
222
|
+
win.setpos(y + height, x + width - 5)
|
223
|
+
win.addstr("#{max_value.round}ms")
|
224
|
+
end
|
225
|
+
|
226
|
+
def print_header(title)
|
227
|
+
win = Curses.stdscr
|
228
|
+
cols = Curses.cols
|
229
|
+
|
230
|
+
win.setpos(1, 0)
|
231
|
+
win.attron(Curses.color_pair(COLORS[:primary]) | Curses::A_BOLD) do
|
232
|
+
win.addstr(" " * cols)
|
233
|
+
win.setpos(1, (cols - title.length) / 2)
|
234
|
+
win.addstr(title)
|
235
|
+
end
|
236
|
+
|
237
|
+
win.setpos(2, 0)
|
238
|
+
win.attron(Curses.color_pair(COLORS[:primary])) do
|
239
|
+
win.addstr("─" * cols)
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
def show_error(message)
|
244
|
+
win = Curses.stdscr
|
245
|
+
win.setpos(Curses.lines - 2, (Curses.cols - message.length) / 2)
|
246
|
+
win.attron(Curses.color_pair(COLORS[:danger]) | Curses::A_BOLD) do
|
247
|
+
win.addstr(message)
|
248
|
+
end
|
249
|
+
win.refresh
|
250
|
+
sleep 2
|
251
|
+
end
|
252
|
+
|
253
|
+
def show_disconnect_menu
|
254
|
+
menu_win = Curses::Window.new(Curses.lines, Curses.cols, 0, 0)
|
255
|
+
menu_win.keypad = true
|
256
|
+
|
257
|
+
selection = 0
|
258
|
+
options = [
|
259
|
+
"Reconnect to current config",
|
260
|
+
"Choose another VPN config",
|
261
|
+
"Exit application"
|
262
|
+
]
|
263
|
+
|
264
|
+
loop do
|
265
|
+
# Clear only the menu area (lines 3 to bottom)
|
266
|
+
menu_win.setpos(3, 0)
|
267
|
+
(3..Curses.lines-1).each { menu_win.addstr(" " * Curses.cols) }
|
268
|
+
|
269
|
+
print_header_to_window(menu_win, "VPN Disconnected")
|
270
|
+
|
271
|
+
menu_win.setpos(5, 4)
|
272
|
+
menu_win.attron(Curses.color_pair(COLORS[:primary])) do
|
273
|
+
menu_win.addstr("What would you like to do?")
|
274
|
+
end
|
275
|
+
|
276
|
+
options.each_with_index do |option, index|
|
277
|
+
menu_win.setpos(7 + index, 4)
|
278
|
+
if index == selection
|
279
|
+
menu_win.attron(Curses.color_pair(COLORS[:accent]) | Curses::A_BOLD) do
|
280
|
+
menu_win.addstr("> #{option}")
|
281
|
+
end
|
282
|
+
else
|
283
|
+
menu_win.addstr(" #{option}")
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
menu_win.setpos(11, 4)
|
288
|
+
menu_win.addstr("↑/↓: Select ENTER: Confirm")
|
289
|
+
|
290
|
+
menu_win.refresh
|
291
|
+
|
292
|
+
case menu_win.getch
|
293
|
+
when Curses::Key::UP then selection = [selection - 1, 0].max
|
294
|
+
when Curses::Key::DOWN then selection = [selection + 1, options.size - 1].min
|
295
|
+
when 10 # Enter key
|
296
|
+
case selection
|
297
|
+
when 0 then return :reconnect_same
|
298
|
+
when 1 then return :reconnect_new
|
299
|
+
when 2 then return :exit
|
300
|
+
end
|
301
|
+
when 'q' then return :exit
|
302
|
+
end
|
303
|
+
end
|
304
|
+
ensure
|
305
|
+
menu_win.close if menu_win
|
306
|
+
end
|
307
|
+
|
308
|
+
def draw_monitor_screen
|
309
|
+
win = Curses.stdscr
|
310
|
+
|
311
|
+
# Only clear the content area (below header)
|
312
|
+
win.setpos(3, 0)
|
313
|
+
(3..Curses.lines-1).each { |i| win.addstr(" " * Curses.cols) }
|
314
|
+
|
315
|
+
print_header("NetCloak Monitor")
|
316
|
+
|
317
|
+
# Connection status
|
318
|
+
status = vpn_connected? ? "CONNECTED" : "DISCONNECTED"
|
319
|
+
color = vpn_connected? ? COLORS[:success] : COLORS[:danger]
|
320
|
+
|
321
|
+
win.setpos(4, 4)
|
322
|
+
win.attron(Curses.color_pair(COLORS[:primary])) do
|
323
|
+
win.addstr("Status:")
|
324
|
+
end
|
325
|
+
win.setpos(4, 12)
|
326
|
+
win.attron(Curses.color_pair(color) | Curses::A_BOLD) do
|
327
|
+
win.addstr(status)
|
328
|
+
end
|
329
|
+
|
330
|
+
# Connection info
|
331
|
+
elapsed_time = (Time.now - @start_time).round
|
332
|
+
latency = get_latency&.then { |_, avg, _| avg }
|
333
|
+
|
334
|
+
if latency
|
335
|
+
@latencies << latency
|
336
|
+
@latencies.shift if @latencies.size > 100
|
337
|
+
end
|
338
|
+
|
339
|
+
win.setpos(6, 4)
|
340
|
+
win.attron(Curses.color_pair(COLORS[:primary])) do
|
341
|
+
win.addstr("Uptime: #{elapsed_time}s")
|
342
|
+
end
|
343
|
+
|
344
|
+
win.setpos(7, 4)
|
345
|
+
win.addstr("Config: #{@selected_ovpn}")
|
346
|
+
|
347
|
+
# Current latency
|
348
|
+
win.setpos(9, 4)
|
349
|
+
win.attron(Curses.color_pair(COLORS[:primary])) do
|
350
|
+
win.addstr("Current Latency:")
|
351
|
+
end
|
352
|
+
|
353
|
+
if latency
|
354
|
+
status_color = if latency < 100
|
355
|
+
COLORS[:success]
|
356
|
+
elsif latency < 300
|
357
|
+
COLORS[:warning]
|
358
|
+
else
|
359
|
+
COLORS[:danger]
|
360
|
+
end
|
361
|
+
|
362
|
+
win.setpos(9, 22)
|
363
|
+
win.attron(Curses.color_pair(status_color) | Curses::A_BOLD) do
|
364
|
+
win.addstr("#{latency.round(2)} ms")
|
365
|
+
end
|
366
|
+
else
|
367
|
+
win.setpos(9, 22)
|
368
|
+
win.attron(Curses.color_pair(COLORS[:danger])) do
|
369
|
+
win.addstr("N/A")
|
370
|
+
end
|
371
|
+
end
|
372
|
+
|
373
|
+
# Stats
|
374
|
+
if !@latencies.empty?
|
375
|
+
avg = (@latencies.sum / @latencies.size).round(2)
|
376
|
+
min = @latencies.min.round(2)
|
377
|
+
max = @latencies.max.round(2)
|
378
|
+
|
379
|
+
win.setpos(11, 4)
|
380
|
+
win.attron(Curses.color_pair(COLORS[:primary])) do
|
381
|
+
win.addstr("Statistics (last #{@latencies.size} samples):")
|
382
|
+
end
|
383
|
+
|
384
|
+
win.setpos(12, 6)
|
385
|
+
win.addstr("Avg: #{avg} ms")
|
386
|
+
|
387
|
+
win.setpos(13, 6)
|
388
|
+
win.addstr("Min: #{min} ms")
|
389
|
+
|
390
|
+
win.setpos(14, 6)
|
391
|
+
win.addstr("Max: #{max} ms")
|
392
|
+
end
|
393
|
+
|
394
|
+
# Graph
|
395
|
+
if !@latencies.empty?
|
396
|
+
win.setpos(16, 4)
|
397
|
+
win.attron(Curses.color_pair(COLORS[:primary])) do
|
398
|
+
win.addstr("Latency Trend:")
|
399
|
+
end
|
400
|
+
|
401
|
+
graph_width = [Curses.cols - 10, 60].min
|
402
|
+
draw_graph(win, @latencies, graph_width, 8, 17, 6)
|
403
|
+
end
|
404
|
+
|
405
|
+
# Controls
|
406
|
+
win.setpos(Curses.lines - 4, 4)
|
407
|
+
win.attron(Curses.color_pair(COLORS[:primary])) do
|
408
|
+
win.addstr("Controls:")
|
409
|
+
end
|
410
|
+
|
411
|
+
win.setpos(Curses.lines - 3, 6)
|
412
|
+
win.addstr("[R] Reconnect [D] Disconnect [Q] Quit")
|
413
|
+
|
414
|
+
win.refresh
|
415
|
+
end
|
416
|
+
|
417
|
+
def monitor_loop
|
418
|
+
while @running
|
419
|
+
draw_monitor_screen
|
420
|
+
|
421
|
+
case Curses.stdscr.getch
|
422
|
+
when 'q', 'Q'
|
423
|
+
@running = false
|
424
|
+
when 'd', 'D'
|
425
|
+
stop_vpn
|
426
|
+
action = show_disconnect_menu
|
427
|
+
case action
|
428
|
+
when :reconnect_same
|
429
|
+
if start_vpn(@selected_ovpn)
|
430
|
+
@start_time = Time.now
|
431
|
+
@latencies.clear
|
432
|
+
else
|
433
|
+
@running = false
|
434
|
+
end
|
435
|
+
when :reconnect_new
|
436
|
+
ovpn_files = list_ovpn_files
|
437
|
+
@selected_ovpn = choose_ovpn_file(ovpn_files)
|
438
|
+
if @selected_ovpn && start_vpn(@selected_ovpn)
|
439
|
+
@start_time = Time.now
|
440
|
+
@latencies.clear
|
441
|
+
else
|
442
|
+
@running = false
|
443
|
+
end
|
444
|
+
when :exit
|
445
|
+
@running = false
|
446
|
+
end
|
447
|
+
when 'r', 'R'
|
448
|
+
stop_vpn
|
449
|
+
if start_vpn(@selected_ovpn)
|
450
|
+
@start_time = Time.now
|
451
|
+
@latencies.clear
|
452
|
+
end
|
453
|
+
end
|
454
|
+
|
455
|
+
sleep 0.1
|
456
|
+
end
|
457
|
+
end
|
458
|
+
|
459
|
+
def stop_vpn
|
460
|
+
return unless @vpn_pid
|
461
|
+
Process.kill('TERM', @vpn_pid) rescue nil
|
462
|
+
@vpn_pid = nil
|
463
|
+
end
|
464
|
+
|
465
|
+
def run
|
466
|
+
while @running
|
467
|
+
ovpn_files = list_ovpn_files
|
468
|
+
@selected_ovpn = choose_ovpn_file(ovpn_files)
|
469
|
+
|
470
|
+
if @selected_ovpn && start_vpn(@selected_ovpn)
|
471
|
+
@start_time = Time.now
|
472
|
+
@latencies.clear
|
473
|
+
monitor_loop
|
474
|
+
end
|
475
|
+
end
|
476
|
+
rescue => e
|
477
|
+
show_error("Error: #{e.message}")
|
478
|
+
ensure
|
479
|
+
stop_vpn
|
480
|
+
Curses.close_screen
|
481
|
+
puts "VPN disconnected" if @running
|
482
|
+
@running = false
|
483
|
+
end
|
484
|
+
end
|
485
|
+
|
486
|
+
end
|
487
|
+
|
488
|
+
NetCloak::NetCloak.new.run
|
metadata
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: netcloak
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Mark Angelo P. Santonil
|
8
|
+
bindir: bin
|
9
|
+
cert_chain: []
|
10
|
+
date: 2025-04-15 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: curses
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - "~>"
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '1.4'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - "~>"
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '1.4'
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: open3
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - "~>"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0.1'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0.1'
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: bundler
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '2.0'
|
47
|
+
type: :development
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '2.0'
|
54
|
+
- !ruby/object:Gem::Dependency
|
55
|
+
name: rake
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - "~>"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '13.0'
|
61
|
+
type: :development
|
62
|
+
prerelease: false
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "~>"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '13.0'
|
68
|
+
- !ruby/object:Gem::Dependency
|
69
|
+
name: rspec
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - "~>"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '3.0'
|
75
|
+
type: :development
|
76
|
+
prerelease: false
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - "~>"
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '3.0'
|
82
|
+
description: NetCloak provides a curses-based interface to manage VPN configurations,
|
83
|
+
monitor latency, and control connections.
|
84
|
+
email:
|
85
|
+
- cillia2203@gmail.com
|
86
|
+
executables:
|
87
|
+
- netcloak
|
88
|
+
extensions: []
|
89
|
+
extra_rdoc_files: []
|
90
|
+
files:
|
91
|
+
- README.md
|
92
|
+
- bin/netcloak
|
93
|
+
- lib/netcloak.rb
|
94
|
+
- lib/netcloak/version.rb
|
95
|
+
homepage: https://github.com/cilliapwndev/NetCloak
|
96
|
+
licenses:
|
97
|
+
- GNU v.3
|
98
|
+
metadata: {}
|
99
|
+
rdoc_options: []
|
100
|
+
require_paths:
|
101
|
+
- lib
|
102
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
103
|
+
requirements:
|
104
|
+
- - ">="
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
version: '0'
|
107
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
108
|
+
requirements:
|
109
|
+
- - ">="
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
version: '0'
|
112
|
+
requirements: []
|
113
|
+
rubygems_version: 3.6.6
|
114
|
+
specification_version: 4
|
115
|
+
summary: A CLI tool to monitor and manage OpenVPN connections.
|
116
|
+
test_files: []
|