Skip to content

Repository files navigation

Sliver Script

Sliver-script is a TypeScript/JavaScript client library for Sliver, it can be used to automate any operator interaction with Sliver. Sliver-script uses existing Sliver client configuration files and connects to servers using gRPC over Mutual-TLS. It also provides RxJS abstractions for easy interactions with real-time components.

This library targets modern Sliver protobuf/gRPC APIs and provides a strongly-typed TypeScript-first client.

Build Check Sliver End-to-End npm version License: GPL v3

The standalone package lineage and generic adaptations are pinned in integration.lock.json. The canonical Sliver/protobuf source and generated artifacts are pinned in protobuf.lock.json, which the integration lock cross-references. Both locks describe only this package and its upstream protocol inputs. The package exposes explicit typed convenience methods and no method-name/request-object dispatcher. For compatibility it still exports the generated rpcpb namespace and the existing statically typed SliverClient.rpc getter; trusted applications must impose their own narrower capability boundary.

Install

Node v24 or later is required for this package, and it can be installed via npm:

npm install sliver-script

End-to-end tests

npm run test:e2e compiles the server from the pinned Sliver submodule, creates an isolated multiplayer profile with the server CLI, starts the native server, and runs the grouped TypeScript client tests against it. GitHub Actions runs the same path on Linux/amd64, Windows/amd64, and macOS/arm64. See e2e/README.md for the implemented groups and expansion plan.

Operator transport

Operator connections use direct mTLS. Configurations containing a WireGuard operator profile are rejected before credentials or channels are created. WireGuard listener and implant C2 APIs remain available.

Stateful forwarding

The client provides stateful TCP port-forward, reverse-port-forward, and SOCKS5 handles. Local port forwards and SOCKS5 listeners are owned by the SliverClient and close when it disconnects. Reverse port forwards are owned by the teamserver: their handles become detached on disconnect and reconcile with the authoritative server inventory after the client reconnects.

const portForward = await client.startPortForward(sessionId, {
  bind: { host: "127.0.0.1", port: 0 }, // zero selects a free local port
  target: { host: "intranet.example", port: 443 },
})

const socks = await client.startSocks5Proxy(sessionId, {
  bind: { host: "127.0.0.1", port: 0 },
  authentication: { username: "operator", password: process.env.SOCKS_PASSWORD! },
})

const reverse = await client.startReversePortForward(sessionId, {
  bind: { host: "127.0.0.1", port: 8080 }, // opened by the implant
  target: { host: "127.0.0.1", port: 3000 }, // opened by the teamserver
})

portForward.connection$.subscribe((connection) => {
  console.log(connection.status, connection.bytesToTarget, connection.bytesFromTarget)
})

await Promise.all([portForward.close(), socks.close(), reverse.close()])

Local listeners expose their actual bound address, immutable state snapshots, state and per-connection observables, bounded connection/buffer controls, and idempotent close(). Use listPortForwards() / stopPortForward(id) and listSocks5Proxies() / stopSocks5Proxy(id) for client-owned inventory. Use listReversePortForwards() / stopReversePortForward() for server-owned inventory, including safe cleanup of legacy listeners whose target metadata is not trusted. Sliver's generic forwarding protocol currently supports TCP; the application traffic carried over it can be HTTP, RDP, or any other TCP protocol. These tunnels use full-close TCP semantics: a local FIN retires both directions, so protocols that send a delayed response only after the client half-closes are not supported. A SOCKS connection's open event means its Sliver lifecycle stream is established; SOCKS authentication and the target CONNECT exchange still occur afterward. Authentication and CONNECT rejections are returned in the SOCKS wire replies and follow the ordinary connection-close lifecycle; the handle does not parse these negotiation results. A protocol-error event means malformed Sliver SOCKS framing, such as invalid sequence or acknowledgement data.

Reproducible protobuf generation

Generated TypeScript under src/pb is locked to Sliver commit bbb20155b7a18d4906ec936566bf0dc61fe38f35, protoc 35.1, and ts-proto 2.12.3. The generator reads only the pinned sliver submodule; it never selects a neighboring checkout.

From a Git source checkout, install protoc 35.1 after npm ci, initialize the pinned submodule, and run:

git submodule update --init --recursive
npm run protobuf:check

npm run protobuf:generate rewrites the five checked-in protobuf modules only after the source commit, source tree, input hashes, descriptor hash, generator version, and generated byte hashes all match protobuf.lock.json.

That command is restorative, not an upgrade command. Advancing Sliver, protoc, or ts-proto requires a deliberate maintainer review that regenerates and updates every source, descriptor, and output hash in the lock together.

The npm tarball carries those generated TypeScript modules and their provenance lock, but deliberately excludes the Sliver submodule and therefore is not a self-contained protobuf regeneration checkout.

Before preparing a package, run npm run audit:all and npm run verify. Verification audits the exact packed runtime dependency graph, then executes unit tests, protobuf checks, a clean TypeScript build, npm pack --dry-run, and CommonJS, ESM, and TypeScript NodeNext smoke tests against the packed tarball in a temporary consumer. The packed tarball includes the TypeScript source and locked protobuf provenance in addition to compiled JavaScript and declarations.

Publishing releases

Pushing a version tag such as v2.0.0 starts the npm publishing workflow. Both Build Check and the full native E2E matrix must pass for that commit before the validated tarball can be published. Stable versions use npm's latest channel; prereleases such as v2.0.0-rc.1 use next.

The workflow also supports a manual dry-run before creating a tag. See RELEASING.md for npm trusted publisher setup, dry-runs, release steps, and retry behavior.

TypeScript Example

Basic

import { SliverClient, ParseConfigFile } from 'sliver-script'

(async function() {
    
    const config = await ParseConfigFile('./localhost.cfg')
    const client = new SliverClient(config)

    await client.connect()

    const version = await client.getVersion()
    console.log(version)

    const sessions = await client.sessions()
    console.log(`Sessions: ${sessions.length}`)

    await client.disconnect()

})()

Monitor Events in Real-time

import { SliverClient, ParseConfigFile } from 'sliver-script'

(async function() {

    const config = await ParseConfigFile('./localhost.cfg')

    const client = new SliverClient(config)
    await client.connect()
    client.event$.subscribe((event) => {
        console.log(event)
    })

})()

Automatically Interact with New Sessions

import { SliverClient, ParseConfigFile } from 'sliver-script'


(async function() {

    const config = await ParseConfigFile('./localhost.cfg')
    const client = new SliverClient(config);
    await client.connect()

    console.log('Waiting for new sessions ...')
    client.session$.subscribe(async (event) => {
        console.log(`New session #${event.Session.ID}!`)
        const session = client.interactSession(event.Session.ID)
        const ls = await session.ls('.')
        console.log(`Path: ${ls.Path}`)
        ls.Files.forEach(file => {
            console.log(`Name: ${file.Name} (Size: ${file.Size})`)
        })
    })

})()

JavaScript Example

const sliver = require('sliver-script');

;(async function() {

    const config = await sliver.ParseConfigFile('./localhost.cfg')
    const client = new sliver.SliverClient(config)
    await client.connect()

    console.log('Waiting for new sessions ...')

    client.session$.subscribe(async (event) => {

        console.log(`New session #${event.Session.ID}!`)

        const session = client.interactSession(event.Session.ID)
        const ls = await session.ls('.')
        console.log(`Path: ${ls.Path}`)
        ls.Files.forEach(file => {
            console.log(`Name: ${file.Name} (Size: ${file.Size})`)
        })
        
    })

})()

About

TypeScript/JavaScript client libraries for Sliver

Resources

Stars

28 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages