feat: Enhance ECH configuration with public IP resolution and A record management
Kim committed
May 7, 2026 at 11:22 UTC
382ecf2543bec3ff632276cdad86a5a542f27f7b
3 files changed
+109
.github/workflows/branch-artifacts.yml
new
+77
@@ -0,0 +1,77 @@
1
+name: Branch Artifacts
2
+
3
+permissions:
4
+ contents: read
5
+
6
+on:
7
+ workflow_dispatch:
8
+ inputs:
9
+ ref:
10
+ description: Branch, tag, or commit SHA to build
11
+ required: true
12
+ default: feature/encrypted-clienthello
13
+
14
+concurrency:
15
+ group: branch-artifacts-${{ inputs.ref }}
16
+ cancel-in-progress: true
17
+
18
+jobs:
19
+ build:
20
+ name: Build Binaries
21
+ runs-on: ubuntu-latest
22
+
23
+ steps:
24
+ - name: Checkout selected ref
25
+ uses: actions/checkout@v6
26
+ with:
27
+ ref: ${{ inputs.ref }}
28
+
29
+ - name: Set up Go
30
+ uses: actions/setup-go@v6
31
+ with:
32
+ go-version-file: go.mod
33
+ cache: true
34
+
35
+ - name: Prepare artifact metadata
36
+ id: meta
37
+ shell: bash
38
+ run: |
39
+ safe_ref="$(printf '%s' "${{ inputs.ref }}" | tr '/:@' '---' | tr -cd 'A-Za-z0-9._-')"
40
+ if [ -z "$safe_ref" ]; then
41
+ safe_ref="selected-ref"
42
+ fi
43
+ echo "safe_ref=$safe_ref" >> "$GITHUB_OUTPUT"
44
+ echo "short_sha=$(git rev-parse --short=12 HEAD)" >> "$GITHUB_OUTPUT"
45
+
46
+ - name: Build binaries
47
+ shell: bash
48
+ run: |
49
+ mkdir -p release
50
+
51
+ make build-tunnel
52
+ cp cmd/relay-server/dist/tunnel/portal-* release/
53
+
54
+ for GOARCH in amd64 arm64; do
55
+ out="release/relay-server-linux-${GOARCH}"
56
+ echo " - ${out}"
57
+ CGO_ENABLED=0 GOOS=linux GOARCH="${GOARCH}" go build -trimpath -ldflags "-s -w" -o "${out}" ./cmd/relay-server
58
+ done
59
+
60
+ cp cmd/portal-tunnel/installer/install.sh release/install.sh
61
+ cp cmd/portal-tunnel/installer/install.ps1 release/install.ps1
62
+
63
+ - name: Generate SHA256 checksums
64
+ shell: bash
65
+ run: |
66
+ cd release
67
+ sha256sum * > checksums.txt
68
+ while read -r sum file; do
69
+ printf '%s %s\n' "$sum" "$file" > "${file}.sha256"
70
+ done < checksums.txt
71
+
72
+ - name: Upload artifact
73
+ uses: actions/upload-artifact@v4
74
+ with:
75
+ name: portal-${{ steps.meta.outputs.safe_ref }}-${{ steps.meta.outputs.short_sha }}
76
+ path: release/*
77
+ if-no-files-found: error
\ No newline at end of file
portal/acme/acme.go
+25
@@ -483,6 +483,14 @@ func (m *Manager) SyncECHConfig(ctx context.Context, hostname string, echConfigL
483
m.echRecords[hostname] = record
484
m.echMu.Unlock()
485
486
+ publicIP, err := utils.ResolvePublicIPv4(ctx)
487
+ if err != nil {
488
+ return fmt.Errorf("detect public ip for ECH hostname %s: %w", hostname, err)
489
+ }
490
+ if err := m.dns.EnsureARecord(ctx, hostname, publicIP); err != nil {
491
+ return fmt.Errorf("ensure ECH A record for %s: %w", hostname, err)
492
+ }
493
+
494
if err := m.dns.EnsureHTTPSRecord(ctx, hostname, record.Priority, record.Target, svcParams, content); err != nil {
495
return err
496
}
@@ -511,6 +519,11 @@ func (m *Manager) DeleteECHConfig(ctx context.Context, hostname string) error {
519
if err := m.dns.DeleteHTTPSRecord(ctx, hostname); err != nil {
520
return err
521
}
522
+ if hostname != m.cfg.BaseDomain {
523
+ if err := m.dns.DeleteARecord(ctx, hostname); err != nil {
524
+ return fmt.Errorf("delete ECH A record for %s: %w", hostname, err)
525
+ }
526
+ }
527
return nil
528
}
529
@@ -527,7 +540,19 @@ func (m *Manager) syncECHRecords(ctx context.Context) error {
540
m.echMu.Unlock()
541
542
var syncErr error
543
+ publicIP := ""
544
+ if len(records) > 0 {
545
+ var err error
546
+ publicIP, err = utils.ResolvePublicIPv4(ctx)
547
+ if err != nil {
548
+ return fmt.Errorf("detect public ip for ECH records: %w", err)
549
+ }
550
+ }
551
for hostname, record := range records {
552
+ if err := m.dns.EnsureARecord(ctx, hostname, publicIP); err != nil {
553
+ syncErr = errors.Join(syncErr, fmt.Errorf("ensure ECH A record for %s: %w", hostname, err))
554
+ continue
555
+ }
556
content, err := record.Content()
557
if err != nil {
558
syncErr = errors.Join(syncErr, fmt.Errorf("build ECH HTTPS record for %s: %w", hostname, err))
utils/cmd.go
+7
@@ -305,6 +305,13 @@ func normalizeFlagArgs(fs *flag.FlagSet, args []string) []string {
305
}
306
boolValue, ok := flagDef.Value.(boolFlagValue)
307
if ok && boolValue.IsBoolFlag() {
308
+ if i+1 < len(args) {
309
+ next := strings.TrimSpace(args[i+1])
310
+ if _, err := strconv.ParseBool(next); err == nil {
311
+ flags[len(flags)-1] = args[i] + "=" + next
312
+ i++
313
+ }
314
+ }
315
continue
316
}
317
if i+1 >= len(args) {