Skip to content

Commit 841d31b

Browse files
authored
Mitigate eMudhra trust and retain session-ticket compatibility on Go 1.26.10 (#687)
* Patch CA trust and preserve session tickets with Go 1.26.10 * Include trust policy changes in main image publication * Record published Linux 7.x staging compatibility evidence * Verify server ticket issuance and legacy TLS resumption on the wire * Record legacy ticket tests against deployed staging cloud API * Point cloud validation reference to the pull request
1 parent 1261f18 commit 841d31b

12 files changed

Lines changed: 401 additions & 5 deletions

‎.github/workflows/go.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ on:
77
branches-ignore: main
88
paths:
99
- 'Dockerfile'
10+
- 'deploy/trust/**'
1011
- 'go.mod'
1112
- 'go.sum'
1213
- '**.go'

‎.github/workflows/push.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ on:
44
branches: [main]
55
paths:
66
- 'Dockerfile'
7+
- 'deploy/trust/**'
78
- 'go.mod'
89
- 'go.sum'
910
- '**.go'

‎Dockerfile‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
# syntax = docker/dockerfile:1.3
2-
FROM --platform=$BUILDPLATFORM golang:1.24-alpine as builder
2+
FROM --platform=$BUILDPLATFORM golang:1.26.10-alpine as builder
33
ARG TARGETOS TARGETARCH
44

55
WORKDIR $GOPATH/src/getlantern/http-proxy-lantern/
66
COPY . .
77

8-
RUN apk add git
8+
RUN apk add --no-cache git ca-certificates openssl
9+
RUN sh deploy/trust/update-trust.sh
910
RUN --mount=type=secret,id=github_oauth_token \
1011
git config --global url."https://$(cat /run/secrets/github_oauth_token):x-oauth-basic@github.com/".insteadOf "https://github.com/"
1112

@@ -16,6 +17,9 @@ FROM --platform=$BUILDPLATFORM alpine as user
1617
RUN adduser -S -u 10000 lantern
1718

1819
FROM alpine
20+
COPY deploy/trust /usr/local/lib/lantern-trust
21+
RUN apk add --no-cache ca-certificates openssl \
22+
&& sh /usr/local/lib/lantern-trust/update-trust.sh
1923
COPY --from=user /etc/passwd /etc/passwd
2024
COPY --from=builder /usr/local/bin/http-proxy /usr/local/bin/http-proxy
2125

‎deploy/trust/blocked-roots.sha256‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
7DD78D5F4F13459A83DFF9ABBBA62EDBAF6F2D102BF257FD712F4D9F2746ED8D
2+
F2EDAAFF3D9B701FB4D14C9F285F8C0BAD90FC3BCC1FA07D47F4718EDABA34A5
3+
D7A7A0FB5D7E2731D771E9484EBCDEF71D5F0C3E0A2948782BC83EE0EA699EF4
4+
86A1ECBA089C4A8D3BBE2734C612BA341D813E043CF9E8A862CD5C57A36BBE6B

‎deploy/trust/update-trust.sh‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#!/bin/sh
2+
set -eu
3+
4+
trust_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
5+
blocked_roots=${BLOCKED_ROOTS_FILE:-$trust_dir/blocked-roots.sha256}
6+
[ -s "$blocked_roots" ] || { printf 'missing distrust policy: %s\n' "$blocked_roots" >&2; exit 1; }
7+
config=/etc/ca-certificates.conf
8+
temporary=$(mktemp -d)
9+
trap 'rm -rf "$temporary"' EXIT HUP INT TERM
10+
11+
# Disable sources by certificate fingerprint, including alternate package filenames.
12+
while IFS= read -r entry || [ -n "$entry" ]; do
13+
case "$entry" in
14+
''|'#'*|'!'*) printf '%s\n' "$entry"; continue ;;
15+
esac
16+
certificate="/usr/share/ca-certificates/$entry"
17+
if [ ! -f "$certificate" ]; then
18+
printf 'missing certificate source: %s\n' "$certificate" >&2
19+
exit 1
20+
fi
21+
fingerprint=$(openssl x509 -in "$certificate" -noout -sha256 -fingerprint)
22+
fingerprint=$(printf '%s' "${fingerprint#*=}" | tr -d ':')
23+
if grep -Fxq "$fingerprint" "$blocked_roots"; then
24+
printf '!%s\n' "$entry"
25+
else
26+
printf '%s\n' "$entry"
27+
fi
28+
done < "$config" > "$temporary/ca-certificates.conf"
29+
30+
cat "$temporary/ca-certificates.conf" > "$config"
31+
update-ca-certificates --fresh
32+
sh "$trust_dir/verify-bundle.sh" /etc/ssl/certs/ca-certificates.crt

‎deploy/trust/verify-bundle.sh‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#!/bin/sh
2+
set -eu
3+
4+
trust_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
5+
blocked_roots=${BLOCKED_ROOTS_FILE:-$trust_dir/blocked-roots.sha256}
6+
[ -s "$blocked_roots" ] || { printf 'missing distrust policy: %s\n' "$blocked_roots" >&2; exit 1; }
7+
bundle=${1:?usage: verify-bundle.sh CA_BUNDLE}
8+
temporary=$(mktemp -d)
9+
trap 'rm -rf "$temporary"' EXIT HUP INT TERM
10+
11+
awk -v directory="$temporary" '
12+
/-----BEGIN CERTIFICATE-----/ { count++; output = directory "/" count ".pem" }
13+
output != "" { print > output }
14+
/-----END CERTIFICATE-----/ { close(output); output = "" }
15+
' "$bundle"
16+
17+
count=0
18+
for certificate in "$temporary"/*.pem; do
19+
[ -f "$certificate" ] || continue
20+
count=$((count + 1))
21+
fingerprint=$(openssl x509 -in "$certificate" -noout -sha256 -fingerprint)
22+
fingerprint=$(printf '%s' "${fingerprint#*=}" | tr -d ':')
23+
if grep -Fxq "$fingerprint" "$blocked_roots"; then
24+
printf 'blocked root in %s: %s\n' "$bundle" "$fingerprint" >&2
25+
exit 1
26+
fi
27+
done
28+
[ "$count" -gt 0 ] || { printf 'empty CA bundle: %s\n' "$bundle" >&2; exit 1; }
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
# Published Linux 7.x compatibility verification
2+
3+
Verified on 2026-10-21 against the dedicated **staging** `http-proxy-test`
4+
route `3e9aa468-47ca-4fc3-b61e-4049a3f92e34` (`45.79.155.229:8021`).
5+
Production was not changed. No client was rebuilt or patched.
6+
7+
## Artifacts
8+
9+
Clients came from the `update_linux_amd64.bz2` assets of the official
10+
[7.9.5](https://github.com/getlantern/lantern/releases/tag/7.9.5) and
11+
[7.7.2](https://github.com/getlantern/lantern/releases/tag/7.7.2) releases.
12+
The decompressed executables had these SHA256 hashes before and after testing:
13+
14+
| Release | Executable SHA256 |
15+
| --- | --- |
16+
| 7.9.5 | `fc62e4b3b5d457b43c7b5bbab7ff78d813c5046871ef5473ed0ee0bcfdac7293` |
17+
| 7.7.2 | `bc8614165560b734c823760a30315530344f0415e695e1668a8f98db86f88ff7` |
18+
19+
The candidate was built from runtime revision `e335fd0` using Go 1.26.10,
20+
`CGO_ENABLED=0`, `GOARCH=amd64`, and `-trimpath`. Its executable SHA256 was
21+
`fec124ccfb6dfd9958e8646fff57e697d6cac7b49f89d66ae148754f81640092`;
22+
the running staging process's `/proc/<pid>/exe` matched. The subsequent
23+
workflow-only change does not change the candidate's runtime source.
24+
25+
## Method and results
26+
27+
The original binaries ran headless under Xvfb in an isolated Ubuntu 22.04
28+
container with their required GUI/libpcap libraries. Their cached `proxies.yaml`
29+
selected only the staging route. `-stickyconfig -readableconfig -proxyall`
30+
prevented remote configuration replacement. A valid cached global config
31+
set `MustProxy` rules for the test domains and disabled autoupdate. This
32+
configuration change was necessary: a standalone partial global config is
33+
rejected by these releases and causes fallback to their embedded config.
34+
35+
Every positive run required all of:
36+
37+
- Plain HTTP `http://example.com` and HTTPS `https://example.com` returning
38+
status 200 and the expected page body through the client's local HTTP proxy.
39+
- `https://api.ipify.org` reporting **45.79.155.229**, rather than the test
40+
machine's direct address.
41+
- Five further successful HTTPS requests with the same staging egress.
42+
43+
| Client | Normal route: Shadowsocks + smux | TLS + smux, API-issued ticket | Cached server-issued ticket after proxy restart |
44+
| --- | --- | --- | --- |
45+
| Linux 7.9.5 | PASS | PASS | PASS |
46+
| Linux 7.7.2 | PASS | PASS | PASS |
47+
48+
For TLS, a temporary config on the same staging listener used fixtures
49+
generated by lantern-cloud's actual `makeTLSSessionState`, `proxyini.Build`,
50+
and `LegacyConnectConfig` paths. The binary required authenticated tickets.
51+
The API-issued ticket was 121 bytes; the old client persisted a **new 123-byte
52+
server-issued ticket**. Cached runs reused that state after restarting the
53+
proxy. Client logs confirmed loading the persisted state. Thus both ticket
54+
encodings were exercised through actual old client executables.
55+
56+
### Server-side packet verification
57+
58+
The tests were repeated with `tcpdump` on the staging VPS, filtered to TCP
59+
port 8021 and the test machine's source IP. Wireshark 3.6.2 decoded the capture.
60+
For both releases the verification matched the **exact bytes** of the ticket
61+
in the client's persisted cache to a `NewSessionTicket` sent by the staging
62+
server, then matched those bytes to a subsequent ClientHello after a proxy
63+
restart. These were 123-byte Go-issued tickets, distinct from the 121-byte
64+
API-issued bootstrap ticket.
65+
66+
The server echoed the ClientHello session ID and sent an abbreviated TLS 1.2
67+
handshake: ServerHello, NewSessionTicket, ChangeCipherSpec, Finished, with no
68+
Certificate or ServerKeyExchange. Resumed connections carried application
69+
records in both directions, and the client-side HTTP/HTTPS/egress checks passed.
70+
This verifies actual resumption, rather than merely successful connection with
71+
a cached ticket that might have fallen back to a full handshake.
72+
73+
| Client | Server issuance → client reuse | Cached reuse after restart | Resumed application traffic |
74+
| --- | --- | --- | --- |
75+
| 7.9.5 | Capture stream 0 → 1 | Ticket from stream 2 offered in stream 4 | Renewed tickets in streams 5–8 |
76+
| 7.7.2 | Capture stream 9 → 10 | Ticket from stream 9 offered in stream 11 | Streams 10–11 |
77+
78+
The [sanitized assertions](test-results/emudhra-staging-resumption.json)
79+
contain the per-release result and stream identifiers. The private capture's
80+
SHA256 is `e6fb3674047aef8c57e33cb110ba56459d4dc6f0a151fcc5f971f4e6a8bfa43a`.
81+
Raw captures, ticket contents, session secrets, and route credentials are not
82+
committed. The temporary TLS listener and capture were removed; the tested
83+
candidate continues to use the route's original configuration.
84+
85+
A negative control flipped one byte in the API-issued ticket. A fresh 7.9.5
86+
client then failed to proxy traffic and produced no egress response. This
87+
checks ticket rejection and rules out a direct-connection success masking a
88+
transport failure. Unit tests separately reject tickets issued under a
89+
different key.
90+
91+
The original staging package was `http-proxy-lantern 0.0.0+22da31e9`, binary
92+
SHA256 `1c1ac5b4fb3eb4ed88365c20cd6ec111288d9a267fd8391107c248f9c48f9132`.
93+
The 7.9.5 baseline passed HTTPS/egress/repeated requests, but plain HTTP
94+
returned 403 (`requested local address example.com:80 ()`). The candidate
95+
returns 200 because it includes the origin-form Host handling fix already
96+
present on main. This was recorded separately from the trust/ticket changes.
97+
98+
## Repetition after the actual lantern-cloud staging deployment
99+
100+
Lantern-cloud was subsequently merged into staging at
101+
[`57280b004ea01d4e5394e46aa7cab9d30811034c`](https://github.com/getlantern/lantern-cloud/commit/57280b004ea01d4e5394e46aa7cab9d30811034c).
102+
The new base and deploy tooling were built first; Vault, Ops, API, Unleash,
103+
backoffice, netstated and update-server were deployed successfully.
104+
[Cloud validation and infrastructure limitations in getlantern/lantern-cloud#3665](https://github.com/getlantern/lantern-cloud/pull/3665).
105+
106+
After checking the running API's revision, both unchanged clients repeated
107+
all three positive cases in the table above. This time the bootstrap ticket
108+
and client configuration came from the **deployed API's route endpoint**, with
109+
`connect_config=true&legacy_connect_config=true`, rather than an in-process
110+
fixture generator. The normal Shadowsocks config was used unchanged. For TLS,
111+
the existing IPv6 staging route `0a49204c-4605-4aa5-bcf0-7c480478ec46` was
112+
temporarily mirrored onto the IPv4 canary using its stored certificate,
113+
authentication token and ticket keys. Only the API-returned endpoint addresses
114+
were rewritten. The API generated the 121-byte bootstrap ticket; no local
115+
helper minted it. The source route and database were not modified.
116+
117+
Both releases passed HTTP 200, HTTPS 200, staging egress and five subsequent
118+
requests for fresh and cached TLS sessions, including proxy restarts. A new
119+
server-side capture again matched each exact 123-byte server-issued ticket
120+
to the client cache and a later ClientHello. Both releases used abbreviated
121+
TLS 1.2 handshakes and carried application records in both directions.
122+
The [new sanitized assertions](test-results/emudhra-staging-live-api-resumption.json)
123+
record those results. Capture SHA256:
124+
`5a0be0fb1937660825f8e478eea6a049b89dc688cef58e4d91199d9d290ee3c3`.
125+
A one-bit mutation of the live API ticket was rejected with no egress response.
126+
The normal Shadowsocks tests also passed for both releases afterward.
127+
128+
The temporary TLS fixtures and remote capture were removed and the rollback
129+
timer stopped. The candidate is back on the original normal route config;
130+
its running executable still matches the candidate SHA256 above.
131+
132+
## Staging deployment and rollback
133+
134+
The installed package and original `/etc/http-proxy/config.ini` were preserved.
135+
Only `http-proxy.service` was restarted. The candidate is selected through
136+
`/etc/systemd/system/http-proxy.service.d/99-emudhra-canary.conf`; the temporary
137+
TLS fixture is replaced by the normal route configuration after testing.
138+
A timed rollback protected the test window.
139+
140+
To restore the original staging executable and restart that service:
141+
142+
```bash
143+
timeout 120 mise -E staging x -- bin/lc routes ssh \
144+
3e9aa468-47ca-4fc3-b61e-4049a3f92e34 \
145+
sh /tmp/emudhra-staging-canary.sh restore
146+
```
147+
148+
Run this from a lantern-cloud checkout. The restore script removes the canary
149+
override, restarts the service, and verifies that the installed binary and
150+
normal config match their saved originals. For production, use the normal
151+
reviewed release/package rollout, not this temporary override.
152+
153+
Coverage is Linux amd64 for these two releases, TLS 1.2, and the two listed
154+
transports. Other operating systems, architectures, transports, sustained
155+
load, and a full installed-package/container rollout remain release gates.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
[
2+
{
3+
"release": "7.9.5",
4+
"server": "45.79.155.229:8021",
5+
"ticket_bytes": 123,
6+
"server_issued_ticket_matches_cache": true,
7+
"client_offered_exact_ticket": true,
8+
"tls12_abbreviated_handshake": true,
9+
"bidirectional_application_data": true,
10+
"streams": [
11+
5,
12+
6,
13+
7
14+
],
15+
"cached_ticket_streams": [
16+
5,
17+
6
18+
],
19+
"application_data_streams": [
20+
5,
21+
7
22+
]
23+
},
24+
{
25+
"release": "7.7.2",
26+
"server": "45.79.155.229:8021",
27+
"ticket_bytes": 123,
28+
"server_issued_ticket_matches_cache": true,
29+
"client_offered_exact_ticket": true,
30+
"tls12_abbreviated_handshake": true,
31+
"bidirectional_application_data": true,
32+
"streams": [
33+
11
34+
],
35+
"cached_ticket_streams": [
36+
11
37+
],
38+
"application_data_streams": [
39+
11
40+
]
41+
}
42+
]
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
[
2+
{
3+
"release": "7.9.5",
4+
"server": "45.79.155.229:8021",
5+
"ticket_bytes": 123,
6+
"server_issued_ticket_matches_cache": true,
7+
"client_offered_exact_ticket": true,
8+
"tls12_abbreviated_handshake": true,
9+
"bidirectional_application_data": true,
10+
"streams": [
11+
4,
12+
5,
13+
6,
14+
7,
15+
8
16+
],
17+
"cached_ticket_streams": [
18+
4
19+
],
20+
"application_data_streams": [
21+
5,
22+
6,
23+
7,
24+
8
25+
]
26+
},
27+
{
28+
"release": "7.7.2",
29+
"server": "45.79.155.229:8021",
30+
"ticket_bytes": 123,
31+
"server_issued_ticket_matches_cache": true,
32+
"client_offered_exact_ticket": true,
33+
"tls12_abbreviated_handshake": true,
34+
"bidirectional_application_data": true,
35+
"streams": [
36+
10,
37+
11
38+
],
39+
"cached_ticket_streams": [
40+
10,
41+
11
42+
],
43+
"application_data_streams": [
44+
10,
45+
11
46+
]
47+
}
48+
]

‎go.mod‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module github.com/getlantern/http-proxy-lantern/v2
22

3-
go 1.24.2
3+
go 1.26.10
44

55
require (
66
cloud.google.com/go/errorreporting v0.3.0

0 commit comments

Comments
 (0)