mirror of
https://github.com/shadowsocks/shadowsocks-libev.git
synced 2026-11-03 16:45:53 +00:00
Add Docker publishing, recommended installation and project history (#3052)
Document the shadowsocks-libev history and shadowsocks-c rename, and recommend Docker installation with mounted configuration and TCP/UDP port mappings. Add native AMD64/ARM64 builds of the unprivileged static runtime image, real container relay and shutdown tests, and GHCR publication after both architecture checks pass. All 24 build/test checks pass at reviewed head 3f3f6bf8e01e5614821d1e15fda3ad8f07812902; publication is intentionally skipped on pull requests.
This commit is contained in:
@@ -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"
|
||||
@@ -1,6 +1,6 @@
|
||||
# shadowsocks-c
|
||||
|
||||
[](https://travis-ci.com/shadowsocks/shadowsocks-libev) [](https://snapcraft.io/shadowsocks-libev)
|
||||
[](https://github.com/shadowsocks/shadowsocks-c/actions/workflows/build.yml) [](https://github.com/shadowsocks/shadowsocks-c/actions/workflows/tests.yml) [](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,35 +56,56 @@ 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.
|
||||
### 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
|
||||
|
||||
Stable channel:
|
||||
|
||||
```bash
|
||||
sudo snap install shadowsocks-libev
|
||||
```json
|
||||
{
|
||||
"server": "0.0.0.0",
|
||||
"server_port": 8388,
|
||||
"password": "replace-with-a-long-random-password",
|
||||
"method": "aes-256-gcm",
|
||||
"mode": "tcp_and_udp"
|
||||
}
|
||||
```
|
||||
|
||||
Edge channel:
|
||||
In a POSIX shell, start the server with the configuration mounted read-only:
|
||||
|
||||
```bash
|
||||
sudo snap install shadowsocks-libev --edge
|
||||
```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
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
`latest` follows `master`; version tags and `sha-<full-commit>` 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
|
||||
@@ -288,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=<password> -p<server-port>:8388 -p<server-port>: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
|
||||
|
||||
|
||||
@@ -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"]
|
||||
|
||||
+42
-1
@@ -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-<full-commit>` 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-<full-commit>`; `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
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user