Skip to content

Conversation

@lunarthegrey
Copy link

This PR adds WebSocket transport support for Shadowsocks connections, enabling SS over WSS to bypass restrictive network filters. It fixes #1676 where much of the work was discussed.

Features

WebSocket Transport Support

  • New listener-based API model allowing access keys to specify WebSocket listeners alongside traditional Shadowsocks
  • WebSocket server automatically enabled when a WebSocket access key is created
  • GET /access-keys/{id} returns YAML configuration for WebSocket-enabled keys (Outline Client v1.15.0+)
  • Supports both TCP over WebSocket (websocket-stream) and UDP over WebSocket (websocket-packet)

Embedded OutlineCaddy Server

  • New outline_caddy_server.ts module for managing Caddy as the WebSocket reverse proxy
  • YAML-based Caddyfile configuration with automatic HTTPS via ACME
  • API proxy support allowing the management API to be accessed via TLS-terminated Caddy
  • Internal API proxy uses HTTPS with self-signed cert verification skipped

API Changes

New Endpoints

  • PUT /server/listeners-for-new-access-keys - Configure listener types for new keys
  • PUT /server/web-server - Configure embedded Caddy web server

Modified Endpoints

  • GET /access-keys/{id} - Returns JSON for traditional keys, YAML for WebSocket keys
  • POST /access-keys - Now accepts listeners array parameter

AccessKey Schema

  • Added optional listeners field (tcp, udp, websocket-stream, websocket-packet)
  • Added optional dynamicConfig field for WebSocket transport configuration
  • password, port, method, accessUrl are now optional (omitted for WSS-only keys)

Build & CI Changes

Docker Build Workflow

  • Rewrote multi-arch build to use Task-based builds instead of Docker buildx matrix
  • Added Node.js, Go, and Task setup steps
  • Builds both amd64 and arm64 images sequentially
  • Creates and pushes multi-arch manifest

GitHub Actions Concurrency

  • Added workflow-specific prefixes to concurrency groups to prevent cross-workflow cancellation
    • build-and-test-* for build workflow
    • license-* for license checks

Taskfile Changes

  • Added download_xcaddy task for cross-platform xcaddy downloads
  • Build task now includes OutlineCaddy binary built via xcaddy with plugins:
    • outlinecaddy@v0.0.1
    • caddy_yaml_adapter
    • caddy-l4@v0.0.0-20251201210923-0c96591f5650

Dependencies

Go Dependencies (go.mod, go.sum)

  • Updated prometheus/client_golang to v1.20.5
  • Updated prometheus/common to v0.62.0
  • Updated oschwald/geoip2-golang to v1.11.0
  • Updated golang.org/x/crypto to v0.32.0
  • Updated golang.org/x/sync to v0.11.0
  • Updated google.golang.org/protobuf to v1.36.4
  • Bumped outline-ss-server to v1.9.2

Documentation

README.md

  • Added "WebSocket Support (SS over WSS)" section with usage examples
  • Documented listener configuration API
  • Added YAML response example for WebSocket keys

api.yml

  • Full OpenAPI documentation for new endpoints
  • Updated AccessKey schema with optional SS fields
  • Documented dual response types (JSON/YAML) for GET /access-keys/{id}

Usage Examples

### Configure WebSocket Listeners

curl --insecure -X PUT -H "Content-Type: application/json" \
  -d '{
    "tcp": {"port": 443},
    "udp": {"port": 443},
    "websocketStream": {"path": "/tcp", "webServerPort": 8080},
    "websocketPacket": {"path": "/udp", "webServerPort": 8080}
  }' \
  $API_URL/server/listeners-for-new-access-keys

### Enable Caddy Web Server

curl --insecure -X PUT -H "Content-Type: application/json" \
  -d '{
    "enabled": true,
    "autoHttps": true,
    "email": "admin@example.com",
    "domain": "your-domain.com"
  }' \
  $API_URL/server/web-server

### Create WebSocket-Enabled Access Key

curl --insecure -X POST -H "Content-Type: application/json" \
  -d '{"name": "WSS User", "listeners": ["tcp", "udp", "websocket-stream", "websocket-packet"]}' \
  $API_URL/access-keys

### Get WebSocket Key Config (returns YAML)

curl --insecure $API_URL/access-keys/1

…y include them in the API response based on listener types.
…nt and remove dedicated dynamic config endpoint.
Copy link
Collaborator

@fortuna fortuna left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's simplify how we build outilnecaddy so it's easier to reason about and there's less to maintain. I recommend putting it in a standalone PR so it's not blocked on the rest of the PR

vars: [OUTPUT_BASE]

tasks:
download_xcaddy:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't you just call go run github.com/caddyserver/xcaddy/cmd/xcaddy?

The way we do this is to add it as a tool, like we do here:
https://github.com/Jigsaw-Code/outline-sdk/blob/fa686d9463ac56af75aa846f0b446547e73a38b7/go.mod#L53

Then you can simply call go tool xcaddy, and we get the proper version pinning in the go.mod/sum

That sounds a lot simpler and removes all this code we need to maintain. Can you do the go tool approach instead? It can be in its own PR to speed up the review/approval process.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, will change this and test it out.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about this? 739d2ec

vars: {TARGET_DIR: '{{.BIN_DIR}}'}
# Set CGO_ENABLED=0 to force static linkage. See https://mt165.co.uk/blog/static-link-go/.
- GOOS={{.TARGET_OS}} GOARCH={{.GOARCH}} CGO_ENABLED=0 go build -ldflags='-s -w -X main.version=embedded' -o '{{.BIN_DIR}}/' github.com/Jigsaw-Code/outline-ss-server/cmd/outline-ss-server
- task: download_xcaddy
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove the download and call go tool xcaddy

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, will change this.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator

@fortuna fortuna left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we are ready to accept this PR. We need to understand how this will play with dynamic access keys. With dynamic keys we don't need to set things for "new keys". We can do it for all keys, simplifying the mental model.

We may want a separate API for dynamic keys, so this needs more discussion.

I encourage you to send the build & CI changes as a PR though.

@lunarthegrey
Copy link
Author

lunarthegrey commented Dec 15, 2025

I don't think we are ready to accept this PR. We need to understand how this will play with dynamic access keys. With dynamic keys we don't need to set things for "new keys". We can do it for all keys, simplifying the mental model.

We may want a separate API for dynamic keys, so this needs more discussion.

I encourage you to send the build & CI changes as a PR though.

I believe I've fixed all the prior issues now, besides the dynamic access key concerns.

I hear your feedback though, and I'm thinking more about it. I'll come up with something for your review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Shadowsocks over WebSocket (SS over WSS) Support

2 participants