Skip to content

Commit 9d471d9

Browse files
myleshortonclaude
andcommitted
Merge main into update-sing-box-minimal
Resolve e2e.yaml conflicts by taking main's version which includes all tested fixes from PRs #124-#126. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2 parents 39b4047 + cbc3b3b commit 9d471d9

File tree

3 files changed

+219
-55
lines changed

3 files changed

+219
-55
lines changed

.github/workflows/e2e.yaml

Lines changed: 147 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@ name: E2E Protocol Tests
22

33
on:
44
workflow_dispatch:
5+
pull_request:
6+
branches: [main]
7+
paths-ignore:
8+
- '.github/workflows/**'
9+
- '*.md'
10+
- 'LICENSE'
511

612
env:
713
DROPLET_NAME: "e2e-lantern-box-${{ github.run_id }}"
@@ -10,6 +16,7 @@ env:
1016
jobs:
1117
e2e:
1218
runs-on: ubuntu-latest
19+
timeout-minutes: 30
1320
steps:
1421
- uses: actions/checkout@v4
1522

@@ -24,20 +31,22 @@ jobs:
2431
2532
- name: Build lantern-box (linux/amd64)
2633
run: |
27-
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
34+
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build \
2835
-tags "with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api" \
36+
-ldflags="-s -w" \
2937
-o lantern-box \
3038
./cmd
39+
ls -lh lantern-box
3140
3241
- name: Generate test credentials
3342
id: creds
3443
run: |
3544
# Samizdat X25519 keypair
3645
openssl genpkey -algorithm X25519 -out samizdat_priv.pem
37-
SAMIZDAT_PRIV=$(openssl pkey -in samizdat_priv.pem -text 2>/dev/null \
38-
| grep -A2 'priv:' | tail -n+2 | tr -d ' :\n')
39-
SAMIZDAT_PUB=$(openssl pkey -in samizdat_priv.pem -pubout -text 2>/dev/null \
40-
| grep -A2 'pub:' | tail -n+2 | tr -d ' :\n')
46+
SAMIZDAT_PRIV=$(openssl pkey -in samizdat_priv.pem -outform DER \
47+
| tail -c 32 | xxd -p | tr -d '\n')
48+
SAMIZDAT_PUB=$(openssl pkey -in samizdat_priv.pem -pubout -outform DER \
49+
| tail -c 32 | xxd -p | tr -d '\n')
4150
SAMIZDAT_SHORT_ID=$(openssl rand -hex 8)
4251
4352
# Self-signed TLS cert for Samizdat
@@ -47,16 +56,17 @@ jobs:
4756
4857
# Locate plain.wasm from Go module cache
4958
WATER_MOD_DIR="$(go env GOPATH)/pkg/mod/github.com/refraction-networking/water@v0.7.1-alpha"
50-
WASM_PATH="${WATER_MOD_DIR}/transport/v0/testdata/plain.wasm"
59+
WASM_PATH="${WATER_MOD_DIR}/transport/v1/testdata/plain.wasm"
5160
if [ ! -f "$WASM_PATH" ]; then
5261
echo "plain.wasm not found at $WASM_PATH, downloading module..."
5362
go mod download github.com/refraction-networking/water@v0.7.1-alpha
5463
WATER_MOD_DIR="$(go env GOPATH)/pkg/mod/github.com/refraction-networking/water@v0.7.1-alpha"
55-
WASM_PATH="${WATER_MOD_DIR}/transport/v0/testdata/plain.wasm"
64+
WASM_PATH="${WATER_MOD_DIR}/transport/v1/testdata/plain.wasm"
5665
fi
5766
WASM_HASH=$(sha256sum "$WASM_PATH" | awk '{print $1}')
5867
cp "$WASM_PATH" plain.wasm
5968
69+
echo "::add-mask::$SAMIZDAT_PRIV"
6070
echo "samizdat_priv=$SAMIZDAT_PRIV" >> "$GITHUB_OUTPUT"
6171
echo "samizdat_pub=$SAMIZDAT_PUB" >> "$GITHUB_OUTPUT"
6272
echo "samizdat_short_id=$SAMIZDAT_SHORT_ID" >> "$GITHUB_OUTPUT"
@@ -82,7 +92,7 @@ jobs:
8292
# Create droplet
8393
DROPLET_ID=$(doctl compute droplet create "$DROPLET_NAME" \
8494
--image ubuntu-24-04-x64 \
85-
--size s-1vcpu-1gb \
95+
--size s-2vcpu-2gb \
8696
--region nyc3 \
8797
--ssh-keys "$SSH_KEY_ID" \
8898
--wait \
@@ -97,15 +107,21 @@ jobs:
97107
echo "Droplet $DROPLET_ID created at $DROPLET_IP"
98108
99109
# Wait for SSH readiness
110+
SSH_READY=0
100111
for i in $(seq 1 30); do
101112
if ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 \
102113
-i e2e_key root@"$DROPLET_IP" echo ready 2>/dev/null; then
103114
echo "SSH is ready"
115+
SSH_READY=1
104116
break
105117
fi
106118
echo "Waiting for SSH... attempt $i/30"
107119
sleep 10
108120
done
121+
if [ "$SSH_READY" -ne 1 ]; then
122+
echo "ERROR: SSH never became ready after 30 attempts"
123+
exit 1
124+
fi
109125
110126
- name: Deploy to droplet
111127
env:
@@ -114,22 +130,33 @@ jobs:
114130
SAMIZDAT_SHORT_ID: ${{ steps.creds.outputs.samizdat_short_id }}
115131
WASM_HASH: ${{ steps.creds.outputs.wasm_hash }}
116132
run: |
117-
SSH_OPTS="-o StrictHostKeyChecking=no -i e2e_key"
133+
SSH_OPTS="-o StrictHostKeyChecking=no -o ServerAliveInterval=30 -i e2e_key"
134+
135+
# Compress binary before upload
136+
gzip -1 -c lantern-box > lantern-box.gz
137+
ls -lh lantern-box.gz
118138
119139
# Upload files
120-
scp $SSH_OPTS lantern-box cert.pem key.pem plain.wasm root@"$DROPLET_IP":/root/
140+
echo "Starting SCP upload..."
141+
scp $SSH_OPTS lantern-box.gz cert.pem key.pem plain.wasm root@"$DROPLET_IP":/root/
142+
echo "SCP upload complete"
121143
122-
# Make binary executable
123-
ssh $SSH_OPTS root@"$DROPLET_IP" chmod +x /root/lantern-box
144+
# Decompress and make binary executable
145+
echo "Decompressing binary..."
146+
ssh $SSH_OPTS root@"$DROPLET_IP" \
147+
"gzip -d /root/lantern-box.gz && chmod +x /root/lantern-box"
148+
echo "Binary ready"
124149
125150
# Start Python HTTP server to serve plain.wasm (for WATER)
126-
ssh $SSH_OPTS root@"$DROPLET_IP" \
127-
"cd /root && nohup python3 -m http.server 8888 > /root/wasm-server.log 2>&1 &"
151+
echo "Starting WASM HTTP server..."
152+
ssh -f $SSH_OPTS root@"$DROPLET_IP" \
153+
"cd /root && python3 -m http.server 8888 > /root/wasm-server.log 2>&1"
154+
echo "WASM HTTP server started"
128155
129156
# --- ALGeneva server config ---
130157
cat > /tmp/algeneva-server.json << 'JSONEOF'
131158
{
132-
"log": {"level": "debug"},
159+
"log": {"level": "trace"},
133160
"inbounds": [{
134161
"type": "algeneva",
135162
"tag": "algeneva-in",
@@ -178,7 +205,8 @@ jobs:
178205
"listen_port": 9003,
179206
"transport": "plain",
180207
"hashsum": "${WASM_HASH}",
181-
"wasm_available_at": ["http://127.0.0.1:8888/plain.wasm"]
208+
"wasm_available_at": ["http://127.0.0.1:8888/plain.wasm"],
209+
"config": {}
182210
}],
183211
"outbounds": [{
184212
"type": "direct",
@@ -189,26 +217,45 @@ jobs:
189217
scp $SSH_OPTS /tmp/water-server.json root@"$DROPLET_IP":/root/water-server.json
190218
191219
# Start all 3 server processes
192-
ssh $SSH_OPTS root@"$DROPLET_IP" \
193-
"nohup /root/lantern-box run --config /root/algeneva-server.json > /root/algeneva-server.log 2>&1 &
194-
nohup /root/lantern-box run --config /root/samizdat-server.json > /root/samizdat-server.log 2>&1 &
195-
nohup /root/lantern-box run --config /root/water-server.json > /root/water-server.log 2>&1 &"
196-
197-
# Wait for servers to start
198-
sleep 5
199-
200-
# Verify server processes are running
201-
ssh $SSH_OPTS root@"$DROPLET_IP" \
202-
"pgrep -f 'lantern-box run' || (echo 'No lantern-box processes found!' && cat /root/*.log && exit 1)"
220+
echo "Starting servers..."
221+
ssh $SSH_OPTS root@"$DROPLET_IP" "nohup /root/lantern-box run --config /root/algeneva-server.json > /root/algeneva-server.log 2>&1 < /dev/null &
222+
nohup /root/lantern-box run --config /root/samizdat-server.json > /root/samizdat-server.log 2>&1 < /dev/null &
223+
nohup /root/lantern-box run --config /root/water-server.json > /root/water-server.log 2>&1 < /dev/null &
224+
sleep 1"
225+
echo "Servers launched"
226+
227+
# Wait for servers to be ready (WATER needs time to download/compile WASM)
228+
echo "Checking port readiness..."
229+
# Note: must use bash explicitly since Ubuntu default shell is dash (no /dev/tcp support)
230+
ssh $SSH_OPTS root@"$DROPLET_IP" bash << 'READYEOF'
231+
for port in 9001 9002 9003; do
232+
echo "Waiting for port $port to be ready..."
233+
for i in $(seq 1 60); do
234+
if echo > /dev/tcp/127.0.0.1/$port 2>/dev/null; then
235+
echo "Port $port is ready"
236+
break
237+
fi
238+
if [ "$i" -eq 60 ]; then
239+
echo "Port $port not ready after 60 seconds" >&2
240+
cat /root/*.log
241+
exit 1
242+
fi
243+
sleep 1
244+
done
245+
done
246+
READYEOF
203247
204248
- name: Test ALGeneva
249+
id: test_algeneva
250+
continue-on-error: true
205251
env:
206252
DROPLET_IP: ${{ steps.droplet.outputs.droplet_ip }}
207253
run: |
208254
# Generate client config
255+
# Use a simple strategy that only modifies the host header (not the CONNECT request line)
209256
cat > /tmp/algeneva-client.json << JSONEOF
210257
{
211-
"log": {"level": "debug"},
258+
"log": {"level": "trace"},
212259
"inbounds": [{
213260
"type": "mixed",
214261
"tag": "mixed-in",
@@ -220,7 +267,7 @@ jobs:
220267
"tag": "algeneva-out",
221268
"server": "${DROPLET_IP}",
222269
"server_port": 9001,
223-
"strategy": "[HTTP:method:*]-insert{%0A:end:value:2}-|"
270+
"strategy": "[HTTP:host:*]-changecase{lower}-|"
224271
}]
225272
}
226273
JSONEOF
@@ -238,11 +285,14 @@ jobs:
238285
fi
239286
240287
# Test
288+
set +e
241289
RESPONSE=$(curl -sf -x socks5h://127.0.0.1:1081 -m 30 http://example.com)
290+
CURL_EXIT=$?
291+
set -e
242292
if echo "$RESPONSE" | grep -q "Example Domain"; then
243293
echo "ALGeneva test PASSED"
244294
else
245-
echo "ALGeneva test FAILED"
295+
echo "ALGeneva test FAILED (curl exit code: $CURL_EXIT)"
246296
echo "Response: $RESPONSE"
247297
cat /tmp/algeneva-client.log
248298
kill $CLIENT_PID 2>/dev/null || true
@@ -252,6 +302,8 @@ jobs:
252302
kill $CLIENT_PID 2>/dev/null || true
253303
254304
- name: Test Samizdat
305+
id: test_samizdat
306+
continue-on-error: true
255307
env:
256308
DROPLET_IP: ${{ steps.droplet.outputs.droplet_ip }}
257309
SAMIZDAT_PUB: ${{ steps.creds.outputs.samizdat_pub }}
@@ -288,11 +340,14 @@ jobs:
288340
exit 1
289341
fi
290342
343+
set +e
291344
RESPONSE=$(curl -sf -x socks5h://127.0.0.1:1082 -m 30 http://example.com)
345+
CURL_EXIT=$?
346+
set -e
292347
if echo "$RESPONSE" | grep -q "Example Domain"; then
293348
echo "Samizdat test PASSED"
294349
else
295-
echo "Samizdat test FAILED"
350+
echo "Samizdat test FAILED (curl exit code: $CURL_EXIT)"
296351
echo "Response: $RESPONSE"
297352
cat /tmp/samizdat-client.log
298353
kill $CLIENT_PID 2>/dev/null || true
@@ -302,6 +357,8 @@ jobs:
302357
kill $CLIENT_PID 2>/dev/null || true
303358
304359
- name: Test WATER
360+
id: test_water
361+
continue-on-error: true
305362
env:
306363
DROPLET_IP: ${{ steps.droplet.outputs.droplet_ip }}
307364
WASM_HASH: ${{ steps.creds.outputs.wasm_hash }}
@@ -324,7 +381,8 @@ jobs:
324381
"hashsum": "${WASM_HASH}",
325382
"wasm_available_at": ["http://${DROPLET_IP}:8888/plain.wasm"],
326383
"download_timeout": "60s",
327-
"water_dir": "/tmp/water"
384+
"water_dir": "/tmp/water",
385+
"config": {}
328386
}]
329387
}
330388
JSONEOF
@@ -339,11 +397,14 @@ jobs:
339397
exit 1
340398
fi
341399
400+
set +e
342401
RESPONSE=$(curl -sf -x socks5h://127.0.0.1:1083 -m 30 http://example.com)
402+
CURL_EXIT=$?
403+
set -e
343404
if echo "$RESPONSE" | grep -q "Example Domain"; then
344405
echo "WATER test PASSED"
345406
else
346-
echo "WATER test FAILED"
407+
echo "WATER test FAILED (curl exit code: $CURL_EXIT)"
347408
echo "Response: $RESPONSE"
348409
cat /tmp/water-client.log
349410
kill $CLIENT_PID 2>/dev/null || true
@@ -352,6 +413,35 @@ jobs:
352413
353414
kill $CLIENT_PID 2>/dev/null || true
354415
416+
- name: Check test results
417+
if: always()
418+
run: |
419+
FAILED=0
420+
echo "=== E2E Test Results ==="
421+
if [ "${{ steps.test_algeneva.outcome }}" = "success" ]; then
422+
echo "ALGeneva: PASSED"
423+
else
424+
echo "ALGeneva: FAILED"
425+
FAILED=1
426+
fi
427+
if [ "${{ steps.test_samizdat.outcome }}" = "success" ]; then
428+
echo "Samizdat: PASSED"
429+
else
430+
echo "Samizdat: FAILED"
431+
FAILED=1
432+
fi
433+
if [ "${{ steps.test_water.outcome }}" = "success" ]; then
434+
echo "WATER: PASSED"
435+
else
436+
echo "WATER: FAILED"
437+
FAILED=1
438+
fi
439+
echo "========================"
440+
if [ "$FAILED" -ne 0 ]; then
441+
echo "One or more tests failed"
442+
exit 1
443+
fi
444+
355445
- name: Collect server logs
356446
if: always()
357447
env:
@@ -375,16 +465,32 @@ jobs:
375465

376466
- name: Cleanup DO resources
377467
if: always()
468+
env:
469+
DO_API_TOKEN: ${{ secrets.DO_API_TOKEN }}
378470
run: |
379471
DROPLET_ID="${{ steps.droplet.outputs.droplet_id }}"
380472
SSH_KEY_ID="${{ steps.droplet.outputs.ssh_key_id }}"
381473
382-
if [ -n "$DROPLET_ID" ]; then
383-
echo "Destroying droplet $DROPLET_ID..."
384-
doctl compute droplet delete "$DROPLET_ID" --force || true
385-
fi
386-
387-
if [ -n "$SSH_KEY_ID" ]; then
388-
echo "Deleting SSH key $SSH_KEY_ID..."
389-
doctl compute ssh-key delete "$SSH_KEY_ID" --force || true
474+
# Fall back to DO API directly if doctl is not available
475+
if command -v doctl &>/dev/null; then
476+
if [ -n "$DROPLET_ID" ]; then
477+
echo "Destroying droplet $DROPLET_ID..."
478+
doctl compute droplet delete "$DROPLET_ID" --force || true
479+
fi
480+
if [ -n "$SSH_KEY_ID" ]; then
481+
echo "Deleting SSH key $SSH_KEY_ID..."
482+
doctl compute ssh-key delete "$SSH_KEY_ID" --force || true
483+
fi
484+
else
485+
echo "doctl not available, using DO API directly"
486+
if [ -n "$DROPLET_ID" ]; then
487+
echo "Destroying droplet $DROPLET_ID..."
488+
curl -sf -X DELETE "https://api.digitalocean.com/v2/droplets/$DROPLET_ID" \
489+
-H "Authorization: Bearer $DO_API_TOKEN" || true
490+
fi
491+
if [ -n "$SSH_KEY_ID" ]; then
492+
echo "Deleting SSH key $SSH_KEY_ID..."
493+
curl -sf -X DELETE "https://api.digitalocean.com/v2/account/keys/$SSH_KEY_ID" \
494+
-H "Authorization: Bearer $DO_API_TOKEN" || true
495+
fi
390496
fi

0 commit comments

Comments
 (0)