@@ -2,6 +2,12 @@ name: E2E Protocol Tests
22
33on :
44 workflow_dispatch :
5+ pull_request :
6+ branches : [main]
7+ paths-ignore :
8+ - ' .github/workflows/**'
9+ - ' *.md'
10+ - ' LICENSE'
511
612env :
713 DROPLET_NAME : " e2e-lantern-box-${{ github.run_id }}"
1016jobs :
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"
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