PHPackages                             rekryt/amphp-vless - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Queues &amp; Workers](/categories/queues)
4. /
5. rekryt/amphp-vless

ActiveLibrary[Queues &amp; Workers](/categories/queues)

rekryt/amphp-vless
==================

Async VLESS client for PHP based on Amp.

v1.1.1(today)01↑2900%MITPHPPHP &gt;=8.2CI passing

Since Aug 6Pushed todayCompare

[ Source](https://github.com/rekryt/amphp-vless)[ Packagist](https://packagist.org/packages/rekryt/amphp-vless)[ Docs](https://github.com/rekryt/amphp-vless)[ RSS](/packages/rekryt-amphp-vless/feed)WikiDiscussions master Synced today

READMEChangelog (2)Dependencies (19)Versions (4)Used By (0)

amphp/vless
===========

[](#amphpvless)

[![License](https://camo.githubusercontent.com/942e017bf0672002dd32a857c95d66f28c5900ab541838c6c664442516309c8a/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d626c75652e7376673f7374796c653d666c61742d737175617265)](LICENSE)

**English** · [Русский](README.ru.md)

An asynchronous **VLESS** client for PHP, built on [AMPHP v3](https://amphp.org).

It speaks the protocol itself. No `sing-box` or `xray` running alongside, no local SOCKS port, no `exec`, no FFI. The only extension it needs is `openssl`, for TLS.

The integration point is `Amp\Socket\SocketConnector`, so anything in the AMPHP ecosystem that accepts a connector works through the tunnel unchanged — HTTP clients, connection pools, WebSocket clients, or plain socket code.

```
use Amp\Vless\VlessConnector;

$connector = VlessConnector::fromUri('vless://@example.org:443?security=tls&type=ws&path=%2Fws%2F');

$socket = $connector->connect('tcp://one.one.one.one:80');
$socket->write("GET / HTTP/1.1\r\nHost: one.one.one.one\r\nConnection: close\r\n\r\n");

echo Amp\ByteStream\buffer($socket);
```

Installation
------------

[](#installation)

```
composer require rekryt/amphp-vless
```

Requires PHP 8.2 or newer.

Making HTTP requests
--------------------

[](#making-http-requests)

`amphp/http-client` needs no modification — only its connector changes.

```
use Amp\Http\Client\Connection\DefaultConnectionFactory;
use Amp\Http\Client\Connection\UnlimitedConnectionPool;
use Amp\Http\Client\HttpClientBuilder;
use Amp\Http\Client\Request;

$client = (new HttpClientBuilder())
    ->usingPool(new UnlimitedConnectionPool(new DefaultConnectionFactory($connector)))
    ->build();

$response = $client->request(new Request('https://example.com/'));

echo $response->getBody()->buffer();
```

HTTPS works end to end: TLS is negotiated with the destination across the tunnel, so certificate verification still protects the caller, and HTTP/2 is negotiated where the server offers it.

Configuration
-------------

[](#configuration)

Three ways to describe the same server, whichever suits the surrounding system.

```
// A share link, as a client hands it out.
$config = VlessConfig::fromUri('vless://@example.org:443?security=tls&sni=example.org&type=ws&path=%2Fws%2F');

// An array laid out like a sing-box outbound.
$config = VlessConfig::fromArray([
    'server' => 'example.org',
    'server_port' => 443,
    'uuid' => '',
    'tls' => ['enabled' => true, 'server_name' => 'example.org'],
    'transport' => ['type' => 'ws', 'path' => '/ws/'],
]);

// Assembled in code.
$config = new VlessConfig(
    server: 'example.org',
    port: 443,
    uuid: '',
    tls: true,
    transport: VlessConfig::TRANSPORT_WS,
    path: '/ws/',
);

$connector = new VlessConnector($config);
```

### Transports

[](#transports)

TLS is a separate switch, not part of the transport, so each of these works with it and without it — six combinations, none of them a special case.

`type`Also writtenSupported`tcp``raw`yes — direct, TLS optional`ws``websocket`yes — the most common deployment`httpupgrade`yes — bare HTTP upgrade, no framing`xhttp``splithttp`yes — all three modes`grpc`yes`mkcp``kcp`no`quic`, `h3`no`http`, `h2`no — and removed from Xray itselfXray renamed `tcp` to `raw` and accepts both spellings, as it does `ws` and `websocket`; links from a recent panel carry the newer names, and both forms are accepted here. An unsupported transport is refused at configuration time with an exception that says so, distinct from the one raised for a link that is simply malformed.

`xhttp` and `grpc` both run over HTTP/2, and the HTTP/2 client is this package's own. An HTTP client cannot serve here: it holds the response back until the request body is complete, and a tunnel's request body never completes.

XHTTP supports all three modes — `packet-up` (the default), `stream-up` and `stream-one` — selected by `mode=` in the link.

**What is left out.** `mkcp` is a reliability layer over UDP and would have to be written outright, with PHP's per-packet overhead counting against exactly the case it exists for. `quic` and `h3` need QUIC, which PHP does not have and cannot get: QUIC embeds the TLS 1.3 handshake, and driving a TLS handshake by hand is the same wall REALITY runs into.

UDP
---

[](#udp)

Datagrams travel to a fixed destination inside one tunnel.

```
$socket = $connector->connectDatagram('udp://1.1.1.1:53');

$socket->send($dnsQuery);
$answer = $socket->receive();
```

Worth knowing what this is: the tunnel is a TCP connection, and each datagram travels inside it behind a two-byte length prefix that restores the message boundary. Delivery and ordering therefore come from TCP — a datagram cannot be individually lost or reordered the way real UDP allows.

What cannot be supported
------------------------

[](#what-cannot-be-supported)

Two VLESS features are out of reach in PHP, and the reason is structural rather than a matter of effort. Both are refused at configuration time, with an explanation, rather than quietly degrading to plain TLS — a caller who believes their traffic is disguised should not be left believing it.

**`flow: xtls-rprx-vision`** needs the TLS record buffers and the raw socket underneath an established TLS session. The reference implementation reaches them through reflection and unsafe pointer arithmetic into Go's `crypto/tls.Conn`private fields. PHP exposes TLS as a stream filter inside OpenSSL; neither the decrypted buffers nor the socket below them are reachable.

**REALITY** needs the client to build a TLS ClientHello, stop before sending it, overwrite bytes at a fixed offset inside the serialised message, and then carry on with the handshake. Go's own `crypto/tls` cannot do that either — the reference implementation reaches for [uTLS](https://github.com/refraction-networking/utls), a fork of it. In PHP it would mean writing TLS 1.3 from scratch.

### What that costs you

[](#what-that-costs-you)

Worth knowing before you choose this library, because it decides whether it is the right tool.

**Without Vision, your traffic is TLS inside TLS** — an outer session with the server and an inner one with the destination. The double wrapping is visible to traffic analysis through record sizes and timing, whatever is inside it. Not adding that second layer is precisely what Vision exists to do.

**Without REALITY you need a real domain with a real certificate**, and the server can be identified as a proxy endpoint by active probing — which is the thing REALITY hides, by serving an unrecognised client somebody else's genuine site instead.

And a hypothetical implementation would not help: REALITY's protection rests on the ClientHello being byte-for-byte indistinguishable from a real browser's, which is why the reference client mimics specific browser fingerprints. PHP with OpenSSL has an OpenSSL fingerprint; a hand-rolled one would be unique, and therefore a beacon rather than a disguise.

So this library fits:

An API blocked by address or domainyesA database or cache that only accepts the VPNyesBlocking by protocol signatureyesDPI doing statistical traffic analysis**no** — use sing-box or xrayThat last row is a boundary, not an excuse. The external process this project exists to avoid remains the only answer to active traffic analysis, and claiming otherwise would be the same deception as silently downgrading REALITY to plain TLS.

`vless-proxy` — the tunnel for programs that are not PHP
--------------------------------------------------------

[](#vless-proxy--the-tunnel-for-programs-that-are-not-php)

Most of the value is in letting everything else on the machine use the tunnel. The package ships an executable that presents it as an ordinary local proxy, so `curl`, a browser, `git`, `ssh` or anything else needs no knowledge of VLESS — only a proxy setting.

```
vendor/bin/vless-proxy --uri='vless://@example.org:443?security=tls&type=ws&path=%2Fws%2F'
```

With no `--uri` it reads `VLESS_URI` from the environment, which keeps the credential out of your shell history and out of `ps`.

For anything beyond a quick check, prefer the file form. A `vless://` link and a proxy password are both credentials, and an environment variable is a poor place for one: it is readable through `docker inspect`, through `/proc//environ`, and in whatever an orchestrator logs about the process.

```
VLESS_URI_FILE=/run/secrets/vless_uri \
VLESS_PROXY_PASSWORD_FILE=/run/secrets/proxy_password \
    vendor/bin/vless-proxy --socks
```

Every setting below has a `_FILE` form read from the file it names — the convention Docker and Kubernetes secrets already use.

### Options

[](#options)

OptionMeaning`--uri=`The server.`--socks[=addr]`Run a SOCKS5 proxy. Default `127.0.0.1:1080`.`--http[=addr]`Run an HTTP proxy. Default `127.0.0.1:8080`.`--user=`Require this username from clients.`--password=`…and this password. Both or neither.`--timeout=`Connect timeout for each tunnel. Default `20`.`--quiet`Log failures only.`--help`Show the usage text.Give either proxy, or both. With neither, a SOCKS5 proxy starts on the default address.

Every option can come from the environment instead, which is how the container image is configured. Command-line options win where both are given.

VariableSame asFile form`VLESS_URI``--uri``VLESS_URI_FILE``VLESS_PROXY_SOCKS``--socks``VLESS_PROXY_SOCKS_FILE``VLESS_PROXY_HTTP``--http``VLESS_PROXY_HTTP_FILE``VLESS_PROXY_USER``--user``VLESS_PROXY_USER_FILE``VLESS_PROXY_PASSWORD``--password``VLESS_PROXY_PASSWORD_FILE``VLESS_PROXY_TIMEOUT``--timeout``VLESS_PROXY_TIMEOUT_FILE``VLESS_PROXY_QUIET``--quiet``VLESS_PROXY_QUIET_FILE`### Requiring a username and password

[](#requiring-a-username-and-password)

Authentication is off until both `--user` and `--password` are given, and then it applies to both proxies — SOCKS5 through its username/password sub-negotiation (RFC 1929), HTTP through `Proxy-Authorization` and a `407`challenge (RFC 7235).

```
vendor/bin/vless-proxy --socks --http --user=alice --password="$PROXY_PASSWORD"
```

```
curl --socks5-hostname alice:secret@127.0.0.1:1080 https://example.com/
curl -x http://alice:secret@127.0.0.1:8080 https://example.com/
```

Half a pair is refused at startup rather than ignored: a deployment that set a username and lost the password would otherwise run wide open while looking configured. The password is compared in constant time, and it is never forwarded to the destination — a proxy credential is between the client and the proxy and nobody else.

`--password` puts the secret in `ps`, where every user on the machine can read it. It is there for a quick check; use `VLESS_PROXY_PASSWORD_FILE` for anything that stays running.

A refused attempt is answered after a delay that doubles with each consecutive failure from the same address, up to eight seconds, and resets on success. That turns an unlimited guess rate into a bounded one, which is what a human-chosen password needs to survive an exposed port.

Two further limits apply whether or not credentials are set: a client has ten seconds to say what it wants before the connection is dropped, and connections past 512 at once are refused rather than queued. Both exist because neither costs an attacker anything otherwise — the negotiation read happens before any credential is checked.

### Using it

[](#using-it)

```
# Both doors at once.
vendor/bin/vless-proxy --socks --http

# SOCKS5 reachable from the local network, HTTP kept to this machine.
vendor/bin/vless-proxy --socks=0.0.0.0:1080 --http=127.0.0.1:8080
```

```
curl --socks5-hostname 127.0.0.1:1080 https://example.com/
http_proxy=http://127.0.0.1:8080 curl http://example.com/
https_proxy=http://127.0.0.1:8080 curl https://example.com/
git -c http.proxy=http://127.0.0.1:8080 clone https://example.com/repo.git
ssh -o ProxyCommand='nc -X 5 -x 127.0.0.1:1080 %h %p' user@host
```

Note `--socks5-hostname` rather than `--socks5`: it has the proxy resolve the name, so the destination is never looked up on this machine. Plain `--socks5`resolves locally and announces where you are going before the connection is made.

**Which door.** SOCKS5 is the wider one — it carries any protocol, and `ssh -D`, browsers and most networked software speak it directly. The HTTP proxy exists for software that only understands `http_proxy`; it handles both absolute-form requests and `CONNECT`, so HTTPS goes through it too.

**`allowInsecure` in a link.** A link may carry `allowInsecure=1`, which turns off certificate verification — the only check that proves the far end is the server the link named. It still works, because a self-signed certificate on a private stand is a real use, but the process says so loudly at startup every time. Traffic on such a tunnel can be read and changed by anyone on the path, whatever `security=tls` in the link suggests.

**Control characters in a link.** A link whose `path`, `host`, `sni` or `serviceName` contains one is refused at configuration time. Those values are written into an HTTP/1.1 handshake, where a newline ends a header and starts another — a crafted link could otherwise append headers or a whole second request. This matters because links arrive from subscriptions, not only from your own notes.

**Binding to `0.0.0.0`.** Without credentials this makes the tunnel available to everyone who can reach the port, and an open proxy on a routable address is found by scanners within hours. Set `--user` and `--password`, or keep the bind on `127.0.0.1`. The process prints a loud warning at startup when it is listening beyond loopback with no credentials set.

Docker
------

[](#docker)

The repository ships a `Dockerfile` and a compose template, so the proxy can run as a service without PHP installed on the host.

```
cp docker-compose.example.yml docker-compose.yml
$EDITOR docker-compose.yml          # at minimum, set VLESS_URI
docker compose up -d
```

```
curl --socks5-hostname 127.0.0.1:1080 https://ifconfig.me/ip
curl -x http://127.0.0.1:8080 https://ifconfig.me/ip
```

Nothing is baked into the image but the code. The server, the ports and the credentials all arrive as the environment variables listed above, so one image serves every configuration and no `.env` file is involved — the container reads the process environment only.

`docker-compose.yml` is gitignored; the example is the template and carries no real values. A `vless://` link is a credential, and so is the proxy password.

Two things the template sets deliberately:

- **Ports are published to `127.0.0.1`.** `"127.0.0.1:1080:1080"` means only this machine can connect, whatever the container binds internally. Dropping the prefix serves other hosts — set `VLESS_PROXY_USER` and `VLESS_PROXY_PASSWORD` before you do.
- **The container binds `0.0.0.0`.** It has to, or the published port would never reach the process. What decides who can connect is the mapping above, not the bind.

Without compose:

```
docker build -t amphp-vless .
docker run --rm -p 127.0.0.1:1080:1080 \
    -e VLESS_URI='vless://@example.org:443?security=tls&type=ws&path=%2Fws%2F' \
    -e VLESS_PROXY_USER=alice \
    -e VLESS_PROXY_PASSWORD="$PROXY_PASSWORD" \
    amphp-vless
```

The image installs with `--no-dev`, so it carries the protocol implementation and nothing else: no test framework, no static analyser, no HTTP client. It runs as `nobody` on a read-only filesystem.

### Embedding it instead

[](#embedding-it-instead)

Both inbounds are ordinary classes, if a program should run one itself rather than shell out:

```
use Amp\Vless\Server\Socks5ProxyServer;

$server = new Socks5ProxyServer(new VlessConnector($config));
$server->listen('127.0.0.1:1080');
```

Two further shapes have examples of their own: [TCP port forwarding](examples/13-tcp-port-forward.php), which carries anything because it never interprets the bytes, and a [reverse proxy](examples/12-reverse-proxy.php), which serves one otherwise unreachable site at a local address.

Examples
--------

[](#examples)

Every claim above has a runnable example. Copy `.env.example` to `.env`, set `VLESS_TEST_URI`, then run any of them directly:

```
php examples/01-tcp-raw.php
```

Each prints what it demonstrates and what to expect, and each ends with the findings worth carrying away rather than only its output.

### What the library does

[](#what-the-library-does)

ExampleWhat it demonstrates[`01-tcp-raw.php`](examples/01-tcp-raw.php)The smallest thing here: a plain socket that happens to reach the internet from somewhere else. No HTTP client, no framework — bytes in, bytes out.[`02-http-client.php`](examples/02-http-client.php)HTTP and HTTPS through `amphp/http-client`, unmodified. Only the connector changes; HTTPS negotiates HTTP/2 end to end.[`03-verify-tunnel.php`](examples/03-verify-tunnel.php)Compares the address the internet sees with and without the tunnel. Inconclusive when your machine egresses through the same network as the server, and says so rather than claiming success.[`04-concurrent.php`](examples/04-concurrent.php)Eight requests at once against the same eight in sequence — the argument against running a proxy process beside the application.[`05-config-variants.php`](examples/05-config-variants.php)The three ways to describe a server — share link, sing-box array, constructor — and what is refused outright.[`06-cancellation.php`](examples/06-cancellation.php)Deadlines and cancellation across every layer of the tunnel, including a cancellation that arrives mid-handshake.[`07-streaming.php`](examples/07-streaming.php)A large download read as a stream, with memory measured to prove it never accumulates.[`08-websocket.php`](examples/08-websocket.php)An application WebSocket through the tunnel — not to be confused with WebSocket as the tunnel's own transport.[`09-dns-udp.php`](examples/09-dns-udp.php)UDP: a real DNS exchange with datagram boundaries preserved by the protocol's length framing.[`10-connection-reuse.php`](examples/10-connection-reuse.php)How many tunnels a run of requests actually opens, and what a connection pool saves.[`11-http-proxy-server.php`](examples/11-http-proxy-server.php)The local inbounds: SOCKS5 and HTTP together, driven by `curl`. What `vless-proxy` runs.[`12-reverse-proxy.php`](examples/12-reverse-proxy.php)A site reachable only through the tunnel, served at a local address. No client configuration at all.[`13-tcp-port-forward.php`](examples/13-tcp-port-forward.php)A forwarded TCP port — SSH, a database, anything — because it never interprets the bytes.### Running it in an application

[](#running-it-in-an-application)

These cover what happens after the first successful request: what each failure means, and how the tunnel fits into a program that has to keep working.

ExampleWhat it demonstrates[`14-diagnostics.php`](examples/14-diagnostics.php)Ten deliberate failures — bad link, Vision, REALITY, dead port, wrong path, wrong uuid — each naming its exception and its fix. **Needs no configuration:** it builds its own broken configs and its own misbehaving servers on loopback.[`15-no-dns-leak.php`](examples/15-no-dns-leak.php)Proof that the destination name never reaches this machine's resolver. A recording resolver captures every lookup; only the server's own hostname appears.[`16-split-tunnel.php`](examples/16-split-tunnel.php)Some traffic through the tunnel, the rest direct, as a `SocketConnector` decorator — with the routing proved, not assumed.[`17-failover.php`](examples/17-failover.php)Several servers: measuring them, racing them, and switching off one that dies. Includes the timing mistake that makes a failover work only when it is not needed.[`18-database.php`](examples/18-database.php)MySQL and Redis that only accept connections from inside the VPN. One constructor argument each.[`19-timeouts-retries.php`](examples/19-timeouts-retries.php)The handshake measured layer by layer, and what to set when it is four deep. Values tuned for a direct connection cut off requests that were fine.[`20-worker.php`](examples/20-worker.php)A long-running process: backoff with jitter, a cap on concurrent tunnels, and which failures are worth retrying at all.[`21-upload-streaming.php`](examples/21-upload-streaming.php)A large body sent without buffering, and the cost of buffering it measured in megabytes.[`22-chaining.php`](examples/22-chaining.php)A tunnel through a tunnel, and — the useful case — a tunnel built on top of whatever connector can leave your network.[`23-subscription.php`](examples/23-subscription.php)Reading a subscription list, sorting it, and refusing what cannot be carried instead of degrading quietly.[`24-form-post.php`](examples/24-form-post.php)Form submissions in both directions: built and sent out, then received, parsed and relayed.[`25-telegram.php`](examples/25-telegram.php)A bot posting to Telegram through the tunnel — the case this library is most often wanted for — with Telegram's own error shapes handled.[`26-transports.php`](examples/26-transports.php)The same request over every transport that is configured, showing that which one a server speaks never reaches your code.[`27-ssh-tunnel.php`](examples/27-ssh-tunnel.php)`ssh -L` without shelling out: a local port forwarded from an SSH server that is itself reached through the tunnel. Two tunnels stacked, neither library aware of the other.Testing
-------

[](#testing)

```
composer test        # unit and mock-level suites
composer psalm       # static analysis
composer check       # both
composer test-e2e    # against a real server, needs .env
```

`composer test` passes on a machine with no credentials at all: the end-to-end suite skips itself when `VLESS_TEST_URI` is absent.

An end-to-end exchange that returns nothing at all is tried once more. VLESS has no reply saying whether the destination was reached, so a connection lost in transit arrives as an ordinary end of stream — indistinguishable from a client that is broken. Only silence is retried; anything that arrived is judged as it came. `VLESS_E2E_PACING_SECONDS` adds a gap between tests for a server that rate-limits.

The protocol codec is checked against byte vectors generated by the upstream Go implementation (`tools/gen-vectors`, pinned to the `sing-vmess` revision that sing-box 1.13.16 uses). Comparing the encoder against numbers the encoder itself produced would prove nothing, so those fixtures are the only accepted source of truth for the wire format.

Credits
-------

[](#credits)

The protocol was implemented by reading [`sing-vmess`](https://github.com/SagerNet/sing-vmess) and [`sing-box`](https://github.com/SagerNet/sing-box), with [`Xray-core`](https://github.com/XTLS/Xray-core) for cross-checking. The connector architecture follows [`amphp/http-tunnel`](https://github.com/amphp/http-tunnel).

License
-------

[](#license)

MIT. See [LICENSE](LICENSE).

###  Health Score

41

—

FairBetter than 87% of packages

Maintenance100

Actively maintained with recent releases

Popularity2

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity48

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

3

Last Release

0d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/181338?v=4)[Sergey Krupkin](/maintainers/Rekryt)[@rekryt](https://github.com/rekryt)

---

Top Contributors

[![rekryt](https://avatars.githubusercontent.com/u/181338?v=4)](https://github.com/rekryt "rekryt (25 commits)")

---

Tags

amphpasyncphpvlessasyncclientampamphpxrayvlesssing-box

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/rekryt-amphp-vless/health.svg)

```
[![Health](https://phpackages.com/badges/rekryt-amphp-vless/health.svg)](https://phpackages.com/packages/rekryt-amphp-vless)
```

###  Alternatives

[amphp/http-server

A non-blocking HTTP application server for PHP based on Amp.

1.3k7.8M121](/packages/amphp-http-server)[danog/madelineproto

Async PHP client API for the telegram MTProto protocol.

3.5k920.5k24](/packages/danog-madelineproto)[amphp/http-client

An advanced async HTTP client library for PHP, enabling efficient, non-blocking, and concurrent requests and responses.

72910.7M217](/packages/amphp-http-client)[amphp/redis

Efficient asynchronous communication with Redis servers, enabling scalable and responsive data storage and retrieval.

167745.3k61](/packages/amphp-redis)[amphp/dns

Async DNS resolution for Amp.

19449.6M61](/packages/amphp-dns)[amphp/parallel

Parallel processing component for Amp.

85653.9M104](/packages/amphp-parallel)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
