Skip to content

Commit 7d77c00

Browse files
authored
fix: support WSL mirrored networking (#274)
Co-authored-by: Premshay <28099628+Premshay@users.noreply.github.com>
1 parent 74d3575 commit 7d77c00

4 files changed

Lines changed: 111 additions & 14 deletions

File tree

docs/MCP_CLI_TEST_PLAN.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,15 @@ Refresh authentication tokens for NotebookLM.
5959
### Test 1.2 - Interactive Login (Primary)
6060
**Tool:** `save_auth_tokens` (Fallback)
6161
**CLI:** `nlm login` (Launches Chrome for automated extraction)
62+
**WSL2 CLI:** `nlm login --wsl` (Supports NAT and mirrored networking modes)
6263

6364
**Prompt:**
6465
```
6566
I need to authenticate with NotebookLM.
6667
```
6768

68-
**Expected:** Chrome opens, logs in, and tokens are saved.
69+
**Expected:** Chrome opens, logs in, and tokens are saved. On WSL2, Windows Chrome opens and
70+
the CDP connection succeeds in both NAT and mirrored networking modes.
6971

7072
---
7173

docs/WSL_SETUP.md

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ nlm login --wsl
8686
```
8787

8888
This will:
89-
- Detect your Windows IP address from WSL
89+
- Detect the WSL networking mode and Windows host address
9090
- **Check Windows Firewall setup** (prompts with instructions)
9191
- Launch Chrome on Windows on port 9223 with remote debugging
9292
- Connect via the port proxy on port 9222
@@ -109,14 +109,15 @@ When you run `nlm login --wsl`:
109109

110110
```
111111
WSL Terminal
112-
↓ detects Windows host IP (from default gateway)
112+
↓ detects networking mode with wslinfo
113+
↓ uses the default gateway (NAT) or 127.0.0.1 (mirrored)
113114
↓ launches /mnt/c/Program Files/Google/Chrome/Application/chrome.exe
114115
Windows Chrome
115116
↓ starts on 127.0.0.1:9223 (Windows side, localhost only)
116117
netsh portproxy
117118
↓ forwards 0.0.0.0:9222 → 127.0.0.1:9223
118119
WSL Auth Script
119-
↓ connects to http://172.x.x.x:9222 (via port proxy)
120+
↓ connects to the Windows host on port 9222 (via port proxy)
120121
↓ opens notebook.google.com tab
121122
↓ waits for login
122123
↓ extracts cookies via CDP
@@ -180,13 +181,18 @@ cat /etc/resolv.conf
180181
grep nameserver /etc/resolv.conf
181182
```
182183

183-
You should see an IP like `172.20.x.x`. If not, your WSL2 networking may be in a different mode.
184+
In NAT mode, you should see an IP like `172.20.x.x`. In mirrored mode, the Windows host is
185+
available at `127.0.0.1`.
184186

185187
**Workaround:**
186188
```bash
187-
# Find Windows IP manually
188-
WINDOWS_IP=$(ip route show | grep default | awk '{print $3}')
189-
nlm login --cdp-url http://$WINDOWS_IP:9222
189+
# Find the Windows host address manually
190+
if [ "$(wslinfo --networking-mode 2>/dev/null)" = "mirrored" ]; then
191+
WINDOWS_IP=127.0.0.1
192+
else
193+
WINDOWS_IP=$(ip route show default | awk '{print $3; exit}')
194+
fi
195+
nlm login --cdp-url "http://${WINDOWS_IP}:9222"
190196
```
191197

192198
### "Chrome did not start within 30 seconds"
@@ -202,7 +208,12 @@ Sometimes Windows firewall or antivirus blocks the connection.
202208
```
203209
```bash
204210
# In WSL (wait a few seconds first)
205-
nlm login --cdp-url http://$(grep nameserver /etc/resolv.conf | awk '{print $2}'):9222
211+
if [ "$(wslinfo --networking-mode 2>/dev/null)" = "mirrored" ]; then
212+
WINDOWS_IP=127.0.0.1
213+
else
214+
WINDOWS_IP=$(ip route show default | awk '{print $3; exit}')
215+
fi
216+
nlm login --cdp-url "http://${WINDOWS_IP}:9222"
206217
```
207218

208219
### Terminal still goes black
@@ -245,11 +256,15 @@ CHROME_PID=$!
245256
# Wait for startup
246257
sleep 3
247258

248-
# Get Windows IP
249-
WINDOWS_IP=$(grep nameserver /etc/resolv.conf | awk '{print $2}')
259+
# Get the Windows host address
260+
if [ "$(wslinfo --networking-mode 2>/dev/null)" = "mirrored" ]; then
261+
WINDOWS_IP=127.0.0.1
262+
else
263+
WINDOWS_IP=$(ip route show default | awk '{print $3; exit}')
264+
fi
250265

251266
# Login via CDP
252-
nlm login --cdp-url http://$WINDOWS_IP:9222
267+
nlm login --cdp-url "http://${WINDOWS_IP}:9222"
253268

254269
# Cleanup
255270
kill $CHROME_PID

src/notebooklm_tools/utils/wsl.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,18 +60,37 @@ def is_wsl() -> bool:
6060
return False
6161

6262

63+
def _is_mirrored_networking() -> bool:
64+
"""Return whether WSL is using mirrored networking mode."""
65+
try:
66+
result = subprocess.run(
67+
["wslinfo", "--networking-mode"],
68+
capture_output=True,
69+
text=True,
70+
check=True,
71+
timeout=5,
72+
)
73+
except (OSError, subprocess.CalledProcessError, subprocess.TimeoutExpired):
74+
return False
75+
76+
return result.stdout.strip().casefold() == "mirrored"
77+
78+
6379
def get_windows_host_ip() -> str | None:
6480
"""Get the Windows host IP address from WSL.
6581
66-
WSL2 uses a virtual network where the Windows host is the default gateway.
67-
We check multiple sources to find the correct IP.
82+
In mirrored mode, Windows is reachable through the shared loopback
83+
interface. In NAT mode, the Windows host is the default gateway.
6884
6985
Returns:
7086
IP address string (e.g., "172.20.112.1") or None if not in WSL.
7187
"""
7288
if not is_wsl():
7389
return None
7490

91+
if _is_mirrored_networking():
92+
return "127.0.0.1"
93+
7594
# Method 1: Get default gateway (most reliable for Chrome binding)
7695
try:
7796
result = subprocess.run(

tests/test_wsl.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
"""Tests for WSL networking utilities."""
2+
3+
import subprocess
4+
from unittest.mock import Mock, call
5+
6+
from notebooklm_tools.utils import wsl
7+
8+
9+
def _result(args: list[str], stdout: str) -> subprocess.CompletedProcess[str]:
10+
return subprocess.CompletedProcess(args, returncode=0, stdout=stdout, stderr="")
11+
12+
13+
def test_get_windows_host_ip_uses_loopback_in_mirrored_mode(monkeypatch):
14+
monkeypatch.setattr(wsl, "is_wsl", lambda: True)
15+
run = Mock(return_value=_result(["wslinfo", "--networking-mode"], "mirrored\n"))
16+
monkeypatch.setattr(wsl.subprocess, "run", run)
17+
18+
assert wsl.get_windows_host_ip() == "127.0.0.1"
19+
run.assert_called_once_with(
20+
["wslinfo", "--networking-mode"],
21+
capture_output=True,
22+
text=True,
23+
check=True,
24+
timeout=5,
25+
)
26+
27+
28+
def test_get_windows_host_ip_uses_gateway_in_nat_mode(monkeypatch):
29+
monkeypatch.setattr(wsl, "is_wsl", lambda: True)
30+
run = Mock(
31+
side_effect=[
32+
_result(["wslinfo", "--networking-mode"], "nat\n"),
33+
_result(["ip", "route"], "default via 172.20.112.1 dev eth0\n"),
34+
]
35+
)
36+
monkeypatch.setattr(wsl.subprocess, "run", run)
37+
38+
assert wsl.get_windows_host_ip() == "172.20.112.1"
39+
assert run.call_args_list == [
40+
call(
41+
["wslinfo", "--networking-mode"],
42+
capture_output=True,
43+
text=True,
44+
check=True,
45+
timeout=5,
46+
),
47+
call(["ip", "route"], capture_output=True, text=True, check=True),
48+
]
49+
50+
51+
def test_get_windows_host_ip_uses_gateway_when_wslinfo_is_unavailable(monkeypatch):
52+
monkeypatch.setattr(wsl, "is_wsl", lambda: True)
53+
54+
def run(args, **kwargs):
55+
if args[0] == "wslinfo":
56+
raise FileNotFoundError
57+
return _result(args, "default via 172.20.112.1 dev eth0\n")
58+
59+
monkeypatch.setattr(wsl.subprocess, "run", run)
60+
61+
assert wsl.get_windows_host_ip() == "172.20.112.1"

0 commit comments

Comments
 (0)