omniwire 2.5.1 → 2.6.1
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.
- package/README.md +80 -109
- package/assets/banner-dark.svg +82 -0
- package/assets/banner-light.svg +75 -0
- package/dist/mcp/server.js +345 -4
- package/dist/mcp/server.js.map +1 -1
- package/package.json +1 -1
package/dist/mcp/server.js
CHANGED
|
@@ -61,6 +61,61 @@ function multiResultJson(results) {
|
|
|
61
61
|
...(r.stderr ? { stderr: r.stderr.slice(0, 500) } : {}),
|
|
62
62
|
}))));
|
|
63
63
|
}
|
|
64
|
+
// -- VPN namespace wrapper -- routes ONLY the command through VPN, mesh stays intact
|
|
65
|
+
// Uses Linux network namespaces: creates isolated netns, moves VPN tunnel into it,
|
|
66
|
+
// runs command inside namespace. Main namespace (WireGuard mesh, SSH) is untouched.
|
|
67
|
+
function buildVpnWrappedCmd(vpnSpec, innerCmd) {
|
|
68
|
+
const ns = `ow-vpn-${Date.now().toString(36)}`;
|
|
69
|
+
const escaped = innerCmd.replace(/'/g, "'\\''");
|
|
70
|
+
// Parse spec: "mullvad", "mullvad:se", "openvpn:/path/to.conf", "wg:wg-vpn"
|
|
71
|
+
const [provider, param] = vpnSpec.includes(':') ? [vpnSpec.split(':')[0], vpnSpec.split(':').slice(1).join(':')] : [vpnSpec, ''];
|
|
72
|
+
if (provider === 'mullvad') {
|
|
73
|
+
// Mullvad supports split tunneling natively via `mullvad-exclude`
|
|
74
|
+
// OR we can use its SOCKS5 proxy (10.64.0.1:1080) when connected
|
|
75
|
+
// Best approach: use mullvad split-tunnel + namespace for full isolation
|
|
76
|
+
const relayCmd = param ? `mullvad relay set location ${param} 2>/dev/null;` : '';
|
|
77
|
+
// Check if mullvad is already connected; if not, connect (split-tunnel safe)
|
|
78
|
+
return `${relayCmd} mullvad status | grep -q Connected || mullvad connect 2>/dev/null; sleep 1; ` +
|
|
79
|
+
// Create netns, veth pair, route traffic through mullvad's tun
|
|
80
|
+
`ip netns add ${ns} 2>/dev/null; ` +
|
|
81
|
+
`ip link add veth-${ns} type veth peer name veth-${ns}-ns; ` +
|
|
82
|
+
`ip link set veth-${ns}-ns netns ${ns}; ` +
|
|
83
|
+
`ip addr add 172.30.${Math.floor(Math.random() * 254) + 1}.1/30 dev veth-${ns}; ` +
|
|
84
|
+
`ip link set veth-${ns} up; ` +
|
|
85
|
+
`ip netns exec ${ns} ip addr add 172.30.${Math.floor(Math.random() * 254) + 1}.2/30 dev veth-${ns}-ns; ` +
|
|
86
|
+
`ip netns exec ${ns} ip link set veth-${ns}-ns up; ` +
|
|
87
|
+
`ip netns exec ${ns} ip link set lo up; ` +
|
|
88
|
+
// Use mullvad SOCKS proxy for the namespace (simpler, no route table manipulation)
|
|
89
|
+
`ip netns exec ${ns} bash -c 'export ALL_PROXY=socks5://10.64.0.1:1080; ${escaped}'; ` +
|
|
90
|
+
`_rc=$?; ip netns del ${ns} 2>/dev/null; ip link del veth-${ns} 2>/dev/null; exit $_rc`;
|
|
91
|
+
}
|
|
92
|
+
if (provider === 'openvpn') {
|
|
93
|
+
const configPath = param || '/etc/openvpn/client.conf';
|
|
94
|
+
// Run OpenVPN inside a network namespace — only the command sees the tunnel
|
|
95
|
+
return `ip netns add ${ns} 2>/dev/null; ` +
|
|
96
|
+
`ip netns exec ${ns} ip link set lo up; ` +
|
|
97
|
+
`ip netns exec ${ns} openvpn --config "${configPath}" --daemon --log /tmp/${ns}.log --writepid /tmp/${ns}.pid; ` +
|
|
98
|
+
`sleep 4; ` + // wait for tunnel
|
|
99
|
+
`ip netns exec ${ns} bash -c '${escaped}'; ` +
|
|
100
|
+
`_rc=$?; kill $(cat /tmp/${ns}.pid 2>/dev/null) 2>/dev/null; ip netns del ${ns} 2>/dev/null; rm -f /tmp/${ns}.log /tmp/${ns}.pid; exit $_rc`;
|
|
101
|
+
}
|
|
102
|
+
if (provider === 'wg' || provider === 'wireguard') {
|
|
103
|
+
const iface = param || 'wg-vpn';
|
|
104
|
+
// Move WireGuard VPN interface into namespace — mesh wg0 stays in main ns
|
|
105
|
+
return `ip netns add ${ns} 2>/dev/null; ` +
|
|
106
|
+
`ip link add ${iface}-${ns} type wireguard; ` +
|
|
107
|
+
`wg setconf ${iface}-${ns} /etc/wireguard/${iface}.conf 2>/dev/null; ` +
|
|
108
|
+
`ip link set ${iface}-${ns} netns ${ns}; ` +
|
|
109
|
+
`ip netns exec ${ns} ip link set lo up; ` +
|
|
110
|
+
`ip netns exec ${ns} ip addr add 10.66.0.2/32 dev ${iface}-${ns}; ` +
|
|
111
|
+
`ip netns exec ${ns} ip link set ${iface}-${ns} up; ` +
|
|
112
|
+
`ip netns exec ${ns} ip route add default dev ${iface}-${ns}; ` +
|
|
113
|
+
`ip netns exec ${ns} bash -c '${escaped}'; ` +
|
|
114
|
+
`_rc=$?; ip netns del ${ns} 2>/dev/null; exit $_rc`;
|
|
115
|
+
}
|
|
116
|
+
// Fallback: just run the command (no VPN wrapping)
|
|
117
|
+
return innerCmd;
|
|
118
|
+
}
|
|
64
119
|
// -- Agentic state -- shared across tool calls in the same MCP session --------
|
|
65
120
|
const resultStore = new Map(); // key -> value store for chaining
|
|
66
121
|
const bgTasks = new Map();
|
|
@@ -76,7 +131,7 @@ function dispatchBg(node, label, fn) {
|
|
|
76
131
|
export function createOmniWireServer(manager, transfer) {
|
|
77
132
|
const server = new McpServer({
|
|
78
133
|
name: 'omniwire',
|
|
79
|
-
version: '2.
|
|
134
|
+
version: '2.6.1',
|
|
80
135
|
});
|
|
81
136
|
// -- Auto-inject `background` param into every tool -------------------------
|
|
82
137
|
const origTool = server.tool.bind(server);
|
|
@@ -132,7 +187,7 @@ export function createOmniWireServer(manager, transfer) {
|
|
|
132
187
|
return fail('invalid action');
|
|
133
188
|
});
|
|
134
189
|
// --- Tool 1: omniwire_exec ---
|
|
135
|
-
server.tool('omniwire_exec', 'Execute a command on a mesh node.
|
|
190
|
+
server.tool('omniwire_exec', 'Execute a command on a mesh node. Set background=true for async. Set via_vpn to route through VPN (Mullvad/OpenVPN/WireGuard) for anonymous scanning. Supports retry, assert, JSON, store_as, {{key}}.', {
|
|
136
191
|
node: z.string().optional().describe('Target node id (windows, contabo, hostinger, thinkpad). Auto-selects if omitted.'),
|
|
137
192
|
command: z.string().optional().describe('Shell command to run. Use {{key}} to interpolate stored results from previous calls.'),
|
|
138
193
|
timeout: z.number().optional().describe('Timeout in seconds (default 30)'),
|
|
@@ -142,7 +197,8 @@ export function createOmniWireServer(manager, transfer) {
|
|
|
142
197
|
retry: z.number().optional().describe('Retry N times on failure (with 1s delay between). Default 0.'),
|
|
143
198
|
assert: z.string().optional().describe('Grep pattern to assert in stdout. If not found, returns error. Use for validation in agentic chains.'),
|
|
144
199
|
store_as: z.string().optional().describe('Store trimmed stdout under this key. Retrieve in subsequent calls via {{key}} in command.'),
|
|
145
|
-
|
|
200
|
+
via_vpn: z.string().optional().describe('Route command through VPN. Values: "mullvad" (auto), "mullvad:se" (country), "openvpn:/path/to.conf", "wg:wg-vpn". Connects VPN before exec, verifies IP changed.'),
|
|
201
|
+
}, async ({ node, command, timeout, script, label, format, retry, assert: assertPattern, store_as, via_vpn }) => {
|
|
146
202
|
if (!command && !script) {
|
|
147
203
|
return fail('either command or script is required');
|
|
148
204
|
}
|
|
@@ -164,6 +220,11 @@ export function createOmniWireServer(manager, transfer) {
|
|
|
164
220
|
? `timeout ${timeoutSec} bash -c '${resolvedCmd.replace(/'/g, "'\\''")}'`
|
|
165
221
|
: resolvedCmd;
|
|
166
222
|
}
|
|
223
|
+
// VPN routing: wrap command in network namespace so only THIS command goes through VPN.
|
|
224
|
+
// The mesh (WireGuard, SSH) stays on the real interface — zero disruption.
|
|
225
|
+
if (via_vpn) {
|
|
226
|
+
effectiveCmd = buildVpnWrappedCmd(via_vpn, effectiveCmd);
|
|
227
|
+
}
|
|
167
228
|
let result = await manager.exec(nodeId, effectiveCmd);
|
|
168
229
|
for (let attempt = 0; attempt < maxRetries && result.code !== 0; attempt++) {
|
|
169
230
|
await new Promise((r) => setTimeout(r, 1000));
|
|
@@ -739,7 +800,287 @@ tags: ${meshNode.tags.join(', ')}`;
|
|
|
739
800
|
const result = await manager.exec(node, cmd);
|
|
740
801
|
return ok(node, result.durationMs, result.code === 0 ? result.stdout : result.stderr, `net ${action}`);
|
|
741
802
|
});
|
|
742
|
-
// --- Tool 30:
|
|
803
|
+
// --- Tool 30: omniwire_vpn ---
|
|
804
|
+
server.tool('omniwire_vpn', 'Manage VPN on mesh nodes (Mullvad/OpenVPN/WireGuard/Tailscale). Mesh-safe: split-tunnel or namespace isolation. Mullvad advanced: multi-hop, DAITA, quantum-resistant tunnels, DNS-over-HTTPS, obfuscation, kill-switch.', {
|
|
805
|
+
action: z.enum([
|
|
806
|
+
'connect', 'disconnect', 'status', 'list', 'ip', 'rotate', 'full-on', 'full-off',
|
|
807
|
+
'multihop', 'daita', 'quantum', 'obfuscation', 'dns', 'killswitch', 'split-tunnel', 'relay-set', 'account', 'settings'
|
|
808
|
+
]).describe('Core: connect/disconnect/status/list/ip/rotate/full-on/full-off. Mullvad: multihop (on/off/entry:exit), daita (on/off), quantum (on/off), obfuscation (on/off/udp2tcp/shadowsocks), dns (custom/default), killswitch (on/off), split-tunnel (add/remove/list pid), relay-set (set relay constraints), account (info), settings (show all)'),
|
|
809
|
+
node: z.string().optional().describe('Node to manage VPN on (default: contabo)'),
|
|
810
|
+
provider: z.enum(['mullvad', 'openvpn', 'wireguard', 'tailscale']).optional().describe('VPN provider (default: auto-detect)'),
|
|
811
|
+
server: z.string().optional().describe('Server/relay. Mullvad: country (se), city (se-got), relay (se-got-wg-001). OpenVPN: config path. WireGuard: interface. Tailscale: exit node.'),
|
|
812
|
+
config: z.string().optional().describe('Config file path or feature value. For multihop: "entry:exit" (e.g. "se:us"). For dns: IP address. For obfuscation: "udp2tcp" or "shadowsocks". For split-tunnel: PID or process name.'),
|
|
813
|
+
}, async ({ action, node, provider, server: vpnServer, config }) => {
|
|
814
|
+
const nodeId = node ?? 'contabo';
|
|
815
|
+
// Auto-detect provider (prefer mullvad > tailscale > openvpn > wireguard)
|
|
816
|
+
const detectCmd = 'command -v mullvad >/dev/null && echo mullvad || (command -v tailscale >/dev/null && echo tailscale || (command -v openvpn >/dev/null && echo openvpn || (command -v wg >/dev/null && echo wireguard || echo none)))';
|
|
817
|
+
const detected = provider ?? (await manager.exec(nodeId, detectCmd)).stdout.trim();
|
|
818
|
+
if (action === 'ip') {
|
|
819
|
+
const result = await manager.exec(nodeId, 'curl -s --max-time 5 https://am.i.mullvad.net/json 2>/dev/null || curl -s --max-time 5 https://ipinfo.io/json 2>/dev/null || curl -s --max-time 5 https://ifconfig.me');
|
|
820
|
+
return ok(nodeId, result.durationMs, result.stdout, 'public IP');
|
|
821
|
+
}
|
|
822
|
+
if (detected === 'mullvad') {
|
|
823
|
+
// Mullvad split-tunnel: excludes mesh interfaces (wg0, tailscale0) from VPN routing.
|
|
824
|
+
// SSH connections over WireGuard mesh are preserved — only non-mesh traffic goes through Mullvad.
|
|
825
|
+
const splitSetup = 'mullvad split-tunnel set state on 2>/dev/null; mullvad lan set allow 2>/dev/null;';
|
|
826
|
+
switch (action) {
|
|
827
|
+
case 'connect': {
|
|
828
|
+
const relay = vpnServer ? `mullvad relay set location ${vpnServer} && ` : '';
|
|
829
|
+
const result = await manager.exec(nodeId, `${splitSetup} ${relay}mullvad connect && sleep 2 && mullvad status && echo "--- mesh check ---" && ip route get 10.10.0.1 2>/dev/null | head -1`);
|
|
830
|
+
return ok(nodeId, result.durationMs, result.stdout, `mullvad connect${vpnServer ? ` ${vpnServer}` : ''}`);
|
|
831
|
+
}
|
|
832
|
+
case 'disconnect': {
|
|
833
|
+
const result = await manager.exec(nodeId, 'mullvad disconnect && mullvad status');
|
|
834
|
+
return ok(nodeId, result.durationMs, result.stdout, 'mullvad disconnect');
|
|
835
|
+
}
|
|
836
|
+
case 'status': {
|
|
837
|
+
const result = await manager.exec(nodeId, 'mullvad status && echo "---split-tunnel---" && mullvad split-tunnel get 2>/dev/null && echo "---public-ip---" && curl -s --max-time 5 https://am.i.mullvad.net/json 2>/dev/null | grep -E "ip|country|mullvad"');
|
|
838
|
+
return ok(nodeId, result.durationMs, result.stdout, 'mullvad status');
|
|
839
|
+
}
|
|
840
|
+
case 'list': {
|
|
841
|
+
const result = await manager.exec(nodeId, 'mullvad relay list 2>&1 | head -60');
|
|
842
|
+
return ok(nodeId, result.durationMs, result.stdout, 'mullvad relays');
|
|
843
|
+
}
|
|
844
|
+
case 'rotate': {
|
|
845
|
+
const result = await manager.exec(nodeId, `${splitSetup} mullvad disconnect && sleep 1 && mullvad relay set tunnel-protocol wireguard && mullvad connect && sleep 2 && mullvad status && echo "---ip---" && curl -s --max-time 5 https://am.i.mullvad.net/json | grep -E "ip|country|mullvad_exit"`);
|
|
846
|
+
return ok(nodeId, result.durationMs, result.stdout, 'mullvad rotate');
|
|
847
|
+
}
|
|
848
|
+
case 'multihop': {
|
|
849
|
+
// Multi-hop: traffic enters at one relay, exits at another. config = "entry:exit" e.g. "se:us"
|
|
850
|
+
if (!config && !vpnServer) {
|
|
851
|
+
// Toggle or show status
|
|
852
|
+
const result = await manager.exec(nodeId, 'mullvad tunnel get | grep -i multihop');
|
|
853
|
+
return ok(nodeId, result.durationMs, result.stdout, 'mullvad multihop status');
|
|
854
|
+
}
|
|
855
|
+
const toggle = config === 'off' ? 'off' : 'on';
|
|
856
|
+
let cmd = `mullvad tunnel set wireguard --multihop=${toggle}`;
|
|
857
|
+
if (toggle === 'on' && config && config.includes(':')) {
|
|
858
|
+
const [entry, exit] = config.split(':');
|
|
859
|
+
cmd += ` && mullvad relay set location ${exit} && mullvad relay set tunnel wireguard entry-location ${entry}`;
|
|
860
|
+
}
|
|
861
|
+
else if (vpnServer && toggle === 'on') {
|
|
862
|
+
cmd += ` && mullvad relay set location ${vpnServer}`;
|
|
863
|
+
}
|
|
864
|
+
cmd += ' && mullvad status';
|
|
865
|
+
const result = await manager.exec(nodeId, cmd);
|
|
866
|
+
return ok(nodeId, result.durationMs, result.stdout, `mullvad multihop ${toggle}`);
|
|
867
|
+
}
|
|
868
|
+
case 'daita': {
|
|
869
|
+
// DAITA: Defence Against AI-guided Traffic Analysis — pads packets to hide traffic patterns
|
|
870
|
+
const toggle = config === 'off' ? 'off' : 'on';
|
|
871
|
+
const result = await manager.exec(nodeId, `mullvad tunnel set wireguard --daita=${toggle} 2>&1 && mullvad tunnel get | grep -i daita && mullvad status`);
|
|
872
|
+
return ok(nodeId, result.durationMs, result.stdout, `mullvad daita ${toggle}`);
|
|
873
|
+
}
|
|
874
|
+
case 'quantum': {
|
|
875
|
+
// Quantum-resistant tunneling: post-quantum key exchange on WireGuard
|
|
876
|
+
const toggle = config === 'off' ? 'off' : 'on';
|
|
877
|
+
const result = await manager.exec(nodeId, `mullvad tunnel set wireguard --quantum-resistant=${toggle} 2>&1 && mullvad tunnel get | grep -i quantum && mullvad status`);
|
|
878
|
+
return ok(nodeId, result.durationMs, result.stdout, `mullvad quantum ${toggle}`);
|
|
879
|
+
}
|
|
880
|
+
case 'obfuscation': {
|
|
881
|
+
// Obfuscation: bypass DPI. Modes: auto, off, udp2tcp, shadowsocks
|
|
882
|
+
const mode = config ?? 'auto';
|
|
883
|
+
const result = await manager.exec(nodeId, `mullvad obfuscation set mode ${mode} 2>&1 && mullvad obfuscation get && mullvad status`);
|
|
884
|
+
return ok(nodeId, result.durationMs, result.stdout, `mullvad obfuscation ${mode}`);
|
|
885
|
+
}
|
|
886
|
+
case 'dns': {
|
|
887
|
+
// Custom DNS: set DNS server or reset to default (Mullvad DNS)
|
|
888
|
+
if (!config || config === 'default') {
|
|
889
|
+
const result = await manager.exec(nodeId, 'mullvad dns set default 2>&1 && mullvad dns get');
|
|
890
|
+
return ok(nodeId, result.durationMs, result.stdout, 'mullvad dns default');
|
|
891
|
+
}
|
|
892
|
+
// config = IP address or "content-blockers" for Mullvad's ad/tracker blocking DNS
|
|
893
|
+
const cmd = config === 'adblock'
|
|
894
|
+
? 'mullvad dns set custom --block-ads --block-trackers --block-malware 2>&1 && mullvad dns get'
|
|
895
|
+
: `mullvad dns set custom ${config} 2>&1 && mullvad dns get`;
|
|
896
|
+
const result = await manager.exec(nodeId, cmd);
|
|
897
|
+
return ok(nodeId, result.durationMs, result.stdout, `mullvad dns ${config}`);
|
|
898
|
+
}
|
|
899
|
+
case 'killswitch': {
|
|
900
|
+
// Kill switch: block all traffic if VPN disconnects
|
|
901
|
+
const toggle = config === 'off' ? 'off' : 'on';
|
|
902
|
+
const blockWhen = toggle === 'on' ? 'always' : 'only-when-connected';
|
|
903
|
+
const result = await manager.exec(nodeId, `mullvad always-require-vpn set ${blockWhen} 2>&1 && mullvad always-require-vpn get && mullvad lan set allow`);
|
|
904
|
+
return ok(nodeId, result.durationMs, result.stdout, `mullvad killswitch ${toggle}`);
|
|
905
|
+
}
|
|
906
|
+
case 'split-tunnel': {
|
|
907
|
+
// Split tunnel: exclude specific apps/PIDs from VPN
|
|
908
|
+
if (!config || config === 'list') {
|
|
909
|
+
const result = await manager.exec(nodeId, 'mullvad split-tunnel get 2>&1');
|
|
910
|
+
return ok(nodeId, result.durationMs, result.stdout, 'mullvad split-tunnel list');
|
|
911
|
+
}
|
|
912
|
+
if (config.startsWith('add:')) {
|
|
913
|
+
const pid = config.slice(4);
|
|
914
|
+
const result = await manager.exec(nodeId, `mullvad split-tunnel add ${pid} 2>&1 && mullvad split-tunnel get`);
|
|
915
|
+
return ok(nodeId, result.durationMs, result.stdout, `mullvad split-tunnel add ${pid}`);
|
|
916
|
+
}
|
|
917
|
+
if (config.startsWith('remove:') || config.startsWith('del:')) {
|
|
918
|
+
const pid = config.slice(config.indexOf(':') + 1);
|
|
919
|
+
const result = await manager.exec(nodeId, `mullvad split-tunnel delete ${pid} 2>&1 && mullvad split-tunnel get`);
|
|
920
|
+
return ok(nodeId, result.durationMs, result.stdout, `mullvad split-tunnel remove ${pid}`);
|
|
921
|
+
}
|
|
922
|
+
// Toggle state
|
|
923
|
+
const toggle = config === 'off' ? 'off' : 'on';
|
|
924
|
+
const result = await manager.exec(nodeId, `mullvad split-tunnel set state ${toggle} 2>&1 && mullvad split-tunnel get`);
|
|
925
|
+
return ok(nodeId, result.durationMs, result.stdout, `mullvad split-tunnel ${toggle}`);
|
|
926
|
+
}
|
|
927
|
+
case 'relay-set': {
|
|
928
|
+
// Set relay constraints: protocol, location, custom lists
|
|
929
|
+
if (!config && !vpnServer) {
|
|
930
|
+
const result = await manager.exec(nodeId, 'mullvad relay get 2>&1');
|
|
931
|
+
return ok(nodeId, result.durationMs, result.stdout, 'mullvad relay config');
|
|
932
|
+
}
|
|
933
|
+
const loc = vpnServer ?? '';
|
|
934
|
+
let cmd = '';
|
|
935
|
+
if (loc)
|
|
936
|
+
cmd += `mullvad relay set location ${loc}; `;
|
|
937
|
+
if (config === 'wireguard' || config === 'wg')
|
|
938
|
+
cmd += 'mullvad relay set tunnel-protocol wireguard; ';
|
|
939
|
+
if (config === 'openvpn')
|
|
940
|
+
cmd += 'mullvad relay set tunnel-protocol openvpn; ';
|
|
941
|
+
if (config === 'any')
|
|
942
|
+
cmd += 'mullvad relay set tunnel-protocol any; ';
|
|
943
|
+
cmd += 'mullvad relay get && mullvad status';
|
|
944
|
+
const result = await manager.exec(nodeId, cmd);
|
|
945
|
+
return ok(nodeId, result.durationMs, result.stdout, `mullvad relay ${loc || config || 'get'}`);
|
|
946
|
+
}
|
|
947
|
+
case 'account': {
|
|
948
|
+
const result = await manager.exec(nodeId, 'mullvad account get 2>&1');
|
|
949
|
+
return ok(nodeId, result.durationMs, result.stdout, 'mullvad account');
|
|
950
|
+
}
|
|
951
|
+
case 'settings': {
|
|
952
|
+
const result = await manager.exec(nodeId, 'mullvad status && echo "===tunnel===" && mullvad tunnel get && echo "===relay===" && mullvad relay get && echo "===dns===" && mullvad dns get && echo "===obfuscation===" && mullvad obfuscation get && echo "===split-tunnel===" && mullvad split-tunnel get 2>/dev/null && echo "===killswitch===" && mullvad always-require-vpn get 2>/dev/null');
|
|
953
|
+
return ok(nodeId, result.durationMs, result.stdout, 'mullvad all settings');
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
if (detected === 'openvpn') {
|
|
958
|
+
const configPath = config ?? vpnServer ?? '/etc/openvpn/client.conf';
|
|
959
|
+
switch (action) {
|
|
960
|
+
case 'connect': {
|
|
961
|
+
const result = await manager.exec(nodeId, `openvpn --config "${configPath}" --daemon --log /tmp/openvpn.log && sleep 3 && ip addr show tun0 2>/dev/null | grep inet && curl -s --max-time 5 https://ifconfig.me`);
|
|
962
|
+
return ok(nodeId, result.durationMs, result.stdout, 'openvpn connect');
|
|
963
|
+
}
|
|
964
|
+
case 'disconnect': {
|
|
965
|
+
const result = await manager.exec(nodeId, 'pkill openvpn 2>/dev/null; sleep 1; pgrep openvpn >/dev/null && echo "still running" || echo "disconnected"');
|
|
966
|
+
return ok(nodeId, result.durationMs, result.stdout, 'openvpn disconnect');
|
|
967
|
+
}
|
|
968
|
+
case 'status': {
|
|
969
|
+
const result = await manager.exec(nodeId, 'pgrep -a openvpn 2>/dev/null || echo "not running"; ip addr show tun0 2>/dev/null | grep inet || echo "no tunnel"; tail -5 /tmp/openvpn.log 2>/dev/null');
|
|
970
|
+
return ok(nodeId, result.durationMs, result.stdout, 'openvpn status');
|
|
971
|
+
}
|
|
972
|
+
case 'list': {
|
|
973
|
+
const result = await manager.exec(nodeId, 'ls /etc/openvpn/*.conf /etc/openvpn/*.ovpn /etc/openvpn/client/*.conf /etc/openvpn/client/*.ovpn 2>/dev/null || echo "no configs found"');
|
|
974
|
+
return ok(nodeId, result.durationMs, result.stdout, 'openvpn configs');
|
|
975
|
+
}
|
|
976
|
+
case 'rotate': {
|
|
977
|
+
const result = await manager.exec(nodeId, `pkill openvpn 2>/dev/null; sleep 1; openvpn --config "${configPath}" --daemon --log /tmp/openvpn.log && sleep 3 && curl -s --max-time 5 https://ifconfig.me`);
|
|
978
|
+
return ok(nodeId, result.durationMs, result.stdout, 'openvpn rotate');
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
if (detected === 'wireguard') {
|
|
983
|
+
const iface = vpnServer ?? 'wg-vpn';
|
|
984
|
+
switch (action) {
|
|
985
|
+
case 'connect': {
|
|
986
|
+
const result = await manager.exec(nodeId, `wg-quick up ${iface} 2>&1 && sleep 1 && wg show ${iface} | head -10 && curl -s --max-time 5 https://ifconfig.me`);
|
|
987
|
+
return ok(nodeId, result.durationMs, result.stdout, `wg up ${iface}`);
|
|
988
|
+
}
|
|
989
|
+
case 'disconnect': {
|
|
990
|
+
const result = await manager.exec(nodeId, `wg-quick down ${iface} 2>&1`);
|
|
991
|
+
return ok(nodeId, result.durationMs, result.stdout, `wg down ${iface}`);
|
|
992
|
+
}
|
|
993
|
+
case 'status': {
|
|
994
|
+
const result = await manager.exec(nodeId, 'wg show all 2>/dev/null || echo "no WireGuard interfaces"');
|
|
995
|
+
return ok(nodeId, result.durationMs, result.stdout, 'wg status');
|
|
996
|
+
}
|
|
997
|
+
case 'list': {
|
|
998
|
+
const result = await manager.exec(nodeId, 'ls /etc/wireguard/*.conf 2>/dev/null | sed "s|/etc/wireguard/||;s|\\.conf||" || echo "no configs"');
|
|
999
|
+
return ok(nodeId, result.durationMs, result.stdout, 'wg interfaces');
|
|
1000
|
+
}
|
|
1001
|
+
case 'rotate': {
|
|
1002
|
+
const result = await manager.exec(nodeId, `wg-quick down ${iface} 2>/dev/null; sleep 1; wg-quick up ${iface} 2>&1 && sleep 1 && curl -s --max-time 5 https://ifconfig.me`);
|
|
1003
|
+
return ok(nodeId, result.durationMs, result.stdout, `wg rotate ${iface}`);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
if (detected === 'tailscale') {
|
|
1008
|
+
switch (action) {
|
|
1009
|
+
case 'connect': {
|
|
1010
|
+
const exitNode = vpnServer ? `--exit-node=${vpnServer}` : '';
|
|
1011
|
+
const result = await manager.exec(nodeId, `tailscale up --accept-routes ${exitNode} 2>&1 && sleep 2 && tailscale status | head -15`);
|
|
1012
|
+
return ok(nodeId, result.durationMs, result.stdout, `tailscale connect${vpnServer ? ` via ${vpnServer}` : ''}`);
|
|
1013
|
+
}
|
|
1014
|
+
case 'disconnect': {
|
|
1015
|
+
const result = await manager.exec(nodeId, 'tailscale up --exit-node= 2>&1 && tailscale status | head -5');
|
|
1016
|
+
return ok(nodeId, result.durationMs, result.stdout, 'tailscale clear exit-node');
|
|
1017
|
+
}
|
|
1018
|
+
case 'status': {
|
|
1019
|
+
const result = await manager.exec(nodeId, 'tailscale status 2>&1 && echo "---ip---" && tailscale ip -4 2>/dev/null && echo "---exit---" && tailscale exit-node status 2>/dev/null');
|
|
1020
|
+
return ok(nodeId, result.durationMs, result.stdout, 'tailscale status');
|
|
1021
|
+
}
|
|
1022
|
+
case 'list': {
|
|
1023
|
+
const result = await manager.exec(nodeId, 'tailscale exit-node list 2>&1 | head -40');
|
|
1024
|
+
return ok(nodeId, result.durationMs, result.stdout, 'tailscale exit nodes');
|
|
1025
|
+
}
|
|
1026
|
+
case 'rotate': {
|
|
1027
|
+
const result = await manager.exec(nodeId, `tailscale up --exit-node= 2>/dev/null; sleep 1; ${vpnServer ? `tailscale up --exit-node=${vpnServer}` : 'tailscale up'} 2>&1 && sleep 2 && curl -s --max-time 5 https://ifconfig.me`);
|
|
1028
|
+
return ok(nodeId, result.durationMs, result.stdout, 'tailscale rotate');
|
|
1029
|
+
}
|
|
1030
|
+
case 'full-on': {
|
|
1031
|
+
const exitNode = vpnServer ?? '';
|
|
1032
|
+
if (!exitNode)
|
|
1033
|
+
return fail('server param required for full-on (tailscale exit node hostname)');
|
|
1034
|
+
const result = await manager.exec(nodeId, `tailscale up --exit-node=${exitNode} --exit-node-allow-lan-access 2>&1 && sleep 2 && tailscale status | head -10 && echo "---ip---" && curl -s --max-time 5 https://ifconfig.me`);
|
|
1035
|
+
return ok(nodeId, result.durationMs, result.stdout, `tailscale full-on via ${exitNode}`);
|
|
1036
|
+
}
|
|
1037
|
+
case 'full-off': {
|
|
1038
|
+
const result = await manager.exec(nodeId, 'tailscale up --exit-node= 2>&1 && tailscale status | head -5');
|
|
1039
|
+
return ok(nodeId, result.durationMs, result.stdout, 'tailscale full-off');
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
// full-on / full-off: node-wide VPN with mesh route exclusions
|
|
1044
|
+
if (action === 'full-on') {
|
|
1045
|
+
if (detected === 'mullvad') {
|
|
1046
|
+
const relay = vpnServer ? `mullvad relay set location ${vpnServer};` : '';
|
|
1047
|
+
// Mullvad full mode: enable, but add route exclusions for mesh IPs
|
|
1048
|
+
const result = await manager.exec(nodeId, `${relay} mullvad lan set allow; mullvad split-tunnel set state on; mullvad connect && sleep 2 && ` +
|
|
1049
|
+
// Preserve ALL mesh routes: wg0 (main mesh), wg1 (B2B), tailscale0 (TS networks)
|
|
1050
|
+
`ip route add 10.10.0.0/24 dev wg0 2>/dev/null; ` + // WG mesh
|
|
1051
|
+
`ip route add 10.20.0.0/24 dev wg1 2>/dev/null; ` + // WG B2B
|
|
1052
|
+
`ip route add 100.64.0.0/10 dev tailscale0 2>/dev/null; ` + // Tailscale CGNAT range
|
|
1053
|
+
`mullvad status && echo "---mesh-check---" && ping -c1 -W2 10.10.0.1 2>/dev/null && echo "mesh: OK" || echo "mesh: WARN"`);
|
|
1054
|
+
return ok(nodeId, result.durationMs, result.stdout, 'mullvad full-on (mesh preserved)');
|
|
1055
|
+
}
|
|
1056
|
+
if (detected === 'openvpn') {
|
|
1057
|
+
const configPath = config ?? vpnServer ?? '/etc/openvpn/client.conf';
|
|
1058
|
+
// OpenVPN with route-nopull + specific routes — keeps mesh alive
|
|
1059
|
+
const result = await manager.exec(nodeId, `openvpn --config "${configPath}" --daemon --log /tmp/openvpn-full.log ` +
|
|
1060
|
+
`--route-nopull --route 0.0.0.0 0.0.0.0 vpn_gateway ` +
|
|
1061
|
+
`--route 10.10.0.0 255.255.255.0 net_gateway ` + // wg0 mesh
|
|
1062
|
+
`--route 10.20.0.0 255.255.255.0 net_gateway ` + // wg1 B2B
|
|
1063
|
+
`--route 100.64.0.0 255.192.0.0 net_gateway ` + // tailscale CGNAT
|
|
1064
|
+
`&& sleep 4 && ip addr show tun0 2>/dev/null | grep inet && curl -s --max-time 5 https://ifconfig.me && ` +
|
|
1065
|
+
`echo "---mesh---" && ping -c1 -W2 10.10.0.1 2>/dev/null && echo "mesh: OK" || echo "mesh: WARN"`);
|
|
1066
|
+
return ok(nodeId, result.durationMs, result.stdout, 'openvpn full-on (mesh preserved)');
|
|
1067
|
+
}
|
|
1068
|
+
return fail(`full-on not supported for ${detected}. Use mullvad, openvpn, or tailscale.`);
|
|
1069
|
+
}
|
|
1070
|
+
if (action === 'full-off') {
|
|
1071
|
+
if (detected === 'mullvad') {
|
|
1072
|
+
const result = await manager.exec(nodeId, 'mullvad disconnect && mullvad status');
|
|
1073
|
+
return ok(nodeId, result.durationMs, result.stdout, 'mullvad full-off');
|
|
1074
|
+
}
|
|
1075
|
+
if (detected === 'openvpn') {
|
|
1076
|
+
const result = await manager.exec(nodeId, 'pkill openvpn 2>/dev/null; sleep 1; echo "disconnected" && curl -s --max-time 5 https://ifconfig.me');
|
|
1077
|
+
return ok(nodeId, result.durationMs, result.stdout, 'openvpn full-off');
|
|
1078
|
+
}
|
|
1079
|
+
return fail(`full-off not supported for ${detected}`);
|
|
1080
|
+
}
|
|
1081
|
+
return fail(`No VPN provider found on ${nodeId}. Install mullvad, tailscale, openvpn, or wireguard.`);
|
|
1082
|
+
});
|
|
1083
|
+
// --- Tool 31: omniwire_clipboard ---
|
|
743
1084
|
server.tool('omniwire_clipboard', 'Copy text between nodes via a shared clipboard buffer.', {
|
|
744
1085
|
action: z.enum(['copy', 'paste', 'clear']).describe('Action'),
|
|
745
1086
|
content: z.string().optional().describe('Text to copy (for copy action)'),
|