From ce76c0ea852e9e81a59ab261ca6da2f897c74bd1 Mon Sep 17 00:00:00 2001 From: Max Lv Date: Fri, 11 Sep 2026 14:27:12 +0800 Subject: [PATCH 1/2] Document project history and shadowsocks-c rename --- README.md | 48 +++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 39 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index a42c34b7a..dbadf9e14 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # shadowsocks-c -[![Build Status](https://travis-ci.com/shadowsocks/shadowsocks-libev.svg?branch=master)](https://travis-ci.com/shadowsocks/shadowsocks-libev) [![Snap Status](https://snapcraft.io/shadowsocks-libev/badge.svg)](https://snapcraft.io/shadowsocks-libev) +[![Build](https://github.com/shadowsocks/shadowsocks-c/actions/workflows/build.yml/badge.svg?branch=master)](https://github.com/shadowsocks/shadowsocks-c/actions/workflows/build.yml) [![Tests](https://github.com/shadowsocks/shadowsocks-c/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/shadowsocks/shadowsocks-c/actions/workflows/tests.yml) [![Portability](https://github.com/shadowsocks/shadowsocks-c/actions/workflows/portability.yml/badge.svg?branch=master)](https://github.com/shadowsocks/shadowsocks-c/actions/workflows/portability.yml) ## Intro @@ -13,6 +13,41 @@ created by [@clowwindy](https://github.com/clowwindy), and maintained by Current version: 3.3.6 | [Changelog](debian/changelog) +## Project history and rename + +This repository began as **shadowsocks-libev**, the lightweight C implementation +of Shadowsocks built around the libev event loop. It later entered a bug-fix-only +maintenance phase, with new development directed toward +[shadowsocks-rust](https://github.com/shadowsocks/shadowsocks-rust). + +In September 2026, the C implementation was modernized with a focus on +self-contained builds and portability. [The build modernization](https://github.com/shadowsocks/shadowsocks-c/pull/3050) +bundled pinned dependency sources, removed several external dependencies, and +added static-build validation across platforms. +[The subsequent migration](https://github.com/shadowsocks/shadowsocks-c/pull/3051) +replaced libev with libuv, added Windows IOCP and macOS kqueue support, expanded +asynchronous runtime DNS coverage, and strengthened CI lint checks. + +The project and GitHub repository were renamed **shadowsocks-c** to reflect its +continuing identity as a pure C implementation. This repository retains the +shadowsocks-libev commit history and releases. The canonical repository is now +[shadowsocks/shadowsocks-c](https://github.com/shadowsocks/shadowsocks-c). + +### Compatibility with shadowsocks-libev + +- Commands such as `ss-local` and `ss-server`, the `shadowsocks.h` API, and the + embedding library ABI remain compatible. +- New builds provide `libshadowsocks-c` and the CMake/pkg-config package + `shadowsocks-c`. Legacy library filenames and the `shadowsocks-libev` package + lookup name remain available as compatibility aliases. +- Existing configuration paths, distribution package names, and service names + are retained. References to `shadowsocks-libev` in the installation examples + below refer to those existing integrations. + +See [the modernization notes](docs/modernization.md) for build options and +platform support, and [the performance measurements](docs/performance.md) for +measured tradeoffs. + ## Features shadowsocks-c is written in pure C and depends on [libuv](https://libuv.org/). It's designed @@ -21,16 +56,11 @@ to be a lightweight implementation of shadowsocks protocol, in order to keep the For a full list of feature comparison between different versions of shadowsocks, refer to the [Wiki page](https://github.com/shadowsocks/shadowsocks/wiki/Feature-Comparison-across-Different-Versions). -The project is now **shadowsocks-c**. Commands (`ss-local`, `ss-server`, etc.) -and the `shadowsocks.h` API remain compatible. New builds produce -`libshadowsocks-c` with CMake/pkg-config package `shadowsocks-c`; legacy library -filenames and package lookup names remain available as compatibility aliases. -Existing configuration paths, distro package names and service names are retained. -Repository links below still point to the current GitHub repository. - ## Quick Start -Snap is the recommended way to install the latest binaries. +The Snap commands below use the existing `shadowsocks-libev` package name. +Distribution packages may predate the modernization; to build the current +source, follow [the CMake instructions](#build-from-source-cmake). ### Install snap core From 3f3f6bf8e01e5614821d1e15fda3ad8f07812902 Mon Sep 17 00:00:00 2001 From: Max Lv Date: Fri, 11 Sep 2026 14:46:19 +0800 Subject: [PATCH 2/2] Add tested multi-platform Docker publishing and recommended installation --- .github/workflows/docker.yml | 104 +++++++++++++++++++++++++++++ README.md | 62 +++++++++++------ docker/static/Dockerfile | 7 ++ docker/static/README.md | 43 +++++++++++- tests/docker_smoke.py | 126 +++++++++++++++++++++++++++++++++++ 5 files changed, 322 insertions(+), 20 deletions(-) create mode 100644 .github/workflows/docker.yml create mode 100644 tests/docker_smoke.py diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 000000000..ddc71da93 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,104 @@ +name: docker + +on: + pull_request: + branches: [master] + push: + branches: [master] + tags: ['v*'] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: docker-${{ github.ref }} + cancel-in-progress: true + +env: + IMAGE: ghcr.io/shadowsocks/shadowsocks-c + +jobs: + image: + strategy: + fail-fast: false + matrix: + include: + - arch: amd64 + runner: ubuntu-24.04 + - arch: arm64 + runner: ubuntu-24.04-arm + runs-on: ${{ matrix.runner }} + timeout-minutes: 20 + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v6 + - uses: docker/setup-buildx-action@v4 + - name: Build tested static runtime image + uses: docker/build-push-action@v7 + with: + context: . + file: docker/static/Dockerfile + target: runtime + platforms: linux/${{ matrix.arch }} + load: true + tags: shadowsocks-c:test + labels: org.opencontainers.image.revision=${{ github.sha }} + cache-from: type=gha,scope=docker-${{ matrix.arch }} + cache-to: type=gha,scope=docker-${{ matrix.arch }},mode=max + - name: Exercise read-only, unprivileged runtime containers + run: python3 tests/docker_smoke.py --image shadowsocks-c:test + - name: Log in to GHCR + if: github.repository == 'shadowsocks/shadowsocks-c' && github.event_name != 'pull_request' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/v')) + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Publish tested architecture image + if: github.repository == 'shadowsocks/shadowsocks-c' && github.event_name != 'pull_request' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/v')) + env: + COMMIT: ${{ github.sha }} + ARCH: ${{ matrix.arch }} + run: | + docker tag shadowsocks-c:test "$IMAGE:sha-$COMMIT-$ARCH" + docker push "$IMAGE:sha-$COMMIT-$ARCH" + + publish: + needs: image + if: github.repository == 'shadowsocks/shadowsocks-c' && github.event_name != 'pull_request' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/v')) + runs-on: ubuntu-24.04 + permissions: + contents: read + packages: write + steps: + - uses: docker/setup-buildx-action@v4 + - uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Image tags + id: meta + uses: docker/metadata-action@v6 + with: + images: ${{ env.IMAGE }} + flavor: latest=false + tags: | + type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' }} + type=sha,format=long + type=semver,pattern={{version}} + - name: Publish multi-platform manifest + env: + COMMIT: ${{ github.sha }} + TAGS: ${{ steps.meta.outputs.tags }} + run: | + args=() + while IFS= read -r tag; do + if [ -n "$tag" ]; then args+=(--tag "$tag"); fi + done <<< "$TAGS" + docker buildx imagetools create "${args[@]}" \ + "$IMAGE:sha-$COMMIT-amd64" "$IMAGE:sha-$COMMIT-arm64" + docker buildx imagetools inspect "$IMAGE:sha-$COMMIT" diff --git a/README.md b/README.md index dbadf9e14..7886016f3 100644 --- a/README.md +++ b/README.md @@ -58,28 +58,54 @@ refer to the [Wiki page](https://github.com/shadowsocks/shadowsocks/wiki/Feature ## Quick Start -The Snap commands below use the existing `shadowsocks-libev` package name. -Distribution packages may predate the modernization; to build the current -source, follow [the CMake instructions](#build-from-source-cmake). +### Docker (recommended) -### Install snap core +Docker is the recommended way to run a server. The image contains the bundled, +fully static C binaries and supports Linux AMD64 and ARM64, including Linux +containers under Docker Desktop on macOS and Windows. -https://snapcraft.io/core +Create `config.json` and replace the example password with your own: -### Install from snapcraft.io +```json +{ + "server": "0.0.0.0", + "server_port": 8388, + "password": "replace-with-a-long-random-password", + "method": "aes-256-gcm", + "mode": "tcp_and_udp" +} +``` -Stable channel: +In a POSIX shell, start the server with the configuration mounted read-only: -```bash -sudo snap install shadowsocks-libev +```sh +docker pull ghcr.io/shadowsocks/shadowsocks-c:latest +docker run -d --name shadowsocks-c --restart unless-stopped \ + --user "$(id -u):$(id -g)" --read-only --cap-drop=ALL \ + --security-opt=no-new-privileges:true \ + -p 8388:8388/tcp -p 8388:8388/udp \ + --mount type=bind,src="$PWD/config.json",dst=/etc/shadowsocks-c/config.json,readonly \ + ghcr.io/shadowsocks/shadowsocks-c:latest ``` -Edge channel: +Using your user ID lets the container read a configuration file owned by you. +View logs with `docker logs shadowsocks-c`; stop it with `docker stop shadowsocks-c`. +Configure your Shadowsocks client with the server address, port, password and +method above. -```bash -sudo snap install shadowsocks-libev --edge +`latest` follows `master`; version tags and `sha-` tags identify +specific published builds. If the registry image is not yet available, build it +from this checkout with the same name, then run the command above without pulling: + +```sh +docker build -f docker/static/Dockerfile --target runtime \ + -t ghcr.io/shadowsocks/shadowsocks-c:latest . ``` +See [Docker image details](docker/static/README.md) for publishing, updates, +client mode and build options. Existing Snap packages still use the +`shadowsocks-libev` name and may predate this modernization. + ## Installation ### Distribution-specific guide @@ -318,13 +344,11 @@ The historical Autotools scripts in `docker/mingw` are superseded by this build. ### Docker -As you expect, simply pull the image and run. -``` -docker pull shadowsocks/shadowsocks-libev -docker run -e PASSWORD= -p:8388 -p:8388/udp -d shadowsocks/shadowsocks-libev -``` - -More information about the image can be found [here](docker/alpine/README.md). +Use the [recommended Docker installation](#docker-recommended) above. +The image is `ghcr.io/shadowsocks/shadowsocks-c`; it accepts a JSON configuration +file or the normal `ss-server` arguments. The historical `PASSWORD` environment +variable wrapper belongs to the older Docker Hub image and is not used here. +See [image and build details](docker/static/README.md). ## Usage diff --git a/docker/static/Dockerfile b/docker/static/Dockerfile index 69e5c2c9c..fff10cd2a 100644 --- a/docker/static/Dockerfile +++ b/docker/static/Dockerfile @@ -30,5 +30,12 @@ COPY --from=build /out/ / # No libc, dynamic loader, package manager or shell in the runtime image. FROM scratch AS runtime +LABEL org.opencontainers.image.title="shadowsocks-c" \ + org.opencontainers.image.description="Self-contained Shadowsocks in C with libuv" \ + org.opencontainers.image.source="https://github.com/shadowsocks/shadowsocks-c" \ + org.opencontainers.image.licenses="GPL-3.0-or-later" COPY --from=build /out/ /usr/local/ +USER 65532:65532 +EXPOSE 8388/tcp 8388/udp ENTRYPOINT ["/usr/local/bin/ss-server"] +CMD ["-c", "/etc/shadowsocks-c/config.json"] diff --git a/docker/static/README.md b/docker/static/README.md index c8765c0f3..fa78caa2c 100644 --- a/docker/static/README.md +++ b/docker/static/README.md @@ -1,4 +1,45 @@ -# Fully static Linux builds +# Docker image and static Linux builds + +Use `ghcr.io/shadowsocks/shadowsocks-c` for Linux AMD64 or ARM64. The +[README quick start](../../README.md#docker-recommended) shows server configuration, +TCP/UDP port mapping and a read-only configuration mount. Docker Desktop runs +these Linux images on macOS and Windows. + +The default entrypoint is `ss-server -c /etc/shadowsocks-c/config.json`. +Passing arguments overrides that default configuration argument. The image runs +as UID/GID `65532:65532` unless `--user` is supplied; the mounted file must be +readable by that user. Logs go to standard output/error. No writable filesystem +is required for normal server operation. + +To update a running server, pull the desired tag, remove the old container with +`docker rm -f shadowsocks-c`, and repeat the quick-start run command. The host +configuration file is preserved. `latest` tracks `master`; choose a published +version tag or `sha-` to pin a build. + +For a SOCKS5 client, override the entrypoint with +`--entrypoint /usr/local/bin/ss-local`, supply a client configuration using `-c`, +and publish the configured local TCP/UDP port. Bind the local listener to +`0.0.0.0` inside the container and restrict its host mapping to loopback, such as +`-p 127.0.0.1:1080:1080/tcp -p 127.0.0.1:1080:1080/udp`. + +## Publishing + +`.github/workflows/docker.yml` builds each architecture on a native runner and +exercises the actual scratch server/client containers with concurrent TCP, +hostname resolution, UDP and clean shutdown. PRs run these checks without +publishing. Pushes to `master` publish `latest` and `sha-`; `v*` +version tags publish the version without the leading `v` and the commit tag. +The multi-platform tag is assembled only after both architecture tests pass. +Publishing uses the repository's `GITHUB_TOKEN` with `packages: write`. + +The first image becomes available after this workflow lands on `master` and +completes. GitHub initially creates packages as private: a package administrator +must set `shadowsocks-c` to **Public** in its package settings before anonymous +pulls work. This is a one-time registry setting; see +[GitHub's container registry documentation](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#pushing-container-images). +Until then, the quick start also provides a local image build command. + +## Build locally The Alpine/musl builder uses Clang with LLD by default and links both third-party dependencies and libc statically. `WITH_STATIC=ON` alone only selects static third-party libraries; this image also diff --git a/tests/docker_smoke.py b/tests/docker_smoke.py new file mode 100644 index 000000000..cdf42f528 --- /dev/null +++ b/tests/docker_smoke.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +"""Exercise the scratch server/client images with real TCP and UDP traffic.""" +import argparse +import base64 +import concurrent.futures +import json +import os +import socket +import time +from pathlib import Path +import subprocess +import tempfile +import threading +import uuid + +import interop + + +def inside(): + # All three containers share an isolated network namespace. Only loopback + # is needed, including for the server's localhost hostname resolution. + for port in (8388, 1080): + deadline = time.monotonic() + 15 + while True: + try: + with socket.create_connection(('127.0.0.1', port), timeout=.2): + break + except OSError: + if time.monotonic() >= deadline: + raise AssertionError(f'container failed to listen on {port}') + time.sleep(.05) + with interop.TCPOrigin(('127.0.0.1', 0), interop.TCPHandler) as tcp, \ + interop.UDPOrigin(('127.0.0.1', 0), interop.UDPHandler) as udp: + for origin in (tcp, udp): + threading.Thread(target=origin.serve_forever, daemon=True).start() + try: + with concurrent.futures.ThreadPoolExecutor(max_workers=3) as pool: + futures = [pool.submit(interop.tcp_case, 1080, tcp.server_address[1], size) + for size in (1, 65536, 1048576)] + for future in futures: + future.result(timeout=30) + interop.udp_case(1080, udp.server_address[1]) + finally: + tcp.shutdown() + udp.shutdown() + print('PASS scratch image: concurrent TCP, hostname resolution, and UDP', flush=True) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--image', default='shadowsocks-c:test') + parser.add_argument('--python-image', default='python:3.12-alpine') + parser.add_argument('--inside', action='store_true', help=argparse.SUPPRESS) + args = parser.parse_args() + if args.inside: + inside() + return + root = Path(__file__).resolve().parent + name = 'ss-docker-test-' + uuid.uuid4().hex[:12] + helper = name + '-helper' + with tempfile.TemporaryDirectory(prefix='ss-docker-test-') as directory: + context = Path(directory) + for filename in ('interop.py', 'docker_smoke.py'): + (context / filename).write_bytes((root / filename).read_bytes()) + (context / 'Dockerfile').write_text( + 'ARG PYTHON_IMAGE=python:3.12-alpine\nFROM ${PYTHON_IMAGE}\n' + 'COPY interop.py docker_smoke.py /tests/\n' + 'USER 65532:65532\nENTRYPOINT ["python3", "/tests/docker_smoke.py", "--inside"]\n') + subprocess.run(['docker', 'build', '--build-arg', 'PYTHON_IMAGE=' + args.python_image, + '-t', helper, str(context)], check=True) + try: + for method in ('aes-256-gcm', '2022-blake3-aes-128-gcm'): + containers = [] + volume = name + '-config' + key = base64.b64encode(os.urandom(16)).decode('ascii') + try: + common = ['docker', 'run', '-d', '--read-only', '--cap-drop=ALL', + '--security-opt=no-new-privileges:true'] + server = name + '-server' + client = name + '-client' + subprocess.run(['docker', 'volume', 'create', volume], check=True, + stdout=subprocess.DEVNULL) + config = json.dumps({'server': '0.0.0.0', 'server_port': 8388, + 'password': key, 'method': method, 'mode': 'tcp_and_udp'}) + subprocess.run(['docker', 'run', '--rm', '-i', '--network', 'none', + '--user', '0:0', '--entrypoint', 'python3', + '--mount', 'type=volume,src=' + volume + ',dst=/config', helper, + '-c', 'import pathlib,sys; p=pathlib.Path("/config/config.json"); ' + 'p.write_text(sys.stdin.read()); p.chmod(0o644)'], + input=config, text=True, check=True) + subprocess.run(common + ['--name', server, '--network', 'none', + '--mount', 'type=volume,src=' + volume + + ',dst=/etc/shadowsocks-c,readonly', args.image], check=True) + containers.append(server) + subprocess.run(common + ['--name', client, '--network', 'container:' + server, + '--entrypoint', '/usr/local/bin/ss-local', args.image, + '-s', '127.0.0.1', '-p', '8388', '-l', '1080', '-k', key, + '-m', method, '-u'], check=True) + containers.append(client) + # The helper waits for both listeners before generating traffic. + subprocess.run(['docker', 'run', '--rm', '--network', 'container:' + server, + '--read-only', '--cap-drop=ALL', helper], check=True, timeout=90) + for container in containers: + assert subprocess.check_output(['docker', 'inspect', '--format', + '{{.State.Running}}', container], text=True).strip() == 'true' + for container in reversed(containers): + subprocess.run(['docker', 'stop', '--time', '5', container], check=True, + stdout=subprocess.DEVNULL) + code = subprocess.check_output(['docker', 'inspect', '--format', + '{{.State.ExitCode}}', container], text=True).strip() + assert code == '0', f'{container}: unclean shutdown ({code})' + print('PASS runtime image: ' + method, flush=True) + except BaseException: + for container in containers: + subprocess.run(['docker', 'logs', container], check=False) + raise + finally: + for container in reversed(containers): + subprocess.run(['docker', 'rm', '-f', container], check=False, stdout=subprocess.DEVNULL) + subprocess.run(['docker', 'volume', 'rm', volume], check=False, stdout=subprocess.DEVNULL) + finally: + subprocess.run(['docker', 'image', 'rm', helper], check=False, stdout=subprocess.DEVNULL) + + +if __name__ == '__main__': + main()