Compare commits

...
Author SHA1 Message Date
moloch-- b46df4b05e ci: remove 386 targets from E2E matrices 2026-09-03 07:22:30 -07:00
Joe cbc9200649 Merge pull request #2359 from BishopFox/feat/portfwd-socks5-e2e
feat: harden portfwd and SOCKS5 tunnels
2026-09-02 22:09:53 -07:00
moloch-- a0bf009c06 refactor tunnel lifecycle bookkeeping 2026-09-02 19:00:28 -07:00
moloch-- 46f516fa3a feat: harden portfwd and SOCKS5 tunnels
Add negotiated bounded flow control and exact-generation tunnel lifecycle handling.

Preserve legacy implant compatibility, serialize HTTP tunnel responses, and add manual local/Proxmox E2E plus fuzz coverage for real traffic and RDP.
2026-09-02 15:09:29 -07:00
Joe b2c0979b63 Merge pull request #2356 from BishopFox/feat/rportfwd-authorization
Rework reverse port forwarding lifecycle and E2E coverage
2026-09-01 16:21:51 -07:00
moloch-- ea5e1412b2 fix: pass cross-platform lint checks 2026-09-01 13:19:51 -07:00
moloch-- bd948b8ab1 test: preserve pinned OPFOR corpus bytes 2026-09-01 11:01:04 -07:00
moloch-- 937a74d597 test: make e2e process cleanup idempotent 2026-09-01 09:20:24 -07:00
moloch-- e747187dda ci: fix reverse port forward workflow context 2026-09-01 09:05:15 -07:00
moloch-- ec992dea76 fix: complete reverse port forward hardening 2026-09-01 09:03:30 -07:00
moloch-- 544c872881 test: integrate reverse port forwarding into e2e 2026-08-31 12:40:48 -07:00
moloch-- 4701b895b7 fix: harden reverse port forward authorization 2026-08-31 12:22:36 -07:00
Joe 18c56ec3c5 Merge pull request #2353 from BishopFox/fix/shell-win
Improvements to windows/shell handling
2026-08-31 09:57:48 -07:00
152 changed files with 40098 additions and 2619 deletions

No files matched your search

+1
View File
@@ -1,3 +1,4 @@
# Preserve pinned upstream source and attribution files byte-for-byte.
client/command/opfor/testdata/corpus/** -text
client/command/opfor/testdata/corpus/operatorskit/*.cna whitespace=-trailing-space,-blank-at-eof
vendor/github.com/sliverarmory/opfor/internal/regexp2/ATTRIB whitespace=-trailing-space
+200 -105
View File
@@ -2,11 +2,29 @@ name: Comprehensive e2e Tests
on:
workflow_dispatch:
inputs:
test_scope:
description: Test scope to run across the complete platform matrix
required: true
default: comprehensive
type: choice
options:
- comprehensive
- rportfwd
workflow_call:
inputs:
test_scope:
description: Test scope to run across the complete platform matrix
required: false
default: comprehensive
type: string
permissions:
contents: read
env:
SLIVER_E2E_SCOPE: ${{ inputs.test_scope || 'comprehensive' }}
concurrency:
group: comprehensive-e2e-${{ github.ref }}
cancel-in-progress: false
@@ -21,8 +39,16 @@ jobs:
uses: actions/github-script@v9
env:
REQUESTING_ACTOR: ${{ github.triggering_actor }}
REQUESTED_SCOPE: ${{ inputs.test_scope || 'comprehensive' }}
with:
script: |
const testScope = process.env.REQUESTED_SCOPE || "comprehensive";
if (!new Set(["comprehensive", "rportfwd"]).has(testScope)) {
core.setFailed(`Unsupported E2E test scope: ${JSON.stringify(testScope)}`);
return;
}
core.info(`Validated E2E test scope ${testScope}`);
const isReleaseTag = context.eventName === "push" && context.ref.startsWith("refs/tags/v");
if (isReleaseTag) {
core.info(`Authorized release test invocation for ${context.ref}`);
@@ -40,6 +66,38 @@ jobs:
core.setFailed("Comprehensive e2e Tests may only be run by repository administrators");
}
rportfwd-security-regression:
name: Reverse-port-forward malicious protocol regression
needs: authorize
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- name: Check out code
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: go.mod
cache: true
- name: Download Sliver build assets
env:
SLIVER_ROOT_DIR: ${{ runner.temp }}/sliver-rportfwd-security
run: go run -buildvcs=false -mod=vendor ./util/cmd/assets
- name: Test malicious destination poisoning and cross-session isolation
env:
SLIVER_ROOT_DIR: ${{ runner.temp }}/sliver-rportfwd-security
run: >-
go test -buildvcs=false -mod=vendor
-tags=server,osusergo,netgo,go_sqlite,sliver_e2e
./server/c2
-run '^TestMTLSYamux_ReversePortForwardAuthorizationCannotBePoisonedOrReplayed$'
-count=1 -v
darwin:
name: comprehensive-e2e-darwin-${{ matrix.goarch }}
needs: authorize
@@ -96,21 +154,31 @@ jobs:
go test -c -buildvcs=false -mod=vendor -trimpath -tags client,go_sqlite
-o sliver-comprehensive-e2e ./test/e2e
- name: Run comprehensive E2E suite
- name: Run selected E2E scope
run: |
set -o pipefail
./sliver-comprehensive-e2e \
-test.v \
-test.run=^TestComprehensiveE2E$ \
-test.timeout=0 \
-repo . \
-server ./sliver-server \
-server-arch ${{ matrix.server_arch }} \
-target-os darwin \
-target-arch ${{ matrix.goarch }} \
-results ./e2e-results \
-transports mtls,wg,http \
-implant-modes session,beacon \
-beacon-interval 10s 2>&1 | tee ./e2e-results/selected-e2e.log
- name: Verify target reverse-port-forward coverage
if: ${{ always() }}
run: >-
./sliver-comprehensive-e2e
-test.v
-test.run=^TestComprehensiveE2E$
-test.timeout=0
-repo .
-server ./sliver-server
-server-arch ${{ matrix.server_arch }}
go run -buildvcs=false -mod=vendor ./test/e2e/rportfwd-report
-input e2e-results
-target-os darwin
-target-arch ${{ matrix.goarch }}
-results ./e2e-results
-transports mtls,wg,http
-implant-modes session,beacon
-beacon-interval 10s
- name: Upload target coverage
if: ${{ always() }}
@@ -178,21 +246,31 @@ jobs:
go test -c -buildvcs=false -mod=vendor -trimpath -tags client,go_sqlite
-o sliver-comprehensive-e2e ./test/e2e
- name: Run comprehensive E2E suite
- name: Run selected E2E scope
run: |
set -o pipefail
./sliver-comprehensive-e2e \
-test.v \
-test.run=^TestComprehensiveE2E$ \
-test.timeout=0 \
-repo . \
-server ./sliver-server \
-server-arch ${{ matrix.server_arch }} \
-target-os linux \
-target-arch ${{ matrix.goarch }} \
-results ./e2e-results \
-transports mtls,wg,http \
-implant-modes session,beacon \
-beacon-interval 10s 2>&1 | tee ./e2e-results/selected-e2e.log
- name: Verify target reverse-port-forward coverage
if: ${{ always() }}
run: >-
./sliver-comprehensive-e2e
-test.v
-test.run=^TestComprehensiveE2E$
-test.timeout=0
-repo .
-server ./sliver-server
-server-arch ${{ matrix.server_arch }}
go run -buildvcs=false -mod=vendor ./test/e2e/rportfwd-report
-input e2e-results
-target-os linux
-target-arch ${{ matrix.goarch }}
-results ./e2e-results
-transports mtls,wg,http
-implant-modes session,beacon
-beacon-interval 10s
- name: Upload target coverage
if: ${{ always() }}
@@ -204,71 +282,6 @@ jobs:
overwrite: true
retention-days: 14
linux-386:
name: comprehensive-e2e-linux-386
needs: authorize
runs-on: ubuntu-24.04
timeout-minutes: 360
env:
CONTAINER_NAME: sliver-comprehensive-e2e-linux-386-${{ github.run_id }}-${{ github.run_attempt }}
IMAGE_NAME: sliver-comprehensive-e2e-linux-386:${{ github.run_id }}-${{ github.run_attempt }}
defaults:
run:
shell: bash
steps:
- name: Check out code
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Prepare coverage artifact
run: |
mkdir -p e2e-results
printf 'linux/386\n' > e2e-results/target-linux-386.txt
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build linux/386 E2E image
run: |
set -euo pipefail
go_version="$(awk '$1 == "go" { print $2; exit }' go.mod)"
docker buildx build \
--load \
--platform linux/386 \
--build-arg "GO_VERSION=${go_version}" \
--file test/e2e/Dockerfile.linux-386 \
--tag "${IMAGE_NAME}" \
.
- name: Run comprehensive E2E suite
run: >-
docker run
--name "${CONTAINER_NAME}"
--platform linux/386
--volume "${PWD}/e2e-results:/results"
"${IMAGE_NAME}"
- name: Clean up linux/386 Docker resources
if: ${{ always() }}
run: |
docker rm --force "${CONTAINER_NAME}" 2>/dev/null || true
docker image rm --force "${IMAGE_NAME}" 2>/dev/null || true
docker buildx prune --all --force >/dev/null 2>&1 || true
- name: Upload target coverage
if: ${{ always() }}
uses: actions/upload-artifact@v7
with:
name: comprehensive-e2e-target-linux-386
path: e2e-results
if-no-files-found: error
overwrite: true
retention-days: 14
windows:
name: comprehensive-e2e-windows-${{ matrix.goarch }}
needs: authorize
@@ -278,9 +291,6 @@ jobs:
fail-fast: false
matrix:
include:
- runner: windows-2022
goarch: 386
server_arch: amd64
- runner: windows-2022
goarch: amd64
server_arch: amd64
@@ -328,21 +338,31 @@ jobs:
go test -c -buildvcs=false -mod=vendor -trimpath -tags client,go_sqlite
-o sliver-comprehensive-e2e.exe ./test/e2e
- name: Run comprehensive E2E suite
- name: Run selected E2E scope
run: |
set -o pipefail
./sliver-comprehensive-e2e.exe \
-test.v \
-test.run=^TestComprehensiveE2E$ \
-test.timeout=0 \
-repo . \
-server ./sliver-server.exe \
-server-arch ${{ matrix.server_arch }} \
-target-os windows \
-target-arch ${{ matrix.goarch }} \
-results ./e2e-results \
-transports mtls,wg,http \
-implant-modes session,beacon \
-beacon-interval 10s 2>&1 | tee ./e2e-results/selected-e2e.log
- name: Verify target reverse-port-forward coverage
if: ${{ always() }}
run: >-
./sliver-comprehensive-e2e.exe
-test.v
-test.run=^TestComprehensiveE2E$
-test.timeout=0
-repo .
-server ./sliver-server.exe
-server-arch ${{ matrix.server_arch }}
go run -buildvcs=false -mod=vendor ./test/e2e/rportfwd-report
-input e2e-results
-target-os windows
-target-arch ${{ matrix.goarch }}
-results ./e2e-results
-transports mtls,wg,http
-implant-modes session,beacon
-beacon-interval 10s
- name: Upload target coverage
if: ${{ always() }}
@@ -354,15 +374,88 @@ jobs:
overwrite: true
retention-days: 14
aggregate-rportfwd:
name: Aggregate reverse-port-forward E2E coverage
needs:
- authorize
- rportfwd-security-regression
- darwin
- linux-native
- windows
if: ${{ always() && needs.authorize.result == 'success' }}
runs-on: ubuntu-24.04
timeout-minutes: 30
defaults:
run:
shell: bash
steps:
- name: Check out code
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version-file: go.mod
cache: true
- name: Download target coverage
id: download
continue-on-error: true
uses: actions/download-artifact@v8
with:
pattern: comprehensive-e2e-target-*
path: rportfwd-coverage-input
- name: Prepare aggregate artifact
if: ${{ always() }}
run: |
mkdir -p rportfwd-coverage-input rportfwd-coverage-summary
printf 'Reverse-port-forward E2E coverage aggregate\n' > rportfwd-coverage-summary/README.txt
- name: Aggregate coverage and enforce all 162 required cells
id: aggregate
if: ${{ always() }}
continue-on-error: true
run: >-
go run -buildvcs=false -mod=vendor ./test/e2e/rportfwd-report
-input rportfwd-coverage-input
-output rportfwd-coverage-summary
- name: Publish reverse-port-forward coverage table
if: ${{ always() }}
run: |
report="rportfwd-coverage-summary/rportfwd-coverage.md"
if [[ -s "${report}" ]]; then
cat "${report}" >> "${GITHUB_STEP_SUMMARY}"
else
printf 'Reverse-port-forward coverage report is missing or empty: %s\n' "${report}" >> "${GITHUB_STEP_SUMMARY}"
fi
- name: Upload aggregate reverse-port-forward coverage
if: ${{ always() }}
uses: actions/upload-artifact@v7
with:
name: comprehensive-e2e-rportfwd-coverage-summary
path: rportfwd-coverage-summary
if-no-files-found: error
overwrite: true
retention-days: 30
- name: Fail on failed prerequisites, missing, malformed, or unavailable coverage
if: ${{ needs.rportfwd-security-regression.result != 'success' || needs.darwin.result != 'success' || needs.linux-native.result != 'success' || needs.windows.result != 'success' || steps.download.outcome != 'success' || steps.aggregate.outcome != 'success' }}
run: exit 1
aggregate-coverage:
name: Aggregate comprehensive E2E coverage
needs:
- authorize
- rportfwd-security-regression
- darwin
- linux-native
- linux-386
- windows
if: ${{ always() && needs.authorize.result == 'success' }}
if: ${{ always() && needs.authorize.result == 'success' && inputs.test_scope != 'rportfwd' }}
outputs:
command_coverage_markdown: ${{ steps.publish_command_coverage.outputs.command_coverage_markdown }}
runs-on: ubuntu-24.04
@@ -441,16 +534,18 @@ jobs:
overwrite: true
retention-days: 30
- name: Fail on failed or not-run coverage
if: ${{ steps.aggregate.outcome != 'success' }}
- name: Fail on failed prerequisites or failed or not-run coverage
if: ${{ needs.rportfwd-security-regression.result != 'success' || needs.darwin.result != 'success' || needs.linux-native.result != 'success' || needs.windows.result != 'success' || steps.aggregate.outcome != 'success' }}
run: exit 1
reflektor:
name: Existing Reflektor test matrix
needs: authorize
if: ${{ inputs.test_scope != 'rportfwd' }}
uses: ./.github/workflows/reflektor-tests.yml
shellcode:
name: Shellcode E2E test matrix
needs: authorize
if: ${{ inputs.test_scope != 'rportfwd' }}
uses: ./.github/workflows/shellcode-tests.yml
-50
View File
@@ -111,53 +111,6 @@ jobs:
-target-os linux
-target-arch ${{ matrix.goarch }}
linux-386:
name: reflektor-linux-386-docker
runs-on: ubuntu-24.04
timeout-minutes: 180
env:
CONTAINER_NAME: sliver-reflektor-linux-386-${{ github.run_id }}-${{ github.run_attempt }}
IMAGE_NAME: sliver-reflektor-linux-386:${{ github.run_id }}-${{ github.run_attempt }}
defaults:
run:
shell: bash
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build linux/386 test image
run: |
set -euo pipefail
go_version="$(awk '$1 == "go" { print $2; exit }' go.mod)"
docker buildx build \
--load \
--platform linux/386 \
--build-arg "GO_VERSION=${go_version}" \
--file test/reflektor/Dockerfile.linux-386 \
--tag "${IMAGE_NAME}" \
.
docker buildx prune --all --force
- name: Load Sliver with Reflektor
run: >-
docker run
--name "${CONTAINER_NAME}"
--platform linux/386
"${IMAGE_NAME}"
- name: Clean up linux/386 Docker resources
if: ${{ always() }}
run: |
docker rm --force "${CONTAINER_NAME}" 2>/dev/null || true
docker image rm --force "${IMAGE_NAME}" 2>/dev/null || true
docker buildx prune --all --force >/dev/null 2>&1 || true
windows:
name: reflektor-windows-${{ matrix.goarch }}
runs-on: ${{ matrix.runner }}
@@ -169,9 +122,6 @@ jobs:
- runner: windows-2022
goarch: amd64
server_arch: amd64
- runner: windows-2022
goarch: 386
server_arch: amd64
- runner: windows-11-arm
goarch: arm64
server_arch: amd64
-7
View File
@@ -40,13 +40,6 @@ jobs:
server_arch: arm64
server_executable: ./sliver-server
test_executable: ./sliver-shellcode-e2e
- runner: windows-2022
target_os: windows
target_arch: "386"
server_os: windows
server_arch: amd64
server_executable: ./sliver-server.exe
test_executable: ./sliver-shellcode-e2e.exe
- runner: windows-2022
target_os: windows
target_arch: amd64
+2
View File
@@ -6,6 +6,8 @@ on:
paths:
- "go.mod"
- "go.sum"
- ".gitattributes"
- "client/command/opfor/testdata/corpus/**"
- ".github/workflows/unit-tests.yml"
- "Makefile"
- "**/*.go"
+9 -1
View File
@@ -228,6 +228,14 @@ func startCursedChromeProcess(isEdge bool, session *clientpb.Session, cmd *cobra
DialTimeout: 30 * time.Second,
}
tcpProxy.AddRoute(bindAddr, channelProxy)
if err := tcpProxy.Start(); err != nil {
channelProxy.Stop()
_ = tcpProxy.Close()
return nil, errors.Join(
fmt.Errorf("start port forward listener %s: %w", bindAddr, err),
terminateStartedCursedProcess(con, session.ID, chromeExec.GetPid()),
)
}
portFwd := core.Portfwds.Add(tcpProxy, channelProxy)
curse := &core.CursedProcess{
@@ -241,7 +249,7 @@ func startCursedChromeProcess(isEdge bool, session *clientpb.Session, cmd *cobra
}
core.CursedProcesses.Store(bindPort, curse)
go func() {
err := tcpProxy.Run()
err := tcpProxy.Wait()
if err != nil {
log.Printf("Proxy error %s", err)
}
+10 -1
View File
@@ -20,6 +20,7 @@ package cursed
import (
"context"
"errors"
"fmt"
"log"
"path"
@@ -190,6 +191,14 @@ func startCursedElectronProcess(electronExe string, session *clientpb.Session, c
DialTimeout: 30 * time.Second,
}
tcpProxy.AddRoute(bindAddr, channelProxy)
if err := tcpProxy.Start(); err != nil {
channelProxy.Stop()
_ = tcpProxy.Close()
return nil, errors.Join(
fmt.Errorf("start port forward listener %s: %w", bindAddr, err),
terminateStartedCursedProcess(con, session.ID, electronExec.GetPid()),
)
}
portFwd := core.Portfwds.Add(tcpProxy, channelProxy)
curse := &core.CursedProcess{
@@ -202,7 +211,7 @@ func startCursedElectronProcess(electronExe string, session *clientpb.Session, c
}
core.CursedProcesses.Store(bindPort, curse)
go func() {
err := tcpProxy.Run()
err := tcpProxy.Wait()
if err != nil {
log.Printf("Proxy error %s", err)
}
+27
View File
@@ -20,20 +20,47 @@ package cursed
import (
"bytes"
"context"
"errors"
"fmt"
"strconv"
"strings"
"text/tabwriter"
"time"
"github.com/bishopfox/sliver/client/command/settings"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/client/core"
"github.com/bishopfox/sliver/client/forms"
"github.com/bishopfox/sliver/protobuf/commonpb"
"github.com/bishopfox/sliver/protobuf/sliverpb"
"github.com/bishopfox/sliver/util"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/spf13/cobra"
)
const cursedStartCleanupTimeout = 30 * time.Second
func terminateStartedCursedProcess(con *console.SliverClient, sessionID string, pid uint32) error {
const maxTerminatePID = uint32(1<<31 - 1)
if con == nil || con.Rpc == nil || sessionID == "" || pid == 0 || pid > maxTerminatePID {
return errors.New("cannot terminate incompletely tracked cursed process")
}
ctx, cancel := context.WithTimeout(context.Background(), cursedStartCleanupTimeout)
defer cancel()
response, err := con.Rpc.Terminate(ctx, &sliverpb.TerminateReq{
Request: &commonpb.Request{SessionID: sessionID},
Pid: int32(pid),
})
if err != nil {
return fmt.Errorf("terminate started cursed process %d: %w", pid, err)
}
if response.GetResponse().GetErr() != "" {
return fmt.Errorf("terminate started cursed process %d: %s", pid, response.GetResponse().GetErr())
}
return nil
}
// CursedChromeCmd - Execute a .NET assembly in-memory.
func CursedCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
// Collect existing curses from core
+39
View File
@@ -5,6 +5,7 @@ package opfor
import (
"bytes"
"context"
"crypto/sha256"
"fmt"
"os"
"path/filepath"
@@ -124,6 +125,44 @@ func expectedStringArguments(value string) []byte {
return arguments
}
func TestSliverArmoryCNACorpusPinnedHashes(t *testing.T) {
fixtures := []struct {
path string
sha256 string
}{
{
path: "bof_collection/cat.cna",
sha256: "94c7bcaae209a6355dcc8c126019f6e19a681173680955166b8c30cf97fc66f7",
},
{
path: "firefoxdump/firefoxdump.cna",
sha256: "c8c9ef28675d16dd1c8786f055c08ce2096eb0fedb16fad68b78f22c81b1bbde",
},
{
path: "operatorskit/finddotnet.cna",
sha256: "897dbee453ed504be7e9ace1229e7070f131a9351fb09a723629dd8e0337aa03",
},
{
path: "operatorskit/findsysmon.cna",
sha256: "1579dcca9e1c0ab7b20a646ff2db7705e05468c067a8ed6b3c35b553f10458d7",
},
}
for _, fixture := range fixtures {
fixture := fixture
t.Run(fixture.path, func(t *testing.T) {
content, err := os.ReadFile(filepath.Join("testdata", "corpus", filepath.FromSlash(fixture.path)))
if err != nil {
t.Fatalf("read pinned corpus fixture: %v", err)
}
digest := fmt.Sprintf("%x", sha256.Sum256(content))
if digest != fixture.sha256 {
t.Fatalf("SHA-256 = %s, want %s", digest, fixture.sha256)
}
})
}
}
type corpusDispatchTest struct {
name string
fixture string
+15 -9
View File
@@ -21,7 +21,6 @@ package portfwd
import (
"fmt"
"log"
"net"
"regexp"
"time"
@@ -50,14 +49,10 @@ func PortfwdAddCmd(cmd *cobra.Command, con *console.SliverClient, args []string)
con.PrintErrorf("Must specify a remote target host:port\n")
return
}
remoteHost, remotePort, err := net.SplitHostPort(remoteAddr)
if err != nil {
con.PrintErrorf("Failed to parse remote target %s\n", err)
if err := validatePortfwdRemoteAddress(remoteAddr); err != nil {
con.PrintErrorf("Failed to parse remote target: %s\n", err)
return
}
if remotePort == "3389" {
con.PrintWarnf("RDP is generally broken over tunneled portfwds, we recommend using WireGuard portfwds\n")
}
bindAddr, _ := cmd.Flags().GetString("bind")
if bindAddr == "" {
con.PrintErrorf("Must specify a bind target host:port (e.g. 127.0.0.1:8000)")
@@ -88,14 +83,25 @@ func PortfwdAddCmd(cmd *cobra.Command, con *console.SliverClient, args []string)
DialTimeout: 30 * time.Second,
}
tcpProxy.AddRoute(bindAddr, channelProxy)
if err := tcpProxy.Start(); err != nil {
channelProxy.Stop()
_ = tcpProxy.Close()
con.PrintErrorf("Failed to start port forward listener: %s\n", err)
return
}
core.Portfwds.Add(tcpProxy, channelProxy)
go func() {
err := tcpProxy.Run()
err := tcpProxy.Wait()
if err != nil {
log.Printf("Proxy error %s", err)
}
}()
con.PrintInfof("Port forwarding %s -> %s:%s\n", bindAddr, remoteHost, remotePort)
con.PrintInfof("Port forwarding %s -> %s\n", bindAddr, remoteAddr)
}
func validatePortfwdRemoteAddress(remoteAddr string) error {
_, _, err := (&core.ChannelProxy{RemoteAddr: remoteAddr}).ValidatedHostPort()
return err
}
@@ -0,0 +1,38 @@
package portfwd
import "testing"
func TestValidatePortfwdRemoteAddress(t *testing.T) {
tests := []struct {
name string
address string
valid bool
}{
{name: "hostname RDP", address: "windows.example:3389", valid: true},
{name: "IPv4", address: "192.0.2.10:443", valid: true},
{name: "IPv6", address: "[2001:db8::10]:8443", valid: true},
{name: "missing port", address: "target.example", valid: false},
{name: "empty port", address: "target.example:", valid: false},
{name: "empty host", address: ":3389", valid: false},
{name: "zero port", address: "target.example:0", valid: false},
{name: "out of range port", address: "target.example:65536", valid: false},
{name: "service name", address: "target.example:https", valid: false},
{name: "unbracketed IPv6", address: "2001:db8::10:8443", valid: false},
{name: "hostname with space", address: "bad host:80", valid: false},
{name: "hostname with tab", address: "bad\thost:80", valid: false},
{name: "hostname with newline", address: "bad\nhost:80", valid: false},
{name: "IPv6 zone with space", address: "[fe80::1%bad zone]:80", valid: false},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
err := validatePortfwdRemoteAddress(test.address)
if test.valid && err != nil {
t.Fatalf("validatePortfwdRemoteAddress(%q): %v", test.address, err)
}
if !test.valid && err == nil {
t.Fatalf("validatePortfwdRemoteAddress(%q) returned nil", test.address)
}
})
}
}
+21 -13
View File
@@ -203,7 +203,7 @@ func ShellKillCmd(cmd *cobra.Command, con *console.SliverClient, args []string)
// Best-effort graceful request before closing the tunnel.
requestRemoteShellExit(sh.Tunnel())
if err := closeShellTunnel(con, sh.TunnelID, sh.SessionID); err != nil {
if err := closeShellTunnel(con, sh.Tunnel()); err != nil {
con.PrintWarnf("Error closing shell tunnel %d: %s\n", sh.TunnelID, err)
}
@@ -285,11 +285,11 @@ func runInteractive(cmd *cobra.Command, shellPath string, noPty bool, con *conso
case <-tunnel.Done():
bindTimer.Stop()
con.PrintErrorf("Shell tunnel closed before it was bound\n")
go backgroundCloseShell(con, tunnel.ID, session.ID)
go backgroundCloseShell(con, tunnel)
return shellAttachFailed
case <-bindTimer.C:
con.PrintErrorf("Timed out binding shell tunnel\n")
go backgroundCloseShell(con, tunnel.ID, session.ID)
go backgroundCloseShell(con, tunnel)
return shellAttachFailed
}
@@ -318,12 +318,12 @@ func runInteractive(cmd *cobra.Command, shellPath string, noPty bool, con *conso
cancelShell()
if err != nil {
con.PrintErrorf("%s\n", err)
go backgroundCloseShell(con, tunnel.ID, session.ID)
go backgroundCloseShell(con, tunnel)
return shellAttachFailed
}
if shell.Response != nil && shell.Response.Err != "" {
con.PrintErrorf("Error: %s\n", shell.Response.Err)
go backgroundCloseShell(con, tunnel.ID, session.ID)
go backgroundCloseShell(con, tunnel)
return shellAttachFailed
}
@@ -393,20 +393,28 @@ func runInteractive(cmd *cobra.Command, shellPath string, noPty bool, con *conso
shells.Remove(managed.ID)
// "exit" should return immediately; tunnel close/cleanup is best-effort in the background.
go backgroundCloseShell(con, tunnel.ID, session.ID)
go backgroundCloseShell(con, tunnel)
return shellExited
}
func backgroundCloseShell(con *console.SliverClient, tunnelID uint64, sessionID string) {
err := closeShellTunnel(con, tunnelID, sessionID)
func backgroundCloseShell(con *console.SliverClient, tunnel *core.TunnelIO) {
if tunnel == nil {
return
}
err := closeShellTunnel(con, tunnel)
if err != nil {
log.Printf("Background close tunnel %d failed: %v", tunnelID, err)
log.Printf("Background close tunnel %d failed: %v", tunnel.ID, err)
}
}
func closeShellTunnel(con *console.SliverClient, tunnelID uint64, sessionID string) error {
core.GetTunnels().Close(tunnelID)
func closeShellTunnel(con *console.SliverClient, tunnel *core.TunnelIO) error {
if tunnel == nil {
return nil
}
if !core.GetTunnels().CloseIf(tunnel) {
return nil
}
if con == nil || con.Rpc == nil {
return nil
}
@@ -414,8 +422,8 @@ func closeShellTunnel(con *console.SliverClient, tunnelID uint64, sessionID stri
ctx, cancel := context.WithTimeout(context.Background(), shellCloseTimeout)
defer cancel()
_, err := con.Rpc.CloseTunnel(ctx, &sliverpb.Tunnel{
TunnelID: tunnelID,
SessionID: sessionID,
TunnelID: tunnel.ID,
SessionID: tunnel.SessionID,
})
return err
}
+64 -1
View File
@@ -6,6 +6,7 @@ import (
"time"
"github.com/bishopfox/sliver/client/console"
"github.com/bishopfox/sliver/client/core"
"github.com/bishopfox/sliver/protobuf/commonpb"
"github.com/bishopfox/sliver/protobuf/rpcpb"
"github.com/bishopfox/sliver/protobuf/sliverpb"
@@ -15,9 +16,21 @@ import (
type closeTunnelTestRPC struct {
rpcpb.SliverRPCClient
deadline time.Time
calls int
}
type closeShellTestStream struct {
grpc.BidiStreamingClient[sliverpb.TunnelData, sliverpb.TunnelData]
sent chan *sliverpb.TunnelData
}
func (stream *closeShellTestStream) Send(frame *sliverpb.TunnelData) error {
stream.sent <- frame
return nil
}
func (r *closeTunnelTestRPC) CloseTunnel(ctx context.Context, _ *sliverpb.Tunnel, _ ...grpc.CallOption) (*commonpb.Empty, error) {
r.calls++
deadline, ok := ctx.Deadline()
if ok {
r.deadline = deadline
@@ -26,12 +39,23 @@ func (r *closeTunnelTestRPC) CloseTunnel(ctx context.Context, _ *sliverpb.Tunnel
}
func TestCloseShellTunnelUsesBoundedContext(t *testing.T) {
tunnels := core.GetTunnels()
tunnels.Reset()
t.Cleanup(tunnels.Reset)
stream := &closeShellTestStream{sent: make(chan *sliverpb.TunnelData, 1)}
tunnels.SetStream(stream)
tunnel := tunnels.Start(99, "session")
waitForShellBindFrame(t, stream.sent)
rpc := &closeTunnelTestRPC{}
con := &console.SliverClient{Rpc: rpc}
if err := closeShellTunnel(con, 99, "session"); err != nil {
if err := closeShellTunnel(con, tunnel); err != nil {
t.Fatalf("close shell tunnel: %v", err)
}
if rpc.calls != 1 {
t.Fatalf("CloseTunnel RPC calls = %d, want 1", rpc.calls)
}
if rpc.deadline.IsZero() {
t.Fatal("CloseTunnel RPC context has no deadline")
}
@@ -39,3 +63,42 @@ func TestCloseShellTunnelUsesBoundedContext(t *testing.T) {
t.Fatalf("CloseTunnel deadline is %v away, want within (0, %v]", remaining, shellCloseTimeout)
}
}
func TestCloseShellTunnelDoesNotCloseReplacementGeneration(t *testing.T) {
tunnels := core.GetTunnels()
tunnels.Reset()
t.Cleanup(tunnels.Reset)
stream := &closeShellTestStream{sent: make(chan *sliverpb.TunnelData, 2)}
tunnels.SetStream(stream)
old := tunnels.Start(100, "old-session")
waitForShellBindFrame(t, stream.sent)
replacement := tunnels.Start(100, "new-session")
waitForShellBindFrame(t, stream.sent)
rpc := &closeTunnelTestRPC{}
con := &console.SliverClient{Rpc: rpc}
if err := closeShellTunnel(con, old); err != nil {
t.Fatalf("close stale shell tunnel: %v", err)
}
if rpc.calls != 0 {
t.Fatalf("stale shell cleanup sent %d numeric CloseTunnel RPCs, want 0", rpc.calls)
}
if current := tunnels.Get(100); current != replacement {
t.Fatal("stale shell cleanup removed the replacement tunnel")
}
select {
case <-replacement.Done():
t.Fatal("stale shell cleanup closed the replacement tunnel")
default:
}
}
func waitForShellBindFrame(t *testing.T, frames <-chan *sliverpb.TunnelData) {
t.Helper()
select {
case <-frames:
case <-time.After(time.Second):
t.Fatal("timed out waiting for shell tunnel bind frame")
}
}
+199 -36
View File
@@ -21,12 +21,15 @@ package core
import (
"context"
"errors"
"fmt"
"io"
"log"
"net"
"strconv"
"strings"
"sync"
"time"
"unicode"
"github.com/bishopfox/sliver/client/tcpproxy"
"github.com/bishopfox/sliver/protobuf/clientpb"
@@ -45,6 +48,11 @@ var (
portfwdID = 0
)
const (
portfwdBindTimeout = 5 * time.Second
portfwdCloseTimeout = 5 * time.Second
)
// PortfwdMeta - Metadata about a portfwd listener
type PortfwdMeta struct {
ID int
@@ -91,13 +99,20 @@ func (f *portfwds) Add(tcpProxy *tcpproxy.Proxy, channelProxy *ChannelProxy) *Po
// Remove - Remove a TCP proxy instance
func (f *portfwds) Remove(portfwdID int) bool {
f.mutex.Lock()
defer f.mutex.Unlock()
if portfwd, ok := f.forwards[portfwdID]; ok {
portfwd.TCPProxy.Close()
portfwd, ok := f.forwards[portfwdID]
if ok {
delete(f.forwards, portfwdID)
return true
}
return false
f.mutex.Unlock()
if !ok {
return false
}
// Remove registry ownership before joining connection workers. A slow or
// broken peer must not hold the global inventory mutex and block unrelated
// proxy lifecycle operations.
portfwd.ChannelProxy.Stop()
_ = portfwd.TCPProxy.Close()
return true
}
// List - List all TCP proxy instances
@@ -123,42 +138,126 @@ type ChannelProxy struct {
RemoteAddr string
KeepAlivePeriod time.Duration
DialTimeout time.Duration
connectionsMutex sync.Mutex
connections map[net.Conn]struct{}
lifecycleCtx context.Context
lifecycleCancel context.CancelFunc
stopped bool
handlerWG sync.WaitGroup
}
// HandleConn - Handle a TCP connection
func (p *ChannelProxy) HandleConn(conn net.Conn) {
log.Printf("[tcpproxy] Handling new connection")
ctx := context.Background()
if conn == nil {
return
}
ctx, tracked := p.trackConnection(conn)
if !tracked {
_ = conn.Close()
return
}
defer p.handlerWG.Done()
defer p.untrackConnection(conn)
defer func() { _ = conn.Close() }()
var cancel context.CancelFunc
if p.DialTimeout >= 0 {
ctx, cancel = context.WithTimeout(ctx, p.dialTimeout())
}
tunnel, err := p.dialImplant(ctx)
if cancel != nil {
defer cancel()
}
tunnel, err := p.dialImplant(ctx)
if err != nil {
return
}
// Cleanup
defer func() {
conn.Close()
GetTunnels().Close(tunnel.ID)
errs := make(chan error, 2)
copyWG := sync.WaitGroup{}
copyWG.Add(2)
go func() {
defer copyWG.Done()
errs <- toImplantLoop(conn, tunnel)
}()
go func() {
defer copyWG.Done()
errs <- fromImplantLoop(conn, tunnel)
}()
errs := make(chan error, 1)
go toImplantLoop(conn, tunnel, errs)
go fromImplantLoop(conn, tunnel, errs)
// Block until error, then cleanup
err = <-errs
// The first completed direction or a receive-admission failure owns shutdown.
// Closing both endpoints wakes the peer direction; join it before allowing
// this handler to finish.
select {
case err = <-errs:
case <-tunnel.receiveFailed():
// A receive-overflow close normally allows queued final frames to
// drain. That is unsafe for a port forward whose local peer stopped
// reading: close the accepted socket so both copy loops wake and the
// bounded remote cleanup below can run.
_ = conn.Close()
err = <-errs
}
if err != nil {
log.Printf("[tcpproxy] Closing tunnel %d with error %s", tunnel.ID, err)
}
_ = conn.Close()
GetTunnels().CloseIf(tunnel)
copyWG.Wait()
if err := p.closeTunnel(tunnel); err != nil {
log.Printf("[tcpproxy] Failed to close tunnel %d: %v", tunnel.ID, err)
}
}
// HostPort - Returns the host and port of the TCP proxy
// Stop closes every connection accepted by this target and rejects handlers
// that race the listener shutdown performed by Portfwds.Remove.
func (p *ChannelProxy) Stop() {
p.connectionsMutex.Lock()
p.stopped = true
lifecycleCancel := p.lifecycleCancel
connections := make([]net.Conn, 0, len(p.connections))
for connection := range p.connections {
connections = append(connections, connection)
}
p.connectionsMutex.Unlock()
if lifecycleCancel != nil {
lifecycleCancel()
}
for _, connection := range connections {
_ = connection.Close()
}
p.handlerWG.Wait()
}
func (p *ChannelProxy) trackConnection(connection net.Conn) (context.Context, bool) {
p.connectionsMutex.Lock()
defer p.connectionsMutex.Unlock()
if p.stopped {
return nil, false
}
if p.lifecycleCtx == nil {
p.lifecycleCtx, p.lifecycleCancel = context.WithCancel(context.Background())
}
if p.connections == nil {
p.connections = map[net.Conn]struct{}{}
}
// Add is serialized with Stop's stopped transition. Once Stop releases this
// mutex, no later Add can race its Wait.
p.handlerWG.Add(1)
p.connections[connection] = struct{}{}
return p.lifecycleCtx, true
}
func (p *ChannelProxy) untrackConnection(connection net.Conn) {
p.connectionsMutex.Lock()
delete(p.connections, connection)
p.connectionsMutex.Unlock()
}
// HostPort returns the remote host and port of the TCP proxy. Invalid values
// retain the historical empty-host/8080 fallback for source and behavioral
// compatibility. New call sites that must reject malformed destinations should
// use ValidatedHostPort.
func (p *ChannelProxy) HostPort() (string, uint32) {
defaultPort := uint32(8080)
host, rawPort, err := net.SplitHostPort(p.RemoteAddr)
@@ -179,6 +278,30 @@ func (p *ChannelProxy) HostPort() (string, uint32) {
return host, port
}
// ValidatedHostPort validates and returns the configured remote destination.
func (p *ChannelProxy) ValidatedHostPort() (string, uint32, error) {
host, rawPort, err := net.SplitHostPort(p.RemoteAddr)
if err != nil {
return "", 0, fmt.Errorf("invalid remote target %q: %w", p.RemoteAddr, err)
}
if host == "" {
return "", 0, fmt.Errorf("invalid remote target %q: host is required", p.RemoteAddr)
}
if strings.IndexFunc(host, func(value rune) bool {
return unicode.IsSpace(value) || unicode.IsControl(value)
}) >= 0 {
return "", 0, fmt.Errorf("invalid remote target %q: host contains whitespace or control characters", p.RemoteAddr)
}
portNumber, err := strconv.ParseUint(rawPort, 10, 16)
if err != nil {
return "", 0, fmt.Errorf("invalid remote target %q: port must be a number from 1 to 65535", p.RemoteAddr)
}
if portNumber == 0 {
return "", 0, fmt.Errorf("invalid remote target %q: port must be a number from 1 to 65535", p.RemoteAddr)
}
return host, uint32(portNumber), nil
}
// Port - Returns the TCP port of the proxy
func (p *ChannelProxy) Port() uint32 {
_, port := p.HostPort()
@@ -191,7 +314,11 @@ func (p *ChannelProxy) Host() string {
return host
}
func (p *ChannelProxy) dialImplant(ctx context.Context) (*TunnelIO, error) {
func (p *ChannelProxy) dialImplant(ctx context.Context) (_ *TunnelIO, resultErr error) {
host, port, err := p.ValidatedHostPort()
if err != nil {
return nil, err
}
log.Printf("[tcpproxy] Dialing implant to create tunnel ...")
@@ -206,16 +333,34 @@ func (p *ChannelProxy) dialImplant(ctx context.Context) (*TunnelIO, error) {
log.Printf("[tcpproxy] Created new tunnel with id %d (session %s)", rpcTunnel.TunnelID, p.Session.ID)
tunnel := GetTunnels().Start(rpcTunnel.TunnelID, rpcTunnel.SessionID)
setupComplete := false
defer func() {
if !setupComplete {
resultErr = errors.Join(resultErr, p.closeTunnel(tunnel))
}
}()
log.Printf("[tcpproxy] Binding tunnel to portfwd %d", p.Port())
bindTimer := time.NewTimer(portfwdBindTimeout)
defer bindTimer.Stop()
select {
case <-tunnel.Bound():
case <-tunnel.Done():
return nil, errors.New("port forward tunnel closed before it was bound")
case <-ctx.Done():
return nil, ctx.Err()
case <-bindTimer.C:
return nil, errors.New("timed out binding port forward tunnel")
}
log.Printf("[tcpproxy] Binding tunnel to portfwd %d", port)
portfwdResp, err := p.Rpc.Portfwd(ctx, &sliverpb.PortfwdReq{
Request: &commonpb.Request{
SessionID: p.Session.ID,
},
Host: p.Host(),
Port: p.Port(),
Protocol: sliverpb.PortFwdProtoTCP,
TunnelID: tunnel.ID,
Host: host,
Port: port,
Protocol: sliverpb.PortFwdProtoTCP,
TunnelID: tunnel.ID,
KeepAlive: int32(p.KeepAlivePeriod.Seconds()),
})
if err != nil {
@@ -223,17 +368,36 @@ func (p *ChannelProxy) dialImplant(ctx context.Context) (*TunnelIO, error) {
}
// Close tunnel in case of error on the implant side
if portfwdResp.Response != nil && portfwdResp.Response.Err != "" {
p.Rpc.CloseTunnel(ctx, &sliverpb.Tunnel{
TunnelID: tunnel.ID,
SessionID: p.Session.ID,
})
return nil, errors.New(portfwdResp.Response.Err)
}
log.Printf("Portfwd response: %v", portfwdResp)
select {
case <-tunnel.Done():
return nil, errors.New("port forward tunnel closed during setup")
default:
}
setupComplete = true
return tunnel, nil
}
func (p *ChannelProxy) closeTunnel(tunnel *TunnelIO) error {
if tunnel == nil {
return nil
}
GetTunnels().CloseIf(tunnel)
if p.Rpc == nil {
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), portfwdCloseTimeout)
defer cancel()
_, err := p.Rpc.CloseTunnel(ctx, &sliverpb.Tunnel{
TunnelID: tunnel.ID,
SessionID: tunnel.SessionID,
})
return err
}
// func (p *ChannelProxy) keepAlivePeriod() time.Duration {
// if p.KeepAlivePeriod != 0 {
// return p.KeepAlivePeriod
@@ -248,23 +412,22 @@ func (p *ChannelProxy) dialTimeout() time.Duration {
return 30 * time.Second
}
func toImplantLoop(conn net.Conn, tunnel *TunnelIO, errs chan<- error) {
func toImplantLoop(conn net.Conn, tunnel *TunnelIO) error {
if wc, ok := conn.(*tcpproxy.Conn); ok && len(wc.Peeked) > 0 {
if _, err := tunnel.Write(wc.Peeked); err != nil {
errs <- err
return
return err
}
wc.Peeked = nil
}
n, err := io.Copy(tunnel, conn)
log.Printf("[tcpproxy] Closing to-implant after %d byte(s)", n)
errs <- err
return err
}
func fromImplantLoop(conn net.Conn, tunnel *TunnelIO, errs chan<- error) {
func fromImplantLoop(conn net.Conn, tunnel *TunnelIO) error {
n, err := io.Copy(conn, tunnel)
log.Printf("[tcpproxy] Closing from-implant after %d byte(s)", n)
errs <- err
return err
}
func nextPortfwdID() int {
+822
View File
@@ -0,0 +1,822 @@
package core
import (
"bytes"
"context"
"errors"
"io"
"net"
"sync"
"testing"
"time"
"github.com/bishopfox/sliver/client/tcpproxy"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/protobuf/commonpb"
"github.com/bishopfox/sliver/protobuf/rpcpb"
"github.com/bishopfox/sliver/protobuf/sliverpb"
"google.golang.org/grpc"
)
type portfwdCloseObservation struct {
request *sliverpb.Tunnel
hasDeadline bool
deadlineRemaining time.Duration
}
type portfwdLifecycleTestStream struct {
grpc.BidiStreamingClient[sliverpb.TunnelData, sliverpb.TunnelData]
sent chan *sliverpb.TunnelData
dataEntered chan struct{}
releaseData chan struct{}
dataEnteredOnce sync.Once
releaseDataOnce sync.Once
}
type portfwdBlockingAddr string
func (address portfwdBlockingAddr) Network() string { return "test" }
func (address portfwdBlockingAddr) String() string { return string(address) }
type portfwdBlockingConn struct {
readStarted chan struct{}
releaseRead chan struct{}
closed chan struct{}
readOnce sync.Once
releaseOnce sync.Once
closeOnce sync.Once
}
func newPortfwdBlockingConn() *portfwdBlockingConn {
return &portfwdBlockingConn{
readStarted: make(chan struct{}),
releaseRead: make(chan struct{}),
closed: make(chan struct{}),
}
}
func (connection *portfwdBlockingConn) Read([]byte) (int, error) {
connection.readOnce.Do(func() { close(connection.readStarted) })
<-connection.releaseRead
return 0, io.EOF
}
func (connection *portfwdBlockingConn) Write(data []byte) (int, error) {
select {
case <-connection.closed:
return 0, net.ErrClosed
default:
return len(data), nil
}
}
func (connection *portfwdBlockingConn) Close() error {
connection.closeOnce.Do(func() { close(connection.closed) })
return nil
}
func (connection *portfwdBlockingConn) LocalAddr() net.Addr {
return portfwdBlockingAddr("local")
}
func (connection *portfwdBlockingConn) RemoteAddr() net.Addr {
return portfwdBlockingAddr("remote")
}
func (connection *portfwdBlockingConn) SetDeadline(time.Time) error { return nil }
func (connection *portfwdBlockingConn) SetReadDeadline(time.Time) error { return nil }
func (connection *portfwdBlockingConn) SetWriteDeadline(time.Time) error { return nil }
func (connection *portfwdBlockingConn) release() {
connection.releaseOnce.Do(func() { close(connection.releaseRead) })
}
func (stream *portfwdLifecycleTestStream) Send(data *sliverpb.TunnelData) error {
if len(data.GetData()) > 0 && stream.dataEntered != nil {
stream.dataEnteredOnce.Do(func() { close(stream.dataEntered) })
<-stream.releaseData
}
stream.sent <- data
return nil
}
func (stream *portfwdLifecycleTestStream) releaseBlockedData() {
if stream.releaseData != nil {
stream.releaseDataOnce.Do(func() { close(stream.releaseData) })
}
}
type portfwdLifecycleTestRPC struct {
rpcpb.SliverRPCClient
createResponse *sliverpb.Tunnel
createErr error
portfwdResponse *sliverpb.Portfwd
portfwdErr error
portfwdCalls chan *sliverpb.PortfwdReq
closeCalls chan portfwdCloseObservation
createCalls chan *sliverpb.Tunnel
blockCreate bool
createEntered chan struct{}
createCanceled chan struct{}
createOnce sync.Once
cancelOnce sync.Once
}
func newPortfwdLifecycleTestRPC(tunnelID uint64, sessionID string) *portfwdLifecycleTestRPC {
return &portfwdLifecycleTestRPC{
createResponse: &sliverpb.Tunnel{TunnelID: tunnelID, SessionID: sessionID},
portfwdResponse: &sliverpb.Portfwd{Response: &commonpb.Response{}},
portfwdCalls: make(chan *sliverpb.PortfwdReq, 4),
closeCalls: make(chan portfwdCloseObservation, 4),
createCalls: make(chan *sliverpb.Tunnel, 4),
}
}
func (rpc *portfwdLifecycleTestRPC) CreateTunnel(ctx context.Context, request *sliverpb.Tunnel, _ ...grpc.CallOption) (*sliverpb.Tunnel, error) {
rpc.createCalls <- request
if rpc.blockCreate {
rpc.createOnce.Do(func() { close(rpc.createEntered) })
<-ctx.Done()
rpc.cancelOnce.Do(func() { close(rpc.createCanceled) })
return nil, ctx.Err()
}
return rpc.createResponse, rpc.createErr
}
func (rpc *portfwdLifecycleTestRPC) Portfwd(_ context.Context, request *sliverpb.PortfwdReq, _ ...grpc.CallOption) (*sliverpb.Portfwd, error) {
rpc.portfwdCalls <- request
return rpc.portfwdResponse, rpc.portfwdErr
}
func (rpc *portfwdLifecycleTestRPC) CloseTunnel(ctx context.Context, request *sliverpb.Tunnel, _ ...grpc.CallOption) (*commonpb.Empty, error) {
deadline, hasDeadline := ctx.Deadline()
observation := portfwdCloseObservation{
request: request,
hasDeadline: hasDeadline,
}
if hasDeadline {
observation.deadlineRemaining = time.Until(deadline)
}
rpc.closeCalls <- observation
return &commonpb.Empty{}, nil
}
func TestChannelProxyWaitsForTunnelBindBeforePortfwd(t *testing.T) {
resetPortfwdLifecycleTestState(t)
stream := installPortfwdLifecycleTestStream()
const (
tunnelID = uint64(301)
sessionID = "portfwd-bind-session"
)
rpc := newPortfwdLifecycleTestRPC(tunnelID, sessionID)
proxy := &ChannelProxy{
Rpc: rpc,
Session: &clientpb.Session{ID: sessionID},
RemoteAddr: "127.0.0.1:8080",
}
type dialResult struct {
tunnel *TunnelIO
err error
}
result := make(chan dialResult, 1)
go func() {
tunnel, err := proxy.dialImplant(context.Background())
result <- dialResult{tunnel: tunnel, err: err}
}()
tunnel := waitForPortfwdLifecycleTunnel(t, tunnelID)
waitForPortfwdLifecycleBindFrame(t, stream, tunnelID)
select {
case <-rpc.portfwdCalls:
t.Fatal("Portfwd RPC ran before the tunnel bind acknowledgement")
case <-time.After(50 * time.Millisecond):
}
tunnel.markBound()
select {
case request := <-rpc.portfwdCalls:
if request.TunnelID != tunnelID {
t.Fatalf("Portfwd tunnel ID = %d, want %d", request.TunnelID, tunnelID)
}
case <-time.After(time.Second):
t.Fatal("Portfwd RPC did not run after tunnel bind acknowledgement")
}
select {
case outcome := <-result:
if outcome.err != nil {
t.Fatalf("dialImplant: %v", outcome.err)
}
if outcome.tunnel != tunnel {
t.Fatal("dialImplant returned a different tunnel generation")
}
if err := proxy.closeTunnel(outcome.tunnel); err != nil {
t.Fatalf("close test tunnel: %v", err)
}
case <-time.After(time.Second):
t.Fatal("dialImplant did not return after Portfwd response")
}
}
//nolint:gocyclo // Final payload ordering and both local/remote cleanup form one relay lifecycle.
func TestChannelProxyForwardsFinalPayloadBeforeCloseTunnel(t *testing.T) {
resetPortfwdLifecycleTestState(t)
stream := &portfwdLifecycleTestStream{
sent: make(chan *sliverpb.TunnelData, 4),
dataEntered: make(chan struct{}),
releaseData: make(chan struct{}),
}
t.Cleanup(stream.releaseBlockedData)
GetTunnels().SetStream(stream)
const (
tunnelID = uint64(311)
sessionID = "portfwd-final-payload-session"
)
rpc := newPortfwdLifecycleTestRPC(tunnelID, sessionID)
proxy := &ChannelProxy{
Rpc: rpc,
Session: &clientpb.Session{ID: sessionID},
RemoteAddr: "127.0.0.1:8080",
}
serverConnection, clientConnection := net.Pipe()
t.Cleanup(func() {
_ = serverConnection.Close()
_ = clientConnection.Close()
})
handlerDone := make(chan struct{})
go func() {
proxy.HandleConn(serverConnection)
close(handlerDone)
}()
tunnel := waitForPortfwdLifecycleTunnel(t, tunnelID)
waitForPortfwdLifecycleBindFrame(t, stream, tunnelID)
tunnel.markBound()
select {
case <-rpc.portfwdCalls:
case <-time.After(time.Second):
t.Fatal("final-payload handler did not establish its port forward")
}
payload := []byte("last local bytes before EOF")
localWriteDone := make(chan error, 1)
go func() {
_, err := clientConnection.Write(payload)
if closeErr := clientConnection.Close(); err == nil {
err = closeErr
}
localWriteDone <- err
}()
select {
case <-stream.dataEntered:
case <-time.After(time.Second):
t.Fatal("final payload did not enter the tunnel stream Send")
}
select {
case err := <-localWriteDone:
if err != nil {
t.Fatalf("write final local payload: %v", err)
}
case <-time.After(time.Second):
t.Fatal("local writer did not finish")
}
select {
case observation := <-rpc.closeCalls:
t.Fatalf("CloseTunnel overtook the blocked final stream Send: %+v", observation.request)
case <-time.After(50 * time.Millisecond):
}
select {
case <-handlerDone:
t.Fatal("port-forward handler returned before the final stream Send completed")
default:
}
stream.releaseBlockedData()
select {
case frame := <-stream.sent:
if frame.GetTunnelID() != tunnelID || !bytes.Equal(frame.GetData(), payload) {
t.Fatalf("final stream frame = %+v, want tunnel=%d data=%q", frame, tunnelID, payload)
}
case <-time.After(time.Second):
t.Fatal("final payload was not sent after the stream was released")
}
assertBoundedPortfwdClose(t, rpc.closeCalls, tunnelID, sessionID)
select {
case <-handlerDone:
case <-time.After(time.Second):
t.Fatal("port-forward handler did not finish after sending the final payload")
}
}
func TestChannelProxyRejectsInvalidRemoteBeforeCreateTunnel(t *testing.T) {
resetPortfwdLifecycleTestState(t)
rpc := newPortfwdLifecycleTestRPC(308, "invalid-remote-session")
proxy := &ChannelProxy{
Rpc: rpc,
Session: &clientpb.Session{ID: "invalid-remote-session"},
RemoteAddr: "target.example:not-a-port",
}
tunnel, err := proxy.dialImplant(context.Background())
if err == nil {
t.Fatal("dialImplant accepted an invalid remote target")
}
if tunnel != nil {
t.Fatal("dialImplant returned a tunnel for an invalid remote target")
}
select {
case request := <-rpc.createCalls:
t.Fatalf("invalid remote target invoked CreateTunnel with %+v", request)
default:
}
if active := GetTunnels().Get(308); active != nil {
t.Fatal("invalid remote target registered a client tunnel")
}
}
//nolint:gocyclo // The compatibility table verifies both legacy and strict result contracts.
func TestChannelProxyHostPortCompatibilityAndValidatedTargets(t *testing.T) {
tests := []struct {
name string
remoteAddr string
wantHost string
wantPort uint32
}{
{name: "hostname", remoteAddr: "target.example:443", wantHost: "target.example", wantPort: 443},
{name: "IPv4", remoteAddr: "192.0.2.10:8080", wantHost: "192.0.2.10", wantPort: 8080},
{name: "IPv6", remoteAddr: "[2001:db8::10]:8443", wantHost: "2001:db8::10", wantPort: 8443},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
proxy := &ChannelProxy{RemoteAddr: test.remoteAddr}
host, port := proxy.HostPort()
if host != test.wantHost || port != test.wantPort {
t.Fatalf("HostPort(%q) = (%q, %d), want (%q, %d)", test.remoteAddr, host, port, test.wantHost, test.wantPort)
}
host, port, err := proxy.ValidatedHostPort()
if err != nil {
t.Fatalf("ValidatedHostPort(%q): %v", test.remoteAddr, err)
}
if host != test.wantHost || port != test.wantPort {
t.Fatalf("ValidatedHostPort(%q) = (%q, %d), want (%q, %d)", test.remoteAddr, host, port, test.wantHost, test.wantPort)
}
})
}
proxy := &ChannelProxy{RemoteAddr: "target.example:not-a-port"}
if host, port := proxy.HostPort(); host != "" || port != 8080 {
t.Fatalf("legacy HostPort invalid fallback = (%q, %d), want (\"\", 8080)", host, port)
}
if host, port, err := proxy.ValidatedHostPort(); err == nil || host != "" || port != 0 {
t.Fatalf("ValidatedHostPort invalid result = (%q, %d, %v), want empty/zero/error", host, port, err)
}
// HostPort historically allowed an empty host and returned its parsed port.
// Validation-sensitive setup rejects the same value through the new API.
proxy.RemoteAddr = ":80"
if host, port := proxy.HostPort(); host != "" || port != 80 {
t.Fatalf("legacy HostPort empty-host result = (%q, %d), want (\"\", 80)", host, port)
}
if host, port, err := proxy.ValidatedHostPort(); err == nil || host != "" || port != 0 {
t.Fatalf("ValidatedHostPort empty-host result = (%q, %d, %v), want empty/zero/error", host, port, err)
}
for _, remoteAddr := range []string{
"bad host:80",
"bad\thost:80",
"bad\nhost:80",
"[fe80::1%bad zone]:80",
} {
proxy.RemoteAddr = remoteAddr
if host, port, err := proxy.ValidatedHostPort(); err == nil || host != "" || port != 0 {
t.Errorf("ValidatedHostPort whitespace/control result for %q = (%q, %d, %v), want empty/zero/error", remoteAddr, host, port, err)
}
}
}
func TestChannelProxySetupFailureClosesClientAndServerTunnel(t *testing.T) {
resetPortfwdLifecycleTestState(t)
stream := installPortfwdLifecycleTestStream()
const (
tunnelID = uint64(302)
sessionID = "portfwd-failure-session"
)
portfwdErr := errors.New("test Portfwd RPC failure")
rpc := newPortfwdLifecycleTestRPC(tunnelID, sessionID)
rpc.portfwdErr = portfwdErr
proxy := &ChannelProxy{
Rpc: rpc,
Session: &clientpb.Session{ID: sessionID},
RemoteAddr: "127.0.0.1:8080",
}
result := make(chan error, 1)
go func() {
_, err := proxy.dialImplant(context.Background())
result <- err
}()
tunnel := waitForPortfwdLifecycleTunnel(t, tunnelID)
waitForPortfwdLifecycleBindFrame(t, stream, tunnelID)
tunnel.markBound()
select {
case err := <-result:
if !errors.Is(err, portfwdErr) {
t.Fatalf("dialImplant error = %v, want %v", err, portfwdErr)
}
case <-time.After(time.Second):
t.Fatal("dialImplant setup failure did not return")
}
if active := GetTunnels().Get(tunnelID); active != nil {
t.Fatal("failed setup retained the client tunnel")
}
assertBoundedPortfwdClose(t, rpc.closeCalls, tunnelID, sessionID)
}
func TestChannelProxyImplantSetupErrorClosesClientAndServerTunnel(t *testing.T) {
resetPortfwdLifecycleTestState(t)
stream := installPortfwdLifecycleTestStream()
const (
tunnelID = uint64(305)
sessionID = "portfwd-implant-failure-session"
)
rpc := newPortfwdLifecycleTestRPC(tunnelID, sessionID)
rpc.portfwdResponse.Response.Err = "target dial refused"
proxy := &ChannelProxy{
Rpc: rpc,
Session: &clientpb.Session{ID: sessionID},
RemoteAddr: "127.0.0.1:8080",
}
result := make(chan error, 1)
go func() {
_, err := proxy.dialImplant(context.Background())
result <- err
}()
tunnel := waitForPortfwdLifecycleTunnel(t, tunnelID)
waitForPortfwdLifecycleBindFrame(t, stream, tunnelID)
tunnel.markBound()
select {
case err := <-result:
if err == nil || err.Error() != "target dial refused" {
t.Fatalf("dialImplant error = %v, want target dial refused", err)
}
case <-time.After(time.Second):
t.Fatal("dialImplant implant failure did not return")
}
if active := GetTunnels().Get(tunnelID); active != nil {
t.Fatal("implant setup failure retained the client tunnel")
}
assertBoundedPortfwdClose(t, rpc.closeCalls, tunnelID, sessionID)
}
func TestChannelProxyClosesAcceptedConnectionWhenCreateTunnelFails(t *testing.T) {
resetPortfwdLifecycleTestState(t)
rpc := newPortfwdLifecycleTestRPC(303, "portfwd-create-failure")
rpc.createErr = errors.New("test CreateTunnel failure")
proxy := &ChannelProxy{
Rpc: rpc,
Session: &clientpb.Session{ID: "portfwd-create-failure"},
RemoteAddr: "127.0.0.1:8080",
}
serverConnection, clientConnection := net.Pipe()
defer func() { _ = clientConnection.Close() }()
handlerDone := make(chan struct{})
go func() {
proxy.HandleConn(serverConnection)
close(handlerDone)
}()
if err := clientConnection.SetReadDeadline(time.Now().Add(time.Second)); err != nil {
t.Fatal(err)
}
buffer := make([]byte, 1)
count, err := clientConnection.Read(buffer)
if count != 0 || !errors.Is(err, io.EOF) {
t.Fatalf("accepted connection read = (%d, %v), want (0, EOF)", count, err)
}
select {
case <-handlerDone:
case <-time.After(time.Second):
t.Fatal("HandleConn did not return after CreateTunnel failure")
}
}
func TestChannelProxyStopCancelsCreateTunnelWithoutDialTimeout(t *testing.T) {
resetPortfwdLifecycleTestState(t)
rpc := newPortfwdLifecycleTestRPC(306, "portfwd-cancel-create")
rpc.blockCreate = true
rpc.createEntered = make(chan struct{})
rpc.createCanceled = make(chan struct{})
proxy := &ChannelProxy{
Rpc: rpc,
Session: &clientpb.Session{ID: "portfwd-cancel-create"},
RemoteAddr: "127.0.0.1:8080",
DialTimeout: -1,
}
serverConnection, clientConnection := net.Pipe()
defer func() { _ = clientConnection.Close() }()
handlerDone := make(chan struct{})
go func() {
proxy.HandleConn(serverConnection)
close(handlerDone)
}()
select {
case <-rpc.createEntered:
case <-time.After(time.Second):
t.Fatal("CreateTunnel did not start")
}
proxy.Stop()
select {
case <-rpc.createCanceled:
case <-time.After(time.Second):
t.Fatal("Stop did not cancel the in-flight CreateTunnel RPC")
}
select {
case <-handlerDone:
case <-time.After(time.Second):
t.Fatal("HandleConn did not return after Stop canceled CreateTunnel")
}
if err := clientConnection.SetReadDeadline(time.Now().Add(time.Second)); err == nil {
buffer := make([]byte, 1)
count, readErr := clientConnection.Read(buffer)
if count != 0 || (!errors.Is(readErr, io.EOF) && !errors.Is(readErr, io.ErrClosedPipe)) {
t.Fatalf("connection read after Stop = (%d, %v), want (0, EOF or closed pipe)", count, readErr)
}
} else if !errors.Is(err, io.ErrClosedPipe) {
t.Fatalf("set read deadline after Stop: %v", err)
}
}
func TestChannelProxyStopWaitsForBlockedHandler(t *testing.T) {
resetPortfwdLifecycleTestState(t)
stream := installPortfwdLifecycleTestStream()
const (
tunnelID = uint64(309)
sessionID = "portfwd-stop-join-session"
)
rpc := newPortfwdLifecycleTestRPC(tunnelID, sessionID)
proxy := &ChannelProxy{
Rpc: rpc,
Session: &clientpb.Session{ID: sessionID},
RemoteAddr: "127.0.0.1:8080",
}
connection := newPortfwdBlockingConn()
t.Cleanup(connection.release)
handlerDone := make(chan struct{})
go func() {
proxy.HandleConn(connection)
close(handlerDone)
}()
tunnel := waitForPortfwdLifecycleTunnel(t, tunnelID)
waitForPortfwdLifecycleBindFrame(t, stream, tunnelID)
tunnel.markBound()
select {
case <-rpc.portfwdCalls:
case <-time.After(time.Second):
t.Fatal("blocked handler did not establish its port forward")
}
select {
case <-connection.readStarted:
case <-time.After(time.Second):
t.Fatal("handler copy loop did not enter the blocking connection read")
}
stopDone := make(chan struct{})
go func() {
proxy.Stop()
close(stopDone)
}()
select {
case <-connection.closed:
case <-time.After(time.Second):
t.Fatal("Stop did not close the tracked connection")
}
select {
case <-stopDone:
t.Fatal("Stop returned before the blocked handler copy loop joined")
case <-time.After(50 * time.Millisecond):
}
connection.release()
select {
case <-stopDone:
case <-time.After(time.Second):
t.Fatal("Stop did not return after the blocked handler was released")
}
select {
case <-handlerDone:
case <-time.After(time.Second):
t.Fatal("HandleConn did not return before Stop completed")
}
if active := GetTunnels().Get(tunnelID); active != nil {
t.Fatal("joined handler retained its client tunnel")
}
assertBoundedPortfwdClose(t, rpc.closeCalls, tunnelID, sessionID)
}
func TestChannelProxyReceiveOverflowClosesLocalAndRemoteTunnel(t *testing.T) {
resetPortfwdLifecycleTestState(t)
stream := installPortfwdLifecycleTestStream()
const (
tunnelID = uint64(310)
sessionID = "portfwd-overflow-session"
)
rpc := newPortfwdLifecycleTestRPC(tunnelID, sessionID)
proxy := &ChannelProxy{
Rpc: rpc,
Session: &clientpb.Session{ID: sessionID},
RemoteAddr: "127.0.0.1:8080",
}
serverConnection, clientConnection := net.Pipe()
t.Cleanup(func() { _ = clientConnection.Close() })
handlerDone := make(chan struct{})
go func() {
proxy.HandleConn(serverConnection)
close(handlerDone)
}()
tunnel := waitForPortfwdLifecycleTunnel(t, tunnelID)
waitForPortfwdLifecycleBindFrame(t, stream, tunnelID)
tunnel.markBound()
select {
case <-rpc.portfwdCalls:
case <-time.After(time.Second):
t.Fatal("overflow test did not establish its port forward")
}
var admissionErr error
for index := 0; index < tunnelRecvBufferSize+2; index++ {
admissionErr = tunnel.RecvData([]byte{byte(index)})
if admissionErr != nil {
break
}
}
if !errors.Is(admissionErr, errTunnelReceiveQueueFull) {
t.Fatalf("receive overflow error = %v, want %v", admissionErr, errTunnelReceiveQueueFull)
}
tunnel.failReceive()
GetTunnels().CloseIf(tunnel)
if err := clientConnection.SetReadDeadline(time.Now().Add(time.Second)); err == nil {
for {
if _, err := clientConnection.Read(make([]byte, 1)); err != nil {
break
}
}
} else if !errors.Is(err, io.ErrClosedPipe) {
t.Fatalf("set read deadline after receive overflow: %v", err)
}
select {
case <-handlerDone:
case <-time.After(time.Second):
t.Fatal("receive overflow did not stop the port-forward handler")
}
assertBoundedPortfwdClose(t, rpc.closeCalls, tunnelID, sessionID)
}
func TestPortfwdsRemoveClosesEstablishedConnectionAndTunnel(t *testing.T) {
resetPortfwdLifecycleTestState(t)
stream := installPortfwdLifecycleTestStream()
const (
tunnelID = uint64(304)
sessionID = "portfwd-remove-session"
)
rpc := newPortfwdLifecycleTestRPC(tunnelID, sessionID)
channelProxy := &ChannelProxy{
Rpc: rpc,
Session: &clientpb.Session{ID: sessionID},
RemoteAddr: "127.0.0.1:8080",
}
registered := Portfwds.Add(&tcpproxy.Proxy{}, channelProxy)
serverConnection, clientConnection := net.Pipe()
defer func() { _ = clientConnection.Close() }()
handlerDone := make(chan struct{})
go func() {
channelProxy.HandleConn(serverConnection)
close(handlerDone)
}()
tunnel := waitForPortfwdLifecycleTunnel(t, tunnelID)
waitForPortfwdLifecycleBindFrame(t, stream, tunnelID)
tunnel.markBound()
select {
case <-rpc.portfwdCalls:
case <-time.After(time.Second):
t.Fatal("established connection did not invoke Portfwd")
}
if !Portfwds.Remove(registered.ID) {
t.Fatal("Portfwds.Remove did not remove the registered proxy")
}
if err := clientConnection.SetReadDeadline(time.Now().Add(time.Second)); err != nil {
// net.Pipe may report the peer teardown while setting the deadline,
// before a subsequent Read can observe EOF.
if !errors.Is(err, io.ErrClosedPipe) {
t.Fatalf("set read deadline after remove: %v", err)
}
} else {
buffer := make([]byte, 1)
count, err := clientConnection.Read(buffer)
if count != 0 || (!errors.Is(err, io.EOF) && !errors.Is(err, io.ErrClosedPipe)) {
t.Fatalf("established connection read after remove = (%d, %v), want (0, EOF or closed pipe)", count, err)
}
}
select {
case <-handlerDone:
case <-time.After(time.Second):
t.Fatal("established handler did not stop after Portfwds.Remove")
}
if active := GetTunnels().Get(tunnelID); active != nil {
t.Fatal("Portfwds.Remove retained the client tunnel")
}
assertBoundedPortfwdClose(t, rpc.closeCalls, tunnelID, sessionID)
}
func TestChannelProxyStaleCloseDoesNotCloseReplacementTunnel(t *testing.T) {
resetPortfwdLifecycleTestState(t)
stream := installPortfwdLifecycleTestStream()
const tunnelID = uint64(307)
rpc := newPortfwdLifecycleTestRPC(tunnelID, "old-session")
proxy := &ChannelProxy{Rpc: rpc}
old := GetTunnels().Start(tunnelID, "old-session")
waitForPortfwdLifecycleBindFrame(t, stream, tunnelID)
replacement := GetTunnels().Start(tunnelID, "new-session")
waitForPortfwdLifecycleBindFrame(t, stream, tunnelID)
if err := proxy.closeTunnel(old); err != nil {
t.Fatalf("close stale port forward tunnel: %v", err)
}
if current := GetTunnels().Get(tunnelID); current != replacement {
t.Fatal("stale port forward cleanup removed the replacement tunnel")
}
select {
case <-replacement.Done():
t.Fatal("stale port forward cleanup closed the replacement tunnel")
default:
}
assertBoundedPortfwdClose(t, rpc.closeCalls, tunnelID, "old-session")
}
func resetPortfwdLifecycleTestState(t *testing.T) {
t.Helper()
for _, metadata := range Portfwds.List() {
Portfwds.Remove(metadata.ID)
}
GetTunnels().Reset()
t.Cleanup(func() {
for _, metadata := range Portfwds.List() {
Portfwds.Remove(metadata.ID)
}
GetTunnels().Reset()
})
}
func waitForPortfwdLifecycleTunnel(t *testing.T, tunnelID uint64) *TunnelIO {
t.Helper()
deadline := time.Now().Add(time.Second)
for time.Now().Before(deadline) {
if tunnel := GetTunnels().Get(tunnelID); tunnel != nil {
return tunnel
}
time.Sleep(time.Millisecond)
}
t.Fatalf("timed out waiting for client tunnel %d", tunnelID)
return nil
}
func installPortfwdLifecycleTestStream() *portfwdLifecycleTestStream {
stream := &portfwdLifecycleTestStream{sent: make(chan *sliverpb.TunnelData, 4)}
GetTunnels().SetStream(stream)
return stream
}
func waitForPortfwdLifecycleBindFrame(t *testing.T, stream *portfwdLifecycleTestStream, tunnelID uint64) {
t.Helper()
select {
case frame := <-stream.sent:
if frame.GetTunnelID() != tunnelID || len(frame.GetData()) != 0 {
t.Fatalf("bind frame = %+v, want empty data for tunnel %d", frame, tunnelID)
}
case <-time.After(time.Second):
t.Fatalf("timed out waiting for tunnel %d bind frame", tunnelID)
}
}
func assertBoundedPortfwdClose(t *testing.T, observations <-chan portfwdCloseObservation, tunnelID uint64, sessionID string) {
t.Helper()
select {
case observation := <-observations:
if observation.request.GetTunnelID() != tunnelID || observation.request.GetSessionID() != sessionID {
t.Fatalf("CloseTunnel request = %+v, want tunnel=%d session=%q", observation.request, tunnelID, sessionID)
}
if !observation.hasDeadline {
t.Fatal("CloseTunnel RPC context has no deadline")
}
if observation.deadlineRemaining <= 0 || observation.deadlineRemaining > portfwdCloseTimeout {
t.Fatalf("CloseTunnel deadline remaining = %v, want within (0, %v]", observation.deadlineRemaining, portfwdCloseTimeout)
}
case <-time.After(time.Second):
t.Fatal("CloseTunnel RPC was not called")
}
}
+967 -109
View File
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+39 -9
View File
@@ -25,6 +25,8 @@ import (
"log"
"github.com/bishopfox/sliver/protobuf/rpcpb"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// TunnelLoop - Parses incoming tunnel messages and distributes them
@@ -32,6 +34,18 @@ import (
// to session/tunnel objects
// Expected to be called only once during initialization
func TunnelLoop(ctx context.Context, rpc rpcpb.SliverRPCClient) error {
return tunnelLoop(ctx, rpc, nil)
}
// TunnelLoopWithReady runs TunnelLoop and closes ready after the tunnel data
// stream has been installed. Callers can use that signal to avoid accepting
// local proxy connections before tunnel bind frames can be sent.
func TunnelLoopWithReady(ctx context.Context, rpc rpcpb.SliverRPCClient, ready chan<- struct{}) error {
return tunnelLoop(ctx, rpc, ready)
}
//nolint:gocyclo // The shared stream loop keeps receive, ownership, close, and cancellation decisions together.
func tunnelLoop(ctx context.Context, rpc rpcpb.SliverRPCClient, ready chan<- struct{}) error {
log.Println("Starting tunnel data loop ...")
defer log.Printf("Warning: TunnelLoop exited")
@@ -44,26 +58,42 @@ func TunnelLoop(ctx context.Context, rpc rpcpb.SliverRPCClient) error {
tunnels := GetTunnels()
streamGeneration := tunnels.SetStream(stream)
defer tunnels.CloseStream(streamGeneration)
if ready != nil {
close(ready)
}
for {
log.Printf("Waiting for TunnelData ...")
incoming, err := stream.Recv()
log.Printf("Recv stream msg: %v", incoming)
log.Printf("Recv stream err: %s", err)
if incoming != nil {
log.Printf(
"Recv stream msg: tunnel=%d session=%s bytes=%d closed=%t",
incoming.TunnelID,
incoming.SessionID,
len(incoming.Data),
incoming.Closed,
)
}
if err != nil {
log.Printf("Recv stream err: %s", err)
}
if err == io.EOF {
log.Printf("EOF Error: Tunnel data stream closed")
return nil
}
if err != nil {
if errors.Is(err, context.Canceled) {
// A canceled status is only a clean shutdown when this caller
// canceled the stream. A remote/server cancellation while ctx is
// still live is a real tunnel-loop failure and must be surfaced.
if ctx.Err() != nil && (errors.Is(err, context.Canceled) || status.Code(err) == codes.Canceled) {
return nil
}
log.Printf("Tunnel data read error: %s", err)
return err
}
log.Printf("Received TunnelData for tunnel %d", incoming.TunnelID)
tunnel := GetTunnels().Get(incoming.TunnelID)
tunnel := tunnels.Get(incoming.TunnelID)
if tunnel != nil {
if !incoming.Closed && len(incoming.GetData()) == 0 {
@@ -76,18 +106,18 @@ func TunnelLoop(ctx context.Context, rpc rpcpb.SliverRPCClient) error {
}
data := incoming.GetData()
log.Printf("This is data on tunnel %d: %s", tunnel.ID, data)
if !incoming.Closed {
log.Printf("Received data on tunnel %d", tunnel.ID)
log.Printf("Received %d byte(s) on tunnel %d", len(data), tunnel.ID)
err = tunnel.RecvData(data)
if err != nil {
log.Printf("Warning! Error sending data to tunnel %d, %v", tunnel.ID, err)
log.Printf("Warning! Closing tunnel %d after receive admission failed: %v", tunnel.ID, err)
tunnel.failReceive()
tunnels.CloseIf(tunnel)
}
} else {
log.Printf("Closing tunnel %d", tunnel.ID)
GetTunnels().Close(tunnel.ID)
tunnels.CloseIf(tunnel)
}
} else {
log.Printf("Received tunnel data for non-existent tunnel id %d", incoming.TunnelID)
+176 -38
View File
@@ -19,16 +19,33 @@ package core
*/
import (
"bytes"
"errors"
"io"
"log"
"sync"
"github.com/bishopfox/sliver/protobuf/sliverpb"
)
const tunnelRecvBufferSize = 16
const (
// HTTP C2 may deliver up to 64 concurrent responses while a command-specific
// reader is starting or briefly backpressured. Match the 128-frame generic
// server/implant transport window so that ordinary bursts remain isolated and
// bounded instead of resetting an otherwise healthy port forward.
tunnelRecvBufferSize = 128
tunnelRecvByteLimit = tunnelRecvBufferSize * sliverpb.MaxTunnelFrameBytes
)
var errClosedTunnel = errors.New("closed tunnel")
var (
errClosedTunnel = errors.New("closed tunnel")
errTunnelReceiveQueueFull = errors.New("tunnel receive queue capacity exceeded")
errTunnelReceiveFrameTooLarge = errors.New("tunnel receive frame exceeds payload limit")
)
type tunnelWriteRequest struct {
data []byte
result chan error
}
// TunnelIO - Duplex data tunnel, compatible with both io.ReadWriter
type TunnelIO struct {
@@ -38,12 +55,26 @@ type TunnelIO struct {
Send chan []byte
Recv chan []byte
done chan struct{}
closeOnce sync.Once
bound chan struct{}
boundOnce sync.Once
isOpen bool
mutex *sync.RWMutex
writeRequests chan tunnelWriteRequest
done chan struct{}
closeOnce sync.Once
recvFailed chan struct{}
recvFailedOnce sync.Once
bound chan struct{}
boundOnce sync.Once
readMu sync.Mutex
remainder []byte
recvBudgetMu sync.Mutex
recvFrames int
recvBytes int
recvFrameLimit int
recvByteLimit int
isOpen bool
mutex *sync.RWMutex
// readSelectHook is an unexported scheduling seam for deterministic tests of
// the close-vs-final-frame select. Production instances leave it nil.
readSelectHook func(doneSelected bool)
}
// NewTunnelIO - Single entry point for creating instance of new TunnelIO
@@ -57,15 +88,22 @@ func NewTunnelIO(tunnelID uint64, sessionID string) *TunnelIO {
// A tunnel can receive its first frames before the command-specific
// reader is running. Keep that short startup burst from blocking the
// single client TunnelLoop while retaining bounded backpressure.
Recv: make(chan []byte, tunnelRecvBufferSize),
done: make(chan struct{}),
bound: make(chan struct{}),
mutex: &sync.RWMutex{},
Recv: make(chan []byte, tunnelRecvBufferSize),
writeRequests: make(chan tunnelWriteRequest),
done: make(chan struct{}),
recvFailed: make(chan struct{}),
bound: make(chan struct{}),
recvFrameLimit: tunnelRecvBufferSize,
recvByteLimit: tunnelRecvByteLimit,
mutex: &sync.RWMutex{},
}
}
// Write - Writer method for interface
func (tun *TunnelIO) Write(data []byte) (int, error) {
if len(data) == 0 {
return 0, nil
}
if !tun.IsOpen() {
log.Printf("Warning: Write on closed tunnel %d", tun.ID)
return 0, io.EOF
@@ -75,8 +113,7 @@ func (tun *TunnelIO) Write(data []byte) (int, error) {
dataCopy := make([]byte, len(data))
n := copy(dataCopy, data)
log.Printf("Write %d bytes", n)
log.Printf("This bytes is: %s", dataCopy)
log.Printf("Write %d byte(s) on tunnel %d", n, tun.ID)
select {
case <-tun.done:
@@ -84,16 +121,49 @@ func (tun *TunnelIO) Write(data []byte) (int, error) {
default:
}
request := tunnelWriteRequest{
data: dataCopy,
result: make(chan error, 1),
}
select {
case tun.Send <- dataCopy:
return n, nil
case tun.writeRequests <- request:
case <-tun.done:
return 0, io.EOF
}
// A successful Write means the exact tunnel generation's gRPC stream Send
// completed. This prevents a full-close owner from deleting the generation
// after a channel handoff but before the final payload reaches the stream.
select {
case err := <-request.result:
if err != nil {
return 0, err
}
return n, nil
case <-tun.done:
// Prefer an already-published Send result when completion and close race.
select {
case err := <-request.result:
if err != nil {
return 0, err
}
return n, nil
default:
return 0, io.EOF
}
}
}
// Read - Reader method for interface
func (tun *TunnelIO) Read(data []byte) (int, error) {
if len(data) == 0 {
return 0, nil
}
tun.readMu.Lock()
defer tun.readMu.Unlock()
if len(tun.remainder) > 0 {
return tun.copyRecvData(data, nil), nil
}
// Prefer already queued output over the close signal so callers can drain
// the final frames before observing EOF.
select {
@@ -102,21 +172,53 @@ func (tun *TunnelIO) Read(data []byte) (int, error) {
default:
}
if tun.readSelectHook != nil {
tun.readSelectHook(false)
}
select {
case recvData := <-tun.Recv:
return tun.copyRecvData(data, recvData), nil
case <-tun.done:
log.Printf("Warning: Read on closed tunnel %d", tun.ID)
return 0, io.EOF
if tun.readSelectHook != nil {
tun.readSelectHook(true)
}
// RecvData that won admission immediately before Close may have queued
// after the first poll. Prefer that final admitted frame over EOF.
select {
case recvData := <-tun.Recv:
return tun.copyRecvData(data, recvData), nil
default:
log.Printf("Warning: Read on closed tunnel %d", tun.ID)
return 0, io.EOF
}
}
}
func (tun *TunnelIO) copyRecvData(data []byte, recvData []byte) int {
var buff bytes.Buffer
log.Printf("Read %d bytes", len(recvData))
buff.Write(recvData)
if recvData != nil {
tun.remainder = recvData
log.Printf("Read frame with %d byte(s) on tunnel %d", len(recvData), tun.ID)
}
n := copy(data, tun.remainder)
tun.remainder = tun.remainder[n:]
tun.releaseRecvBudget(n, len(tun.remainder) == 0)
if len(tun.remainder) == 0 {
tun.remainder = nil
}
return n
}
return copy(data, buff.Bytes())
func (tun *TunnelIO) releaseRecvBudget(bytes int, frameComplete bool) {
tun.recvBudgetMu.Lock()
if bytes >= tun.recvBytes {
tun.recvBytes = 0
} else {
tun.recvBytes -= bytes
}
if frameComplete && tun.recvFrames > 0 {
tun.recvFrames--
}
tun.recvBudgetMu.Unlock()
}
// Close - Close tunnel IO operations
@@ -147,6 +249,20 @@ func (tun *TunnelIO) Done() <-chan struct{} {
return tun.done
}
// receiveFailed is closed only for local receive-admission failure. Ordinary
// peer close leaves it open so owners can still drain already admitted final
// frames before closing their local endpoint.
func (tun *TunnelIO) receiveFailed() <-chan struct{} {
return tun.recvFailed
}
func (tun *TunnelIO) failReceive() {
tun.recvFailedOnce.Do(func() {
close(tun.recvFailed)
})
_ = tun.Close()
}
// Bound is closed after the server acknowledges the tunnel's streaming bind.
func (tun *TunnelIO) Bound() <-chan struct{} {
return tun.bound
@@ -185,23 +301,45 @@ func (tun *TunnelIO) Open() error {
return nil
}
// RecvData - safely queues data on the bounded internal Recv channel.
// It blocks only when that startup buffer is full.
// RecvData admits data to the bounded receive queue without blocking the
// process-wide TunnelLoop. The reservation includes unread short-buffer
// remainder, so both the frame count and byte footprint remain bounded.
func (tun *TunnelIO) RecvData(data []byte) error {
if !tun.IsOpen() {
return errClosedTunnel
}
select {
case <-tun.done:
return errClosedTunnel
default:
}
select {
case tun.Recv <- data:
if len(data) == 0 {
return nil
case <-tun.done:
}
if len(data) > sliverpb.MaxTunnelFrameBytes {
return errTunnelReceiveFrameTooLarge
}
tun.mutex.RLock()
defer tun.mutex.RUnlock()
if !tun.isOpen {
return errClosedTunnel
}
return tun.queueRecvData(data)
}
// queueRecvData completes admission after the caller has established that the
// tunnel is open. Keeping this step separate also lets tests hold the Read done
// branch at the exact historical race point without relying on scheduler luck.
func (tun *TunnelIO) queueRecvData(data []byte) error {
dataCopy := append([]byte(nil), data...)
tun.recvBudgetMu.Lock()
defer tun.recvBudgetMu.Unlock()
if tun.recvFrames >= tun.recvFrameLimit || tun.recvBytes+len(dataCopy) > tun.recvByteLimit {
return errTunnelReceiveQueueFull
}
tun.recvFrames++
tun.recvBytes += len(dataCopy)
select {
case tun.Recv <- dataCopy:
return nil
default:
tun.recvFrames--
tun.recvBytes -= len(dataCopy)
return errTunnelReceiveQueueFull
}
}
+183 -35
View File
@@ -4,8 +4,11 @@ import (
"bytes"
"errors"
"io"
"sync"
"testing"
"time"
"github.com/bishopfox/sliver/protobuf/sliverpb"
)
func TestTunnelIOBuffersDataBeforeReaderStarts(t *testing.T) {
@@ -46,56 +49,108 @@ func TestTunnelIOBuffersDataBeforeReaderStarts(t *testing.T) {
}
}
func TestTunnelIOCloseUnblocksFullReceiveBuffer(t *testing.T) {
func TestTunnelIOAdmitsHTTPTransportBurstAndDrainsInOrder(t *testing.T) {
const (
httpBurstFrames = 64
httpFrameBytes = 32 * 1024
)
tunnel := NewTunnelIO(10, "session")
if err := tunnel.Open(); err != nil {
t.Fatalf("open tunnel: %v", err)
}
t.Cleanup(func() { _ = tunnel.Close() })
// HTTP C2 may make this entire response window runnable before the local
// port-forward reader is scheduled. Admission must remain nonblocking and
// preserve every frame rather than treating that ordinary burst as overload.
for sequence := 0; sequence < httpBurstFrames; sequence++ {
payload := bytes.Repeat([]byte{byte(sequence)}, httpFrameBytes)
if err := tunnel.RecvData(payload); err != nil {
t.Fatalf("admit HTTP burst frame %d: %v", sequence, err)
}
}
if !tunnel.IsOpen() {
t.Fatal("ordinary HTTP response burst closed the tunnel")
}
buffer := make([]byte, httpFrameBytes)
for sequence := 0; sequence < httpBurstFrames; sequence++ {
n, err := tunnel.Read(buffer)
if err != nil {
t.Fatalf("drain HTTP burst frame %d: %v", sequence, err)
}
if n != len(buffer) {
t.Fatalf("drain HTTP burst frame %d length = %d, want %d", sequence, n, len(buffer))
}
want := byte(sequence)
for offset, got := range buffer {
if got != want {
t.Fatalf("drain HTTP burst frame %d byte %d = %d, want %d", sequence, offset, got, want)
}
}
}
tunnel.recvBudgetMu.Lock()
frames, size := tunnel.recvFrames, tunnel.recvBytes
tunnel.recvBudgetMu.Unlock()
if frames != 0 || size != 0 {
t.Fatalf("receive budget after HTTP burst drain = (%d frames, %d bytes), want zero", frames, size)
}
}
func TestTunnelIORejectsFullReceiveBufferWithoutBlocking(t *testing.T) {
tunnel := NewTunnelIO(2, "session")
if err := tunnel.Open(); err != nil {
t.Fatalf("open tunnel: %v", err)
}
t.Cleanup(func() { _ = tunnel.Close() })
for i := 0; i < tunnelRecvBufferSize; i++ {
recvDone := make(chan error, 1)
go func() {
recvDone <- tunnel.RecvData([]byte{byte(i)})
}()
select {
case err := <-recvDone:
if err != nil {
t.Fatalf("fill receive buffer frame %d: %v", i, err)
}
case <-time.After(time.Second):
t.Fatalf("frame %d blocked before receive buffer was full", i)
if err := tunnel.RecvData([]byte{byte(i)}); err != nil {
t.Fatalf("fill receive buffer frame %d: %v", i, err)
}
}
blockedRecv := make(chan error, 1)
recvDone := make(chan error, 1)
go func() {
blockedRecv <- tunnel.RecvData([]byte("seventeenth frame"))
recvDone <- tunnel.RecvData([]byte("frame beyond receive window"))
}()
select {
case err := <-blockedRecv:
t.Fatalf("frame beyond bounded receive capacity returned before close: %v", err)
case <-time.After(50 * time.Millisecond):
case err := <-recvDone:
if !errors.Is(err, errTunnelReceiveQueueFull) {
t.Fatalf("frame beyond bounded receive capacity = %v, want %v", err, errTunnelReceiveQueueFull)
}
case <-time.After(100 * time.Millisecond):
t.Fatal("frame beyond bounded receive capacity blocked")
}
}
func TestTunnelIOReceiveAdmissionBoundsBytesAndFrameSize(t *testing.T) {
tunnel := NewTunnelIO(8, "session")
if err := tunnel.Open(); err != nil {
t.Fatalf("open tunnel: %v", err)
}
t.Cleanup(func() { _ = tunnel.Close() })
// Use a small budget to exercise byte admission independently of the
// production frame-count window.
tunnel.recvByteLimit = 3
if err := tunnel.RecvData([]byte("abc")); err != nil {
t.Fatalf("fill receive byte budget: %v", err)
}
if err := tunnel.RecvData([]byte("d")); !errors.Is(err, errTunnelReceiveQueueFull) {
t.Fatalf("frame beyond receive byte budget = %v, want %v", err, errTunnelReceiveQueueFull)
}
closeDone := make(chan error, 1)
go func() {
closeDone <- tunnel.Close()
}()
select {
case err := <-closeDone:
if err != nil {
t.Fatalf("close full tunnel: %v", err)
}
case <-time.After(time.Second):
t.Fatal("Close blocked behind RecvData on a full receive buffer")
buffer := make([]byte, 3)
if n, err := tunnel.Read(buffer); n != 3 || err != nil || string(buffer) != "abc" {
t.Fatalf("read byte-budget frame = (%d, %q, %v)", n, buffer, err)
}
select {
case err := <-blockedRecv:
if !errors.Is(err, errClosedTunnel) {
t.Fatalf("blocked RecvData error = %v, want %v", err, errClosedTunnel)
}
case <-time.After(time.Second):
t.Fatal("blocked RecvData did not wake when tunnel closed")
if err := tunnel.RecvData([]byte("d")); err != nil {
t.Fatalf("admit after byte budget released: %v", err)
}
if err := tunnel.RecvData(make([]byte, sliverpb.MaxTunnelFrameBytes+1)); !errors.Is(err, errTunnelReceiveFrameTooLarge) {
t.Fatalf("oversized receive frame = %v, want %v", err, errTunnelReceiveFrameTooLarge)
}
}
@@ -203,3 +258,96 @@ func TestTunnelIOCloseBeforeOpenIsTerminal(t *testing.T) {
t.Fatal("tunnel reopened after terminal Close")
}
}
//nolint:gocyclo // Partial reads, terminal ordering, and EOF form one adapter contract.
func TestTunnelIOReadRetainsShortBufferRemainderAfterClose(t *testing.T) {
tunnel := NewTunnelIO(7, "session")
if err := tunnel.Open(); err != nil {
t.Fatalf("open tunnel: %v", err)
}
if n, err := tunnel.Read(make([]byte, 0)); n != 0 || err != nil {
t.Fatalf("zero-length read = (%d, %v), want (0, nil)", n, err)
}
if n, err := tunnel.Write(nil); n != 0 || err != nil {
t.Fatalf("zero-length write = (%d, %v), want (0, nil)", n, err)
}
select {
case data := <-tunnel.Send:
t.Fatalf("zero-length write queued data %x", data)
default:
}
if err := tunnel.RecvData([]byte("abcdef")); err != nil {
t.Fatalf("queue short-buffer payload: %v", err)
}
first := make([]byte, 2)
if n, err := tunnel.Read(first); n != 2 || err != nil || string(first) != "ab" {
t.Fatalf("first short read = (%d, %q, %v), want (2, ab, nil)", n, first, err)
}
if err := tunnel.Close(); err != nil {
t.Fatalf("close tunnel with remainder: %v", err)
}
second := make([]byte, 4)
if n, err := tunnel.Read(second); n != 4 || err != nil || string(second) != "cdef" {
t.Fatalf("remainder read = (%d, %q, %v), want (4, cdef, nil)", n, second, err)
}
if n, err := tunnel.Read(make([]byte, 1)); n != 0 || !errors.Is(err, io.EOF) {
t.Fatalf("post-remainder read = (%d, %v), want (0, EOF)", n, err)
}
}
func TestTunnelIOReadDrainsFrameAfterDoneCaseSelected(t *testing.T) {
tunnel := NewTunnelIO(9, "session")
if err := tunnel.Open(); err != nil {
t.Fatalf("open tunnel: %v", err)
}
selectReady := make(chan struct{})
doneSelected := make(chan struct{})
releaseDoneBranch := make(chan struct{})
var readyOnce sync.Once
var doneOnce sync.Once
tunnel.readSelectHook = func(selected bool) {
if !selected {
readyOnce.Do(func() { close(selectReady) })
return
}
doneOnce.Do(func() { close(doneSelected) })
<-releaseDoneBranch
}
type readResult struct {
data []byte
n int
err error
}
readDone := make(chan readResult, 1)
go func() {
buffer := make([]byte, 32)
n, err := tunnel.Read(buffer)
readDone <- readResult{data: buffer[:n], n: n, err: err}
}()
waitForTestSignal(t, selectReady, "tunnel Read blocking select")
if err := tunnel.Close(); err != nil {
t.Fatalf("close tunnel: %v", err)
}
waitForTestSignal(t, doneSelected, "tunnel Read done case")
payload := []byte("last admitted frame")
if err := tunnel.queueRecvData(payload); err != nil {
t.Fatalf("queue final admitted frame: %v", err)
}
close(releaseDoneBranch)
select {
case result := <-readDone:
if result.n != len(payload) || result.err != nil || !bytes.Equal(result.data, payload) {
t.Fatalf("Read after done case = (%d, %q, %v), want (%d, %q, nil)", result.n, result.data, result.err, len(payload), payload)
}
case <-time.After(time.Second):
t.Fatal("Read did not drain the final admitted frame")
}
tunnel.readSelectHook = nil
if n, err := tunnel.Read(make([]byte, 1)); n != 0 || !errors.Is(err, io.EOF) {
t.Fatalf("Read after final drain = (%d, %v), want (0, EOF)", n, err)
}
}
+217 -3
View File
@@ -11,17 +11,23 @@ import (
"github.com/bishopfox/sliver/protobuf/rpcpb"
"github.com/bishopfox/sliver/protobuf/sliverpb"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
type tunnelLoopTestRPC struct {
rpcpb.SliverRPCClient
stream *tunnelLoopTestStream
opened chan struct{}
stream *tunnelLoopTestStream
opened chan struct{}
startErr error
}
func (r *tunnelLoopTestRPC) TunnelData(ctx context.Context, _ ...grpc.CallOption) (grpc.BidiStreamingClient[sliverpb.TunnelData, sliverpb.TunnelData], error) {
r.stream.ctx = ctx
close(r.opened)
if r.startErr != nil {
return nil, r.startErr
}
r.stream.ctx = ctx
return r.stream, nil
}
@@ -79,6 +85,57 @@ func TestTunnelBindSendFailureClosesTunnel(t *testing.T) {
}
}
func TestTunnelLoopWithReadySignalsAfterStreamInstallation(t *testing.T) {
tunnels := GetTunnels()
tunnels.Reset()
stream := &tunnelLoopTestStream{
incoming: make(chan *sliverpb.TunnelData),
sent: make(chan *sliverpb.TunnelData, 1),
recvCalls: make(chan struct{}, 2),
}
rpc := &tunnelLoopTestRPC{stream: stream, opened: make(chan struct{})}
ctx, cancel := context.WithCancel(context.Background())
ready := make(chan struct{})
loopDone := make(chan error, 1)
go func() { loopDone <- TunnelLoopWithReady(ctx, rpc, ready) }()
t.Cleanup(func() {
cancel()
select {
case <-loopDone:
case <-time.After(time.Second):
t.Error("TunnelLoopWithReady did not stop after cancellation")
}
tunnels.Reset()
})
waitForTestSignal(t, ready, "TunnelLoop readiness")
tunnel := tunnels.Start(10, "session")
select {
case bind := <-stream.sent:
if bind.TunnelID != tunnel.ID {
t.Fatalf("bind tunnel ID = %d, want %d", bind.TunnelID, tunnel.ID)
}
case <-time.After(time.Second):
t.Fatal("ready tunnel loop did not send a bind frame")
}
}
func TestTunnelLoopWithReadyDoesNotSignalOnStartupFailure(t *testing.T) {
startErr := errors.New("test stream startup failure")
rpc := &tunnelLoopTestRPC{opened: make(chan struct{}), startErr: startErr}
ready := make(chan struct{})
err := TunnelLoopWithReady(context.Background(), rpc, ready)
if !errors.Is(err, startErr) {
t.Fatalf("TunnelLoopWithReady error = %v, want %v", err, startErr)
}
select {
case <-ready:
t.Fatal("readiness was signaled after stream startup failed")
default:
}
}
func (s *tunnelLoopTestStream) Recv() (*sliverpb.TunnelData, error) {
s.recvCalls <- struct{}{}
select {
@@ -180,6 +237,99 @@ func TestTunnelLoopFiltersEmptyBindAcknowledgement(t *testing.T) {
}
}
//nolint:gocyclo // The overload and sibling-isolation assertions share one stream lifecycle.
func TestTunnelLoopOverloadedTunnelDoesNotBlockHealthyTunnel(t *testing.T) {
tunnels := GetTunnels()
tunnels.Reset()
stream := &tunnelLoopTestStream{
incoming: make(chan *sliverpb.TunnelData, tunnelRecvBufferSize+4),
sent: make(chan *sliverpb.TunnelData, 2),
recvCalls: make(chan struct{}, tunnelRecvBufferSize+8),
}
rpc := &tunnelLoopTestRPC{stream: stream, opened: make(chan struct{})}
ctx, cancel := context.WithCancel(context.Background())
loopDone := make(chan error, 1)
go func() { loopDone <- TunnelLoop(ctx, rpc) }()
t.Cleanup(func() {
cancel()
select {
case <-loopDone:
case <-time.After(time.Second):
t.Error("TunnelLoop did not stop after cancellation")
}
tunnels.Reset()
})
waitForTestSignal(t, rpc.opened, "TunnelLoop stream startup")
waitForTestSignal(t, stream.recvCalls, "initial TunnelData receive")
stalled := tunnels.Start(11, "session")
healthy := tunnels.Start(12, "session")
bound := map[uint64]bool{}
for len(bound) < 2 {
select {
case frame := <-stream.sent:
bound[frame.TunnelID] = true
case <-time.After(time.Second):
t.Fatal("client did not send both tunnel bind frames")
}
}
for sequence := 0; sequence < tunnelRecvBufferSize; sequence++ {
stream.incoming <- &sliverpb.TunnelData{
TunnelID: stalled.ID,
SessionID: stalled.SessionID,
Data: []byte{byte(sequence)},
}
waitForTestSignal(t, stream.recvCalls, "receive after stalled tunnel frame")
}
stream.incoming <- &sliverpb.TunnelData{
TunnelID: stalled.ID,
SessionID: stalled.SessionID,
Data: []byte("overflow"),
}
waitForTestSignal(t, stream.recvCalls, "receive after stalled tunnel overflow")
healthyPayload := []byte("healthy tunnel data")
stream.incoming <- &sliverpb.TunnelData{
TunnelID: healthy.ID,
SessionID: healthy.SessionID,
Data: healthyPayload,
}
waitForTestSignal(t, stream.recvCalls, "receive after healthy tunnel frame")
select {
case <-stalled.Done():
case <-time.After(time.Second):
t.Fatal("overloaded tunnel was not closed")
}
if got := tunnels.Get(stalled.ID); got != nil {
t.Fatal("overloaded tunnel remained registered")
}
type healthyReadResult struct {
data []byte
n int
err error
}
healthyRead := make(chan healthyReadResult, 1)
go func() {
buffer := make([]byte, len(healthyPayload))
n, err := healthy.Read(buffer)
healthyRead <- healthyReadResult{data: buffer[:n], n: n, err: err}
}()
select {
case result := <-healthyRead:
if result.n != len(healthyPayload) || result.err != nil || !bytes.Equal(result.data, healthyPayload) {
t.Fatalf("healthy tunnel read = (%d, %q, %v), want (%d, %q, nil)", result.n, result.data, result.err, len(healthyPayload), healthyPayload)
}
case <-time.After(time.Second):
t.Fatal("healthy tunnel read blocked behind overloaded tunnel")
}
if !healthy.IsOpen() {
t.Fatal("healthy tunnel closed after unrelated tunnel overload")
}
}
func TestTunnelLoopClosesActiveTunnelsOnStreamEOF(t *testing.T) {
tunnels := GetTunnels()
tunnels.Reset()
@@ -225,6 +375,70 @@ func TestTunnelLoopClosesActiveTunnelsOnStreamEOF(t *testing.T) {
}
}
func TestTunnelLoopPropagatesRemoteGRPCCanceled(t *testing.T) {
tunnels := GetTunnels()
tunnels.Reset()
t.Cleanup(tunnels.Reset)
stream := &tunnelLoopTestStream{
incoming: make(chan *sliverpb.TunnelData),
sent: make(chan *sliverpb.TunnelData, 1),
recvCalls: make(chan struct{}, 2),
recvErr: make(chan error, 1),
}
rpc := &tunnelLoopTestRPC{stream: stream, opened: make(chan struct{})}
loopDone := make(chan error, 1)
go func() {
loopDone <- TunnelLoop(context.Background(), rpc)
}()
waitForTestSignal(t, rpc.opened, "TunnelLoop stream startup")
waitForTestSignal(t, stream.recvCalls, "initial TunnelData receive")
remoteErr := status.Error(codes.Canceled, "test remote shutdown")
stream.recvErr <- remoteErr
select {
case err := <-loopDone:
if status.Code(err) != codes.Canceled {
t.Fatalf("TunnelLoop remote canceled status result = %v, want %v", err, remoteErr)
}
case <-time.After(time.Second):
t.Fatal("TunnelLoop did not return after remote canceled gRPC status")
}
}
func TestTunnelLoopTreatsCallerCancellationAsCleanShutdown(t *testing.T) {
tunnels := GetTunnels()
tunnels.Reset()
t.Cleanup(tunnels.Reset)
stream := &tunnelLoopTestStream{
incoming: make(chan *sliverpb.TunnelData),
sent: make(chan *sliverpb.TunnelData, 1),
recvCalls: make(chan struct{}, 2),
recvErr: make(chan error, 1),
}
rpc := &tunnelLoopTestRPC{stream: stream, opened: make(chan struct{})}
ctx, cancel := context.WithCancel(context.Background())
loopDone := make(chan error, 1)
go func() {
loopDone <- TunnelLoop(ctx, rpc)
}()
waitForTestSignal(t, rpc.opened, "TunnelLoop stream startup")
waitForTestSignal(t, stream.recvCalls, "initial TunnelData receive")
cancel()
select {
case err := <-loopDone:
if err != nil {
t.Fatalf("TunnelLoop caller cancellation result = %v, want nil", err)
}
case <-time.After(time.Second):
t.Fatal("TunnelLoop did not return after caller cancellation")
}
}
func waitForTestSignal(t *testing.T, signal <-chan struct{}, description string) {
t.Helper()
select {
+83 -29
View File
@@ -34,6 +34,8 @@ var (
tunnelsSingletonLock = &sync.Mutex{}
)
var errSupersededTunnel = errors.New("superseded tunnel")
// GetTunnels - singleton function that returns or initializes all tunnels
func GetTunnels() *tunnels {
tunnelsSingletonLock.Lock()
@@ -98,10 +100,20 @@ func (t *tunnels) Get(tunnelID uint64) *TunnelIO {
// send - safe way to send a message to the stream
// protobuf stream allow only one writer at a time, so just in case there is a mutex for it
func (t *tunnels) send(tunnelData *sliverpb.TunnelData) error {
func (t *tunnels) send(expected *TunnelIO, tunnelData *sliverpb.TunnelData) error {
t.streamMutex.Lock()
defer t.streamMutex.Unlock()
// Start publishes replacement tunnel generations while holding streamMutex.
// Checking the exact pointer under the same lock ensures an old generation
// cannot send another frame after its replacement becomes visible.
t.mutex.RLock()
current := (*t.tunnels)[expected.ID]
t.mutex.RUnlock()
if current != expected {
return errSupersededTunnel
}
if t.stream == nil {
return errors.New("uninitizlied stream")
}
@@ -113,39 +125,59 @@ func (t *tunnels) send(tunnelData *sliverpb.TunnelData) error {
// Start - Add a tunnel to the core mapper
func (t *tunnels) Start(tunnelID uint64, sessionID string) *TunnelIO {
t.mutex.Lock()
defer t.mutex.Unlock()
tunnel := NewTunnelIO(tunnelID, sessionID)
if err := tunnel.Open(); err != nil {
log.Printf("Failed to open tunnel %d: %v", tunnelID, err)
return tunnel
}
// Publish the new generation atomically with respect to tunnel sends. If an
// ID is reused, the previous generation is made terminal before the new
// pointer becomes visible in the map.
t.streamMutex.Lock()
t.mutex.Lock()
previous := (*t.tunnels)[tunnelID]
if previous != nil {
_ = previous.Close()
}
(*t.tunnels)[tunnelID] = tunnel
t.mutex.Unlock()
t.streamMutex.Unlock()
go func(tunnel *TunnelIO) {
if err := tunnel.Open(); err != nil {
log.Printf("Failed to open tunnel %d: %v", tunnelID, err)
return
}
log.Printf("Tunnel now is open, %d", tunnelID)
for {
var (
data []byte
result chan error
)
select {
case <-tunnel.Done():
log.Printf("Tunnel send loop stopped. %d", tunnelID)
return
case data := <-tunnel.Send:
log.Printf("Send %d bytes on tunnel %d", len(data), tunnel.ID)
case request := <-tunnel.writeRequests:
data = request.data
result = request.result
case data = <-tunnel.Send:
}
err := t.send(&sliverpb.TunnelData{
TunnelID: tunnel.ID,
SessionID: tunnel.SessionID,
Data: data,
})
if err != nil {
log.Printf("Error sending, %s", err)
t.Close(tunnel.ID)
return
}
log.Printf("Send %d bytes on tunnel %d", len(data), tunnel.ID)
err := t.send(tunnel, &sliverpb.TunnelData{
TunnelID: tunnel.ID,
SessionID: tunnel.SessionID,
Data: data,
})
if result != nil {
// The result channel is buffered so a concurrent Close can wake the
// writer without stranding this exact-generation send loop. Publish
// the result before closing the tunnel on failure.
result <- err
}
if err != nil {
log.Printf("Error sending, %s", err)
t.CloseIf(tunnel)
return
}
}
}(tunnel)
@@ -167,26 +199,48 @@ func (t *tunnels) Close(tunnelID uint64) {
tunnel := (*t.tunnels)[tunnelID]
if tunnel != nil {
tunnel.Close()
_ = tunnel.Close()
delete((*t.tunnels), tunnelID)
}
}
// CloseIf closes and removes expected only when it is still the current
// generation registered for its numeric tunnel ID.
func (t *tunnels) CloseIf(expected *TunnelIO) bool {
if expected == nil {
return false
}
t.mutex.Lock()
defer t.mutex.Unlock()
current := (*t.tunnels)[expected.ID]
if current != expected {
return false
}
log.Printf("Closing tunnel %d", expected.ID)
_ = expected.Close()
delete(*t.tunnels, expected.ID)
return true
}
// CloseForSession - closing all tunnels for specified session id
func (t *tunnels) CloseForSession(sessionID string) {
t.mutex.RLock()
defer t.mutex.RUnlock()
log.Printf("Closing all tunnels for session %s", sessionID)
for tunnelID, tunnel := range *t.tunnels {
tunnels := []*TunnelIO{}
for _, tunnel := range *t.tunnels {
if tunnel.SessionID == sessionID {
// Weird way to avoid deadlocks but let it be
go func(tunnelID uint64) {
GetTunnels().Close(tunnelID)
}(tunnelID)
tunnels = append(tunnels, tunnel)
}
}
t.mutex.RUnlock()
for _, tunnel := range tunnels {
go t.CloseIf(tunnel)
}
}
// Reset closes all open tunnels and clears the underlying storage/stream.
+247
View File
@@ -0,0 +1,247 @@
package core
import (
"errors"
"io"
"sync"
"testing"
"time"
"github.com/bishopfox/sliver/protobuf/rpcpb"
"github.com/bishopfox/sliver/protobuf/sliverpb"
)
type tunnelGenerationTestStream struct {
rpcpb.SliverRPC_TunnelDataClient
sent chan *sliverpb.TunnelData
dataEntered chan struct{}
releaseData chan struct{}
dataErr error
enterOnce sync.Once
}
func (stream *tunnelGenerationTestStream) Send(frame *sliverpb.TunnelData) error {
if len(frame.GetData()) > 0 && stream.dataEntered != nil {
stream.enterOnce.Do(func() { close(stream.dataEntered) })
<-stream.releaseData
if stream.dataErr != nil {
return stream.dataErr
}
}
stream.sent <- frame
return nil
}
func TestTunnelsCloseIfDoesNotCloseReplacementGeneration(t *testing.T) {
tunnels := newTunnelGenerationTestStore()
stream := &tunnelGenerationTestStream{sent: make(chan *sliverpb.TunnelData, 2)}
tunnels.SetStream(stream)
old := tunnels.Start(41, "old-session")
waitForTunnelGenerationFrame(t, stream.sent)
replacement := tunnels.Start(41, "new-session")
waitForTunnelGenerationFrame(t, stream.sent)
select {
case <-old.Done():
default:
t.Fatal("replaced tunnel generation remained open")
}
if current := tunnels.Get(41); current != replacement {
t.Fatal("replacement tunnel generation was not published")
}
if tunnels.CloseIf(old) {
t.Fatal("CloseIf reported closing a superseded tunnel generation")
}
select {
case <-replacement.Done():
t.Fatal("stale CloseIf closed the replacement tunnel generation")
default:
}
if !tunnels.CloseIf(replacement) {
t.Fatal("CloseIf did not close the current tunnel generation")
}
}
func TestTunnelSendFailureCannotClosePublishedReplacement(t *testing.T) {
tunnels := newTunnelGenerationTestStore()
sendErr := errors.New("test old-generation send failure")
stream := &tunnelGenerationTestStream{
sent: make(chan *sliverpb.TunnelData, 2),
dataEntered: make(chan struct{}),
releaseData: make(chan struct{}),
dataErr: sendErr,
}
tunnels.SetStream(stream)
old := tunnels.Start(42, "old-session")
waitForTunnelGenerationFrame(t, stream.sent)
type writeResult struct {
n int
err error
}
writeDone := make(chan writeResult, 1)
go func() {
n, err := old.Write([]byte("old generation"))
writeDone <- writeResult{n: n, err: err}
}()
waitForTunnelGenerationSignal(t, stream.dataEntered, "old-generation stream send")
select {
case result := <-writeDone:
t.Fatalf("Write completed before stream Send: (%d, %v)", result.n, result.err)
case <-time.After(50 * time.Millisecond):
}
replacementDone := make(chan *TunnelIO, 1)
go func() {
replacementDone <- tunnels.Start(42, "new-session")
}()
select {
case replacement := <-replacementDone:
t.Fatalf("replacement %p was published before the old send completed", replacement)
case <-time.After(50 * time.Millisecond):
}
if current := tunnels.Get(42); current != old {
t.Fatal("old generation stopped being current while its stream send was in flight")
}
close(stream.releaseData)
select {
case result := <-writeDone:
if result.n != 0 || result.err == nil {
t.Fatalf("failed stream Write = (%d, %v), want (0, error)", result.n, result.err)
}
case <-time.After(time.Second):
t.Fatal("Write did not return after stream Send failed")
}
var replacement *TunnelIO
select {
case replacement = <-replacementDone:
case <-time.After(time.Second):
t.Fatal("replacement Start did not return after the old send completed")
}
waitForTunnelGenerationFrame(t, stream.sent)
if current := tunnels.Get(42); current != replacement {
t.Fatal("old send failure removed the published replacement generation")
}
select {
case <-replacement.Done():
t.Fatal("old send failure closed the published replacement generation")
default:
}
tunnels.CloseIf(replacement)
}
func TestTunnelWriteWaitsForSuccessfulExactGenerationStreamSend(t *testing.T) {
tunnels := newTunnelGenerationTestStore()
stream := &tunnelGenerationTestStream{
sent: make(chan *sliverpb.TunnelData, 2),
dataEntered: make(chan struct{}),
releaseData: make(chan struct{}),
}
tunnels.SetStream(stream)
tunnel := tunnels.Start(43, "final-write-session")
waitForTunnelGenerationFrame(t, stream.sent)
payload := []byte("final payload")
type writeResult struct {
n int
err error
}
writeDone := make(chan writeResult, 1)
go func() {
n, err := tunnel.Write(payload)
writeDone <- writeResult{n: n, err: err}
}()
waitForTunnelGenerationSignal(t, stream.dataEntered, "final stream send")
select {
case result := <-writeDone:
t.Fatalf("Write completed before stream Send: (%d, %v)", result.n, result.err)
case <-time.After(50 * time.Millisecond):
}
close(stream.releaseData)
select {
case result := <-writeDone:
if result.n != len(payload) || result.err != nil {
t.Fatalf("successful stream Write = (%d, %v), want (%d, nil)", result.n, result.err, len(payload))
}
case <-time.After(time.Second):
t.Fatal("Write did not complete after successful stream Send")
}
select {
case frame := <-stream.sent:
if string(frame.GetData()) != string(payload) {
t.Fatalf("stream payload = %q, want %q", frame.GetData(), payload)
}
case <-time.After(time.Second):
t.Fatal("successful Write returned before the payload was recorded")
}
if !tunnels.CloseIf(tunnel) {
t.Fatal("failed to close final-write tunnel")
}
}
func TestTunnelCloseWakesWriteWaitingForStreamSend(t *testing.T) {
tunnels := newTunnelGenerationTestStore()
stream := &tunnelGenerationTestStream{
sent: make(chan *sliverpb.TunnelData, 2),
dataEntered: make(chan struct{}),
releaseData: make(chan struct{}),
}
tunnels.SetStream(stream)
tunnel := tunnels.Start(44, "closing-write-session")
waitForTunnelGenerationFrame(t, stream.sent)
type writeResult struct {
n int
err error
}
writeDone := make(chan writeResult, 1)
go func() {
n, err := tunnel.Write([]byte("blocked payload"))
writeDone <- writeResult{n: n, err: err}
}()
waitForTunnelGenerationSignal(t, stream.dataEntered, "blocked stream send")
if !tunnels.CloseIf(tunnel) {
t.Fatal("failed to close tunnel with in-flight Write")
}
select {
case result := <-writeDone:
if result.n != 0 || !errors.Is(result.err, io.EOF) {
t.Fatalf("Write after concurrent close = (%d, %v), want (0, EOF)", result.n, result.err)
}
case <-time.After(time.Second):
t.Fatal("Close did not wake Write waiting for stream Send")
}
close(stream.releaseData)
}
func newTunnelGenerationTestStore() *tunnels {
tunnelMap := map[uint64]*TunnelIO{}
return &tunnels{
tunnels: &tunnelMap,
mutex: &sync.RWMutex{},
streamMutex: &sync.Mutex{},
}
}
func waitForTunnelGenerationFrame(t *testing.T, frames <-chan *sliverpb.TunnelData) {
t.Helper()
select {
case <-frames:
case <-time.After(time.Second):
t.Fatal("timed out waiting for tunnel stream frame")
}
}
func waitForTunnelGenerationSignal(t *testing.T, signal <-chan struct{}, description string) {
t.Helper()
select {
case <-signal:
case <-time.After(time.Second):
t.Fatalf("timed out waiting for %s", description)
}
}
+18 -16
View File
@@ -49,10 +49,10 @@ func pivotListenersHandler(envelope *pb.Envelope, connection *transports.Connect
Listeners: pivots.GetListeners(),
Response: &commonpb.Response{},
})
connection.Send <- &pb.Envelope{
connection.SendEnvelope(&pb.Envelope{
ID: envelope.ID,
Data: data,
}
})
}
func pivotStartListenerHandler(envelope *pb.Envelope, connection *transports.Connection) {
@@ -62,38 +62,40 @@ func pivotStartListenerHandler(envelope *pb.Envelope, connection *transports.Con
if err != nil {
resp.Response.Err = err.Error()
data, _ := proto.Marshal(resp)
connection.Send <- &pb.Envelope{
connection.SendEnvelope(&pb.Envelope{
ID: envelope.ID,
Data: data,
}
})
return
}
if createListener, ok := pivots.SupportedPivotListeners[req.Type]; ok {
listener, err := createListener(req.BindAddress, connection.Send, req.Options...)
listener, err := createListener(req.BindAddress, connection.SendEnvelope, req.Options...)
if err != nil {
resp.Response.Err = err.Error()
data, _ := proto.Marshal(resp)
connection.Send <- &pb.Envelope{
connection.SendEnvelope(&pb.Envelope{
ID: envelope.ID,
Data: data,
}
})
return
}
go listener.Start()
pivots.AddListener(listener)
data, _ := proto.Marshal(listener.ToProtobuf())
connection.Send <- &pb.Envelope{
if !connection.SendEnvelope(&pb.Envelope{
ID: envelope.ID,
Data: data,
}) {
pivots.RemoveListener(listener.ID)
}
} else {
resp.Response.Err = "Unsupported pivot listener type"
data, _ := proto.Marshal(resp)
connection.Send <- &pb.Envelope{
connection.SendEnvelope(&pb.Envelope{
ID: envelope.ID,
Data: data,
}
})
}
}
@@ -104,18 +106,18 @@ func pivotStopListenerHandler(envelope *pb.Envelope, connection *transports.Conn
if err != nil {
resp.Response.Err = err.Error()
data, _ := proto.Marshal(resp)
connection.Send <- &pb.Envelope{
connection.SendEnvelope(&pb.Envelope{
ID: envelope.ID,
Data: data,
}
})
return
}
pivots.StopListener(req.ID)
pivots.RemoveListener(req.ID)
connection.Send <- &pb.Envelope{
connection.SendEnvelope(&pb.Envelope{
ID: envelope.ID,
Data: []byte{},
}
})
}
func pivotPeerEnvelopeHandler(envelope *pb.Envelope, connection *transports.Connection) {
@@ -136,9 +138,9 @@ func pivotPeerEnvelopeHandler(envelope *pb.Envelope, connection *transports.Conn
Type: pb.PeerFailureType_SEND_FAILURE,
Err: errStr,
})
connection.Send <- &pb.Envelope{
connection.SendEnvelope(&pb.Envelope{
Type: pb.MsgPivotPeerFailure,
Data: data,
}
})
}
}
+36 -17
View File
@@ -55,19 +55,20 @@ func rportFwdListenersHandler(envelope *pb.Envelope, connection *transports.Conn
for _, portfwd := range forwards {
portfwdListeners = append(portfwdListeners, &pb.RportFwdListener{
ID: uint32(portfwd.ID),
BindAddress: portfwd.BindAddr,
ForwardAddress: portfwd.RemoteAddr,
ID: uint32(portfwd.ID),
BindAddress: portfwd.BindAddr,
ForwardAddress: portfwd.RemoteAddr,
AuthorizationID: portfwd.AuthorizationID,
})
}
data, _ := proto.Marshal(&pb.RportFwdListeners{
Listeners: portfwdListeners,
Response: &commonpb.Response{},
})
connection.Send <- &pb.Envelope{
connection.SendEnvelope(&pb.Envelope{
ID: envelope.ID,
Data: data,
}
})
}
func rportFwdStartListenerHandler(envelope *pb.Envelope, connection *transports.Connection) {
@@ -77,10 +78,10 @@ func rportFwdStartListenerHandler(envelope *pb.Envelope, connection *transports.
if err != nil {
resp.Response.Err = err.Error()
data, _ := proto.Marshal(resp)
connection.Send <- &pb.Envelope{
connection.SendEnvelope(&pb.Envelope{
ID: envelope.ID,
Data: data,
}
})
return
}
rportfwds := rportfwd.Portfwds.List()
@@ -89,10 +90,10 @@ func rportFwdStartListenerHandler(envelope *pb.Envelope, connection *transports.
if r.BindAddr == req.BindAddress {
resp.Response.Err = "Already listening on " + r.BindAddr + "\n"
data, _ := proto.Marshal(resp)
connection.Send <- &pb.Envelope{
connection.SendEnvelope(&pb.Envelope{
ID: envelope.ID,
Data: data,
}
})
return
}
}
@@ -110,14 +111,28 @@ func rportFwdStartListenerHandler(envelope *pb.Envelope, connection *transports.
Conn: connection,
RemoteAddr: req.ForwardAddress,
BindAddr: req.BindAddress,
AuthorizationID: req.AuthorizationID,
KeepAlivePeriod: keepAlivePeriod,
DialTimeout: 30 * time.Second,
}
tcpProxy.AddRoute(req.BindAddress, channelProxy)
rportfwd := rportfwd.Portfwds.Add(tcpProxy, channelProxy)
if err := tcpProxy.Start(); err != nil {
resp.Response.Err = err.Error()
data, _ := proto.Marshal(resp)
connection.SendEnvelope(&pb.Envelope{ID: envelope.ID, Data: data})
return
}
forward := rportfwd.Portfwds.Add(tcpProxy, channelProxy)
go func() {
select {
case <-connection.Done():
rportfwd.Portfwds.RemoveIf(forward.ID, forward)
case <-forward.Done():
}
}()
go func() {
err := tcpProxy.Run()
err := tcpProxy.Wait()
if err != nil {
// {{if .Config.Debug}}
log.Printf("Proxy error %s", err)
@@ -128,12 +143,15 @@ func rportFwdStartListenerHandler(envelope *pb.Envelope, connection *transports.
resp.ForwardAddress = req.ForwardAddress
resp.BindPort = req.ForwardPort
resp.ForwardPort = req.ForwardPort
resp.ID = uint32(rportfwd.ID)
resp.AuthorizationID = req.AuthorizationID
resp.ID = uint32(forward.ID)
data, _ := proto.Marshal(resp)
connection.Send <- &pb.Envelope{
if !connection.SendEnvelope(&pb.Envelope{
ID: envelope.ID,
Data: data,
}) {
rportfwd.Portfwds.Remove(forward.ID)
}
}
@@ -144,10 +162,10 @@ func rportFwdStopListenerHandler(envelope *pb.Envelope, connection *transports.C
if err != nil {
resp.Response.Err = err.Error()
data, _ := proto.Marshal(resp)
connection.Send <- &pb.Envelope{
connection.SendEnvelope(&pb.Envelope{
ID: envelope.ID,
Data: data,
}
})
return
}
@@ -156,13 +174,14 @@ func rportFwdStopListenerHandler(envelope *pb.Envelope, connection *transports.C
resp.ID = uint32(rportfwd.ID)
resp.BindAddress = rportfwd.ChannelProxy.BindAddr
resp.ForwardAddress = rportfwd.ChannelProxy.RemoteAddr
resp.AuthorizationID = rportfwd.ChannelProxy.AuthorizationID
} else {
resp.Response.Err = "Invalid ID\n"
}
data, _ := proto.Marshal(resp)
connection.Send <- &pb.Envelope{
connection.SendEnvelope(&pb.Envelope{
ID: envelope.ID,
Data: data,
}
})
}
@@ -0,0 +1,217 @@
package handlers
import (
"net"
"sync"
"testing"
"time"
"github.com/bishopfox/sliver/implant/sliver/rportfwd"
"github.com/bishopfox/sliver/implant/sliver/transports"
"github.com/bishopfox/sliver/protobuf/sliverpb"
"google.golang.org/protobuf/proto"
)
type rportfwdDiscardWriteCloser struct{}
func (rportfwdDiscardWriteCloser) Write(data []byte) (int, error) { return len(data), nil }
func (rportfwdDiscardWriteCloser) Close() error { return nil }
func TestRportFwdListenerClosesWithOwningConnectionAndAllowsFreshBind(t *testing.T) {
isolateRportFwdListeners(t)
bindAddress := reserveRportFwdBindAddress(t)
firstConnection := newRportFwdTestConnection()
first := startRportFwdTestListener(t, firstConnection, bindAddress, "first-authorization", 1)
if got := len(rportfwd.Portfwds.List()); got != 1 {
t.Fatalf("listener count = %d, want 1", got)
}
firstConnection.Cleanup()
waitForRportFwdListenerCount(t, 0)
waitForRportFwdBindRelease(t, bindAddress)
secondConnection := newRportFwdTestConnection()
second := startRportFwdTestListener(t, secondConnection, bindAddress, "second-authorization", 2)
if second.ID == first.ID {
t.Fatalf("fresh connection reused listener generation %d", second.ID)
}
if second.AuthorizationID != "second-authorization" {
t.Fatalf("fresh listener authorization = %q", second.AuthorizationID)
}
secondConnection.Cleanup()
waitForRportFwdListenerCount(t, 0)
waitForRportFwdBindRelease(t, bindAddress)
}
func TestRportFwdListenerSurvivesRetiredTunnelWindowRotation(t *testing.T) {
isolateRportFwdListeners(t)
bindAddress := reserveRportFwdBindAddress(t)
connection := newRportFwdTestConnection()
startRportFwdTestListener(t, connection, bindAddress, "rotated-authorization", 3)
for tunnelID := uint64(1); tunnelID <= 5_000; tunnelID++ {
tunnel := transports.NewTunnel(tunnelID, rportfwdDiscardWriteCloser{})
if result := connection.TryAddTunnel(tunnel); result != transports.TunnelAdded {
tunnel.Close()
t.Fatalf("claim tunnel %d result = %v, want %v", tunnelID, result, transports.TunnelAdded)
}
if !connection.CloseTunnelRemote(tunnel) {
t.Fatalf("failed to retire tunnel %d", tunnelID)
}
}
select {
case <-connection.Done():
t.Fatal("retired tunnel window rotation closed the owning connection")
default:
}
if got := len(rportfwd.Portfwds.List()); got != 1 {
t.Fatalf("listener count after replay-window rotation = %d, want 1", got)
}
connection.Cleanup()
waitForRportFwdListenerCount(t, 0)
waitForRportFwdBindRelease(t, bindAddress)
freshConnection := newRportFwdTestConnection()
startRportFwdTestListener(t, freshConnection, bindAddress, "fresh-authorization", 4)
freshConnection.Cleanup()
waitForRportFwdListenerCount(t, 0)
waitForRportFwdBindRelease(t, bindAddress)
}
func TestRportFwdListenerFailedResponseAndStopCleanupRacesDoNotLeak(t *testing.T) {
t.Run("failed start response", func(t *testing.T) {
isolateRportFwdListeners(t)
bindAddress := reserveRportFwdBindAddress(t)
connection := &transports.Connection{}
request := marshalRportFwdStartRequest(t, bindAddress, "failed-response-authorization")
rportFwdStartListenerHandler(&sliverpb.Envelope{ID: 5, Data: request}, connection)
waitForRportFwdListenerCount(t, 0)
waitForRportFwdBindRelease(t, bindAddress)
connection.Cleanup()
})
t.Run("explicit stop and connection close", func(t *testing.T) {
for iteration := 0; iteration < 32; iteration++ {
isolateRPortFwdListenersNow()
bindAddress := reserveRportFwdBindAddress(t)
connection := newRportFwdTestConnection()
listener := startRportFwdTestListener(t, connection, bindAddress, "race-authorization", int64(100+iteration))
stopData, err := proto.Marshal(&sliverpb.RportFwdStopListenerReq{ID: listener.ID})
if err != nil {
t.Fatalf("marshal stop request: %v", err)
}
start := make(chan struct{})
var waitGroup sync.WaitGroup
waitGroup.Add(2)
go func() {
defer waitGroup.Done()
<-start
connection.Cleanup()
}()
go func() {
defer waitGroup.Done()
<-start
rportFwdStopListenerHandler(&sliverpb.Envelope{ID: int64(200 + iteration), Data: stopData}, connection)
}()
close(start)
waitGroup.Wait()
waitForRportFwdListenerCount(t, 0)
waitForRportFwdBindRelease(t, bindAddress)
}
isolateRPortFwdListenersNow()
})
}
func newRportFwdTestConnection() *transports.Connection {
return &transports.Connection{Send: make(chan *sliverpb.Envelope, 4)}
}
func startRportFwdTestListener(t *testing.T, connection *transports.Connection, bindAddress string, authorizationID string, envelopeID int64) *sliverpb.RportFwdListener {
t.Helper()
request := marshalRportFwdStartRequest(t, bindAddress, authorizationID)
rportFwdStartListenerHandler(&sliverpb.Envelope{ID: envelopeID, Data: request}, connection)
select {
case envelope := <-connection.Send:
response := &sliverpb.RportFwdListener{}
if err := proto.Unmarshal(envelope.Data, response); err != nil {
t.Fatalf("decode start response: %v", err)
}
if response.Response == nil {
t.Fatal("start response omitted status")
}
if response.Response.Err != "" {
t.Fatalf("start listener response error: %s", response.Response.Err)
}
return response
case <-time.After(time.Second):
t.Fatal("timed out waiting for start response")
return nil
}
}
func marshalRportFwdStartRequest(t *testing.T, bindAddress string, authorizationID string) []byte {
t.Helper()
request, err := proto.Marshal(&sliverpb.RportFwdStartListenerReq{
BindAddress: bindAddress,
ForwardAddress: "127.0.0.1:4444",
AuthorizationID: authorizationID,
})
if err != nil {
t.Fatalf("marshal start request: %v", err)
}
return request
}
func reserveRportFwdBindAddress(t *testing.T) string {
t.Helper()
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("reserve loopback listener: %v", err)
}
address := listener.Addr().String()
if err := listener.Close(); err != nil {
t.Fatalf("release reserved loopback listener: %v", err)
}
return address
}
func waitForRportFwdListenerCount(t *testing.T, want int) {
t.Helper()
deadline := time.Now().Add(time.Second)
for time.Now().Before(deadline) {
if len(rportfwd.Portfwds.List()) == want {
return
}
time.Sleep(time.Millisecond)
}
t.Fatalf("listener count = %d, want %d", len(rportfwd.Portfwds.List()), want)
}
func waitForRportFwdBindRelease(t *testing.T, bindAddress string) {
t.Helper()
deadline := time.Now().Add(time.Second)
for time.Now().Before(deadline) {
listener, err := net.Listen("tcp", bindAddress)
if err == nil {
_ = listener.Close()
return
}
time.Sleep(time.Millisecond)
}
t.Fatalf("listener bind %s was not released", bindAddress)
}
func isolateRportFwdListeners(t *testing.T) {
t.Helper()
isolateRPortFwdListenersNow()
t.Cleanup(isolateRPortFwdListenersNow)
}
func isolateRPortFwdListenersNow() {
for _, listener := range rportfwd.Portfwds.List() {
rportfwd.Portfwds.Remove(listener.ID)
}
}
@@ -19,6 +19,8 @@ package tunnel_handlers
*/
import (
"errors"
"time"
// {{if .Config.Debug}}
"log"
@@ -30,23 +32,85 @@ import (
"google.golang.org/protobuf/proto"
)
const tunnelTerminalCloseTimeout = 10 * time.Second
func closeTunnelLocal(connection *transports.Connection, tunnel *transports.Tunnel) bool {
if connection == nil || tunnel == nil {
return false
}
shell.StopSession(tunnel.ID)
return connection.CloseTunnelLocal(tunnel)
}
func closeTunnelRemote(connection *transports.Connection, tunnel *transports.Tunnel) bool {
if connection == nil || tunnel == nil {
return false
}
shell.StopSession(tunnel.ID)
return connection.CloseTunnelRemote(tunnel)
}
// TunnelCloseHandler applies a peer terminal frame to its exact tunnel generation.
func TunnelCloseHandler(envelope *sliverpb.Envelope, connection *transports.Connection) {
tunnelClose := &sliverpb.TunnelData{
Closed: true,
}
proto.Unmarshal(envelope.Data, tunnelClose)
// Closing the tunnel pipes alone does not guarantee that a Windows shell
// process exits. Stop the process that owns this tunnel first.
shell.StopSession(tunnelClose.TunnelID)
if err := proto.Unmarshal(envelope.Data, tunnelClose); err != nil || !tunnelClose.Closed {
return
}
handleTunnelClose(tunnelClose, connection, tunnelTerminalCloseTimeout)
}
func handleTunnelClose(tunnelClose *sliverpb.TunnelData, connection *transports.Connection, timeout time.Duration) {
if tunnelClose == nil || connection == nil {
return
}
tunnel := connection.Tunnel(tunnelClose.TunnelID)
if tunnel == nil {
// A close can overtake destination dialing and active-tunnel
// publication. Retire the exact connection-owned setup first, then
// re-read the active map in case publication won that race.
connection.CancelPendingTunnel(tunnelClose.TunnelID)
tunnel = connection.Tunnel(tunnelClose.TunnelID)
}
if tunnel != nil {
// {{if .Config.Debug}}
log.Printf("[tunnel] Closing tunnel with id %d", tunnel.ID)
// {{end}}
connection.RemoveTunnel(tunnel.ID)
tunnel.Close() // Call tunnel.Close instead of individually closing each Reader/Writer here
tunnelDataCache.DeleteTun(tunnel.ID)
// Sequence zero is the legacy protocol's immediate terminal. It must not
// wait behind an inbound destination write: closing the writer is what
// unblocks a stalled shell, port-forward, or WASM handler.
if tunnelClose.Sequence == 0 {
closeTunnelRemote(connection, tunnel)
return
}
// A sequenced terminal normally waits for every lower data frame. Arm the
// fail-closed deadline first because MarkPeerClose serializes with the
// inbound writer and that writer may be non-cooperative until Close.
tunnel.StartPeerCloseDeadline(timeout, func() {
if closeTunnelRemote(connection, tunnel) {
connection.Cleanup()
}
})
ready, err := tunnel.MarkPeerClose(tunnelClose.Sequence)
if err != nil {
if errors.Is(err, transports.ErrTunnelClosed) {
return
}
if closeTunnelRemote(connection, tunnel) {
connection.Cleanup()
}
return
}
if ready {
closeTunnelRemote(connection, tunnel)
return
}
} else {
// Preserve the shell startup tombstone as well; StopSession is a no-op
// for non-shell tunnel IDs.
shell.StopSession(tunnelClose.TunnelID)
// {{if .Config.Debug}}
log.Printf("[tunnel][tunnelCloseHandler] Received close message for unknown tunnel id %d", tunnelClose.TunnelID)
// {{end}}
@@ -1,84 +0,0 @@
package tunnel_handlers
/*
Sliver Implant Framework
Copyright (C) 2022 Bishop Fox
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import (
"sync"
"github.com/bishopfox/sliver/protobuf/sliverpb"
)
var (
// TunnelID -> Sequence Number -> Data
tunnelDataCache = dataCache{mutex: &sync.RWMutex{}, cache: map[uint64]map[uint64]*sliverpb.TunnelData{}}
)
type dataCache struct {
mutex *sync.RWMutex
cache map[uint64]map[uint64]*sliverpb.TunnelData
}
func (c *dataCache) Add(tunnelID uint64, sequence uint64, tunnelData *sliverpb.TunnelData) {
c.mutex.Lock()
defer c.mutex.Unlock()
if _, ok := c.cache[tunnelID]; !ok {
c.cache[tunnelID] = map[uint64]*sliverpb.TunnelData{}
}
c.cache[tunnelID][sequence] = tunnelData
}
func (c *dataCache) Get(tunnelID uint64, sequence uint64) (*sliverpb.TunnelData, bool) {
c.mutex.RLock()
defer c.mutex.RUnlock()
if _, ok := c.cache[tunnelID]; !ok {
return nil, false
}
val, ok := c.cache[tunnelID][sequence]
return val, ok
}
func (c *dataCache) DeleteTun(tunnelID uint64) {
c.mutex.Lock()
defer c.mutex.Unlock()
delete(c.cache, tunnelID)
}
func (c *dataCache) DeleteSeq(tunnelID uint64, sequence uint64) {
c.mutex.Lock()
defer c.mutex.Unlock()
if _, ok := c.cache[tunnelID]; !ok {
return
}
delete(c.cache[tunnelID], sequence)
}
func (c *dataCache) Len(tunnelID uint64) int {
c.mutex.RLock()
defer c.mutex.RUnlock()
return len(c.cache[tunnelID])
}
@@ -19,6 +19,8 @@ package tunnel_handlers
*/
import (
"errors"
"io"
// {{if .Config.Debug}}
"log"
@@ -31,62 +33,82 @@ import (
func TunnelDataHandler(envelope *sliverpb.Envelope, connection *transports.Connection) {
tunnelData := &sliverpb.TunnelData{}
proto.Unmarshal(envelope.Data, tunnelData)
if err := proto.Unmarshal(envelope.Data, tunnelData); err != nil {
return
}
tunnel := connection.Tunnel(tunnelData.TunnelID)
if tunnel != nil {
// Since we have no guarantees that we will receive tunnel data in the correct order, we need
// to ensure we write the data back to the reader in the correct order. The server will ensure
// that TunnelData protobuf objects are numbered in the correct order using the Sequence property.
// Similarly we ensure that any data we write-back to the server is also numbered correctly. To
// reassemble the data, we just dump it into the cache and then advance the writer until we no longer
// have sequential data. So we can receive `n` number of incorrectly ordered Protobuf objects and
// correctly write them back to the reader.
// Resend is a control frame in the legacy generic tunnel protocol, not
// zero-byte stream data. Reverse tunnels never emit it; reliable C2
// transports plus the bounded reorder actor handle delayed frames.
if tunnelData.Resend {
return
}
// {{if .Config.Debug}}
log.Printf("[tunnel] Cache tunnel %d (seq: %d)", tunnel.ID, tunnelData.Sequence)
log.Printf("[tunnel] Process tunnel %d (seq: %d)", tunnel.ID, tunnelData.Sequence)
// {{end}}
tunnelDataCache.Add(tunnel.ID, tunnelData.Sequence, tunnelData)
// NOTE: The read/write semantics can be a little mind boggling, just remember we're reading
// from the server and writing to the tunnel's reader (e.g. stdout), so that's why ReadSequence
// is used here whereas WriteSequence is used for data written back to the server
// Go through cache and write all sequential data to the reader
for recv, ok := tunnelDataCache.Get(tunnel.ID, tunnel.ReadSequence()); ok; recv, ok = tunnelDataCache.Get(tunnel.ID, tunnel.ReadSequence()) {
pending, err := tunnel.ProcessInbound(tunnelData.Sequence, tunnelData.Data, func(payload []byte) error {
// {{if .Config.Debug}}
log.Printf("[tunnel] Write %d bytes to tunnel %d (read seq: %d)", len(recv.Data), recv.TunnelID, recv.Sequence)
// {{end}}
tunnel.Writer.Write(recv.Data)
// Delete the entry we just wrote from the cache
tunnelDataCache.DeleteSeq(tunnel.ID, tunnel.ReadSequence())
tunnel.IncReadSequence() // Increment sequence counter
// {{if .Config.Debug}}
log.Printf("[message just received] %v", tunnelData)
log.Printf("[tunnel] Write %d bytes to tunnel %d", len(payload), tunnel.ID)
// {{end}}
written, writeErr := tunnel.Writer.Write(payload)
if writeErr != nil {
return writeErr
}
if written != len(payload) {
return io.ErrShortWrite
}
return nil
})
if err != nil {
if errors.Is(err, transports.ErrTunnelClosed) {
return
}
protocolFailure := errors.Is(err, transports.ErrTunnelTerminalSequence) ||
errors.Is(err, transports.ErrTunnelFrameTooLarge) ||
errors.Is(err, transports.ErrTunnelSequenceWindow) ||
errors.Is(err, transports.ErrTunnelPendingBytes)
if protocolFailure {
if closeTunnelRemote(connection, tunnel) {
connection.Cleanup()
}
} else {
closeTunnelLocal(connection, tunnel)
}
return
}
if tunnel.PeerCloseReady() {
closeTunnelRemote(connection, tunnel)
return
}
//If cache is building up it probably means a msg was lost and the server is currently hung waiting for it.
//Send a Resend packet to have the msg resent from the cache
if tunnelDataCache.Len(tunnel.ID) > 3 {
data, err := proto.Marshal(&sliverpb.TunnelData{
Sequence: tunnel.WriteSequence(), // The tunnel write sequence
Ack: tunnel.ReadSequence(),
Resend: true,
TunnelID: tunnel.ID,
Data: []byte{},
if pending > 3 && !tunnel.IsReverse() {
err := connection.QueueTunnelControl(tunnel, func(sequence uint64, ack uint64) (*sliverpb.Envelope, error) {
data, marshalErr := proto.Marshal(&sliverpb.TunnelData{
Sequence: sequence,
Ack: ack,
Resend: true,
TunnelID: tunnel.ID,
Data: []byte{},
})
if marshalErr != nil {
return nil, marshalErr
}
return &sliverpb.Envelope{Type: sliverpb.MsgTunnelData, Data: data}, nil
})
if err != nil {
// {{if .Config.Debug}}
log.Printf("[shell] Failed to marshal protobuf %s", err)
// {{end}}
} else {
if !errors.Is(err, transports.ErrTunnelClosed) {
// {{if .Config.Debug}}
log.Printf("[shell] Failed to marshal protobuf %s", err)
// {{end}}
}
closeTunnelLocal(connection, tunnel)
} else if err == nil {
// {{if .Config.Debug}}
log.Printf("[tunnel] Requesting resend of tunnelData seq: %d", tunnel.ReadSequence())
// {{end}}
connection.RequestResend(data)
}
}
@@ -0,0 +1,157 @@
package tunnel_handlers
import (
"bytes"
"errors"
"io"
"sync"
"testing"
"time"
"github.com/bishopfox/sliver/implant/sliver/transports"
"github.com/bishopfox/sliver/protobuf/sliverpb"
"google.golang.org/protobuf/proto"
)
type recordingWriteCloser struct {
bytes.Buffer
closed bool
}
func (writer *recordingWriteCloser) Close() error {
writer.closed = true
return nil
}
func TestReverseTunnelIgnoresLegacyResendControlFrame(t *testing.T) {
connection := &transports.Connection{Send: make(chan *sliverpb.Envelope, 1)}
writer := &recordingWriteCloser{}
tunnel := transports.NewReverseTunnel(0x5101, writer)
if !connection.AddTunnel(tunnel) {
t.Fatal("failed to add reverse tunnel")
}
t.Cleanup(func() { connection.CloseTunnelRemote(tunnel) })
payload, err := proto.Marshal(&sliverpb.TunnelData{
TunnelID: tunnel.ID,
Sequence: 0,
Resend: true,
Data: []byte("must-not-be-relayed"),
})
if err != nil {
t.Fatalf("marshal resend frame: %v", err)
}
TunnelDataHandler(&sliverpb.Envelope{Type: sliverpb.MsgTunnelData, Data: payload}, connection)
if got := writer.String(); got != "" {
t.Fatalf("resend control payload reached reverse destination: %q", got)
}
if got := tunnel.ReadSequence(); got != 0 {
t.Fatalf("resend control advanced reverse read sequence to %d", got)
}
if got := connection.Tunnel(tunnel.ID); got != tunnel {
t.Fatalf("resend control detached reverse tunnel: got %p, want %p", got, tunnel)
}
select {
case envelope := <-connection.Send:
t.Fatalf("resend control emitted unexpected envelope type %d", envelope.Type)
default:
}
}
type closeUnblocksWriteCloser struct {
started chan struct{}
released chan struct{}
startOnce sync.Once
closeOnce sync.Once
}
func newCloseUnblocksWriteCloser() *closeUnblocksWriteCloser {
return &closeUnblocksWriteCloser{
started: make(chan struct{}),
released: make(chan struct{}),
}
}
func (writer *closeUnblocksWriteCloser) Write([]byte) (int, error) {
writer.startOnce.Do(func() { close(writer.started) })
<-writer.released
return 0, io.ErrClosedPipe
}
func (writer *closeUnblocksWriteCloser) Close() error {
writer.closeOnce.Do(func() { close(writer.released) })
return nil
}
func TestTunnelCloseUnblocksInboundWriter(t *testing.T) {
tests := []struct {
name string
sequence uint64
closeTimeout time.Duration
wantC2Cleanup bool
}{
{name: "legacy immediate close", sequence: 0, closeTimeout: time.Second},
{name: "sequenced close deadline", sequence: 1, closeTimeout: 20 * time.Millisecond, wantC2Cleanup: true},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
connection := &transports.Connection{Send: make(chan *sliverpb.Envelope, 1)}
writer := newCloseUnblocksWriteCloser()
tunnel := transports.NewTunnel(0x5201, writer)
if !connection.AddTunnel(tunnel) {
t.Fatal("failed to add tunnel")
}
inboundDone := make(chan error, 1)
go func() {
_, err := tunnel.ProcessInbound(0, []byte("blocked"), func(data []byte) error {
_, writeErr := writer.Write(data)
return writeErr
})
inboundDone <- err
}()
select {
case <-writer.started:
case <-time.After(time.Second):
t.Fatal("inbound writer did not block")
}
closeDone := make(chan struct{})
go func() {
handleTunnelClose(&sliverpb.TunnelData{
Closed: true,
TunnelID: tunnel.ID,
Sequence: test.sequence,
}, connection, test.closeTimeout)
close(closeDone)
}()
select {
case <-closeDone:
case <-time.After(time.Second):
t.Fatal("tunnel close remained blocked behind inbound writer")
}
select {
case err := <-inboundDone:
if !errors.Is(err, io.ErrClosedPipe) {
t.Fatalf("inbound error = %v, want %v", err, io.ErrClosedPipe)
}
case <-time.After(time.Second):
t.Fatal("closing tunnel did not unblock inbound writer")
}
if active := connection.Tunnel(tunnel.ID); active != nil {
t.Fatalf("closed tunnel remained published: %p", active)
}
select {
case <-connection.Done():
if !test.wantC2Cleanup {
t.Fatal("legacy close unexpectedly tore down the C2 connection")
}
default:
if test.wantC2Cleanup {
t.Fatal("incomplete sequenced close did not fail the C2 connection closed")
}
}
})
}
}
@@ -20,10 +20,10 @@ package tunnel_handlers
import (
"context"
"fmt"
"errors"
"io"
"net"
"sync"
"strconv"
"time"
// {{if .Config.Debug}}
@@ -37,7 +37,16 @@ import (
"google.golang.org/protobuf/proto"
)
const portfwdDialTimeout = 30 * time.Second
type portfwdDialFunc func(context.Context, string, string) (net.Conn, error)
// PortfwdReqHandler opens and registers one server-requested forward tunnel.
func PortfwdReqHandler(envelope *sliverpb.Envelope, connection *transports.Connection) {
handlePortfwdReq(envelope, connection, new(net.Dialer).DialContext, portfwdDialTimeout)
}
func handlePortfwdReq(envelope *sliverpb.Envelope, connection *transports.Connection, dial portfwdDialFunc, dialTimeout time.Duration) {
portfwdReq := &sliverpb.PortfwdReq{}
err := proto.Unmarshal(envelope.Data, portfwdReq)
if err != nil {
@@ -53,43 +62,44 @@ func PortfwdReqHandler(envelope *sliverpb.Envelope, connection *transports.Conne
return
}
var defaultDialer = new(net.Dialer)
pending, addResult := connection.BeginTunnel(portfwdReq.TunnelID, dialTimeout)
if addResult != transports.TunnelAdded {
reportPortfwdError(envelope, connection, errors.New("port forward tunnel setup is no longer active"))
return
}
defer pending.Cancel()
// TODO: Configurable context
remoteAddress := fmt.Sprintf("%s:%d", portfwdReq.Host, portfwdReq.Port)
remoteAddress := portfwdRemoteAddress(portfwdReq.Host, portfwdReq.Port)
// {{if .Config.Debug}}
log.Printf("[portfwd] Dialing -> %s", remoteAddress)
// {{end}}
ctx, cancelContext := context.WithCancel(context.Background())
dst, err := defaultDialer.DialContext(ctx, "tcp", remoteAddress)
dst, err := dial(pending.Context(), "tcp", remoteAddress)
if err != nil {
// {{if .Config.Debug}}
log.Printf("[portfwd] Failed to dial remote address %s", err)
// {{end}}
cancelContext()
portfwdResp, _ := proto.Marshal(&sliverpb.Portfwd{
Response: &commonpb.Response{
Err: err.Error(),
},
})
reportError(envelope, connection, portfwdResp)
reportPortfwdError(envelope, connection, err)
return
}
if conn, ok := dst.(*net.TCPConn); ok {
// {{if .Config.Debug}}
log.Printf("[portfwd] Configuring keep alive")
// {{end}}
var keepAliveErr error
if portfwdReq.KeepAlive < 0 {
conn.SetKeepAlive(false)
} else {
conn.SetKeepAlive(true)
keepAliveErr = conn.SetKeepAlive(false)
} else if keepAliveErr = conn.SetKeepAlive(true); keepAliveErr == nil {
keepAlivePeriod := 30 * time.Second
if portfwdReq.KeepAlive > 0 {
conn.SetKeepAlivePeriod(time.Duration(portfwdReq.KeepAlive) * time.Second)
} else {
conn.SetKeepAlivePeriod(30 * time.Second)
keepAlivePeriod = time.Duration(portfwdReq.KeepAlive) * time.Second
}
keepAliveErr = conn.SetKeepAlivePeriod(keepAlivePeriod)
}
if keepAliveErr != nil {
_ = dst.Close()
reportPortfwdError(envelope, connection, keepAliveErr)
return
}
}
@@ -102,7 +112,16 @@ func PortfwdReqHandler(envelope *sliverpb.Envelope, connection *transports.Conne
dst,
dst,
)
connection.AddTunnel(tunnel)
if connection.PublishTunnel(pending, tunnel) != transports.TunnelAdded {
tunnel.Close()
reportPortfwdError(envelope, connection, errors.New("port forward tunnel setup was canceled"))
return
}
if connection.Tunnel(tunnel.ID) != tunnel {
tunnel.Close()
reportPortfwdError(envelope, connection, errors.New("port forward tunnel closed during setup"))
return
}
// Send portfwd response
portfwdResp, _ := proto.Marshal(&sliverpb.Portfwd{
@@ -111,36 +130,12 @@ func PortfwdReqHandler(envelope *sliverpb.Envelope, connection *transports.Conne
Protocol: sliverpb.PortFwdProtoTCP,
TunnelID: portfwdReq.TunnelID,
})
connection.Send <- &sliverpb.Envelope{
if !connection.SendEnvelope(&sliverpb.Envelope{
ID: envelope.ID,
Data: portfwdResp,
}
once := sync.Once{}
cleanup := func(reason error) {
once.Do(func() {
// {{if .Config.Debug}}
log.Printf("[portfwd] Closing tunnel %d (%s)", tunnel.ID, reason)
// {{end}}
cleanupTunnel := connection.Tunnel(tunnel.ID)
if cleanupTunnel == nil {
return
}
tunnelClose, _ := proto.Marshal(&sliverpb.TunnelData{
Closed: true,
TunnelID: cleanupTunnel.ID,
})
connection.Send <- &sliverpb.Envelope{
Type: sliverpb.MsgTunnelClose,
Data: tunnelClose,
}
connection.RemoveTunnel(cleanupTunnel.ID)
if dst != nil {
dst.Close()
}
cancelContext()
})
}) {
connection.CloseTunnelRemote(tunnel)
return
}
go func() {
@@ -151,10 +146,25 @@ func PortfwdReqHandler(envelope *sliverpb.Envelope, connection *transports.Conne
// portfwd only uses one reader, hence the tunnel.Readers[0]
n, err := io.Copy(tWriter, tunnel.Readers[0])
_ = n // avoid not used compiler error if debug mode is disabled
_ = err
// {{if .Config.Debug}}
log.Printf("[tunnel] Tunnel done, wrote %v bytes", n)
// {{end}}
cleanup(err)
// {{if .Config.Debug}}
log.Printf("[portfwd] Closing tunnel %d (%s)", tunnel.ID, err)
// {{end}}
connection.CloseTunnelLocal(tunnel)
}()
}
func reportPortfwdError(envelope *sliverpb.Envelope, connection *transports.Connection, err error) {
portfwdResp, _ := proto.Marshal(&sliverpb.Portfwd{
Response: &commonpb.Response{Err: err.Error()},
})
reportError(envelope, connection, portfwdResp)
}
func portfwdRemoteAddress(host string, port uint32) string {
return net.JoinHostPort(host, strconv.FormatUint(uint64(port), 10))
}
@@ -0,0 +1,163 @@
package tunnel_handlers
import (
"context"
"errors"
"io"
"net"
"testing"
"time"
"github.com/bishopfox/sliver/implant/sliver/transports"
"github.com/bishopfox/sliver/protobuf/commonpb"
"github.com/bishopfox/sliver/protobuf/sliverpb"
"google.golang.org/protobuf/proto"
)
func TestPortfwdRemoteAddress(t *testing.T) {
tests := []struct {
name string
host string
port uint32
want string
}{
{name: "hostname", host: "target.example", port: 443, want: "target.example:443"},
{name: "IPv4", host: "192.0.2.10", port: 8080, want: "192.0.2.10:8080"},
{name: "IPv6", host: "2001:db8::10", port: 8443, want: "[2001:db8::10]:8443"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if got := portfwdRemoteAddress(test.host, test.port); got != test.want {
t.Fatalf("portfwdRemoteAddress(%q, %d) = %q, want %q", test.host, test.port, got, test.want)
}
})
}
}
func TestPortfwdCloseCancelsBlockedDial(t *testing.T) {
const tunnelID = uint64(0x7011)
connection := &transports.Connection{Send: make(chan *sliverpb.Envelope, 2)}
t.Cleanup(connection.Cleanup)
started := make(chan struct{})
dialer := portfwdDialFunc(func(ctx context.Context, _, _ string) (net.Conn, error) {
close(started)
<-ctx.Done()
return nil, ctx.Err()
})
done := runPortfwdHandler(t, connection, tunnelID, dialer, time.Hour)
waitPortfwdTestSignal(t, started, "destination dial")
handleTunnelClose(&sliverpb.TunnelData{TunnelID: tunnelID, Closed: true}, connection, time.Second)
waitPortfwdTestSignal(t, done, "canceled port-forward handler")
if active := connection.Tunnel(tunnelID); active != nil {
t.Fatalf("canceled destination dial published tunnel %p", active)
}
if replacement, result := connection.BeginTunnel(tunnelID, time.Hour); result != transports.TunnelAddDuplicate || replacement != nil {
t.Fatalf("closed setup ID replacement = %p, %v; want nil, %v", replacement, result, transports.TunnelAddDuplicate)
}
}
func TestPortfwdCloseOvertakesLateDialAndClosesSocket(t *testing.T) {
const tunnelID = uint64(0x7012)
connection := &transports.Connection{Send: make(chan *sliverpb.Envelope, 2)}
t.Cleanup(connection.Cleanup)
implantSide, targetSide := net.Pipe()
t.Cleanup(func() { _ = targetSide.Close() })
started := make(chan struct{})
canceled := make(chan struct{})
release := make(chan struct{})
dialer := portfwdDialFunc(func(ctx context.Context, _, _ string) (net.Conn, error) {
close(started)
go func() {
<-ctx.Done()
close(canceled)
}()
<-release // Simulate a dialer that returns a socket after cancellation.
return implantSide, nil
})
done := runPortfwdHandler(t, connection, tunnelID, dialer, time.Hour)
waitPortfwdTestSignal(t, started, "late destination dial")
handleTunnelClose(&sliverpb.TunnelData{TunnelID: tunnelID, Closed: true}, connection, time.Second)
waitPortfwdTestSignal(t, canceled, "pending dial cancellation")
if err := targetSide.SetReadDeadline(time.Now().Add(time.Second)); err != nil {
t.Fatalf("set target-side read deadline before late socket closes: %v", err)
}
close(release)
waitPortfwdTestSignal(t, done, "late port-forward handler")
if active := connection.Tunnel(tunnelID); active != nil {
t.Fatalf("late destination dial published tunnel %p", active)
}
buffer := make([]byte, 1)
if _, err := targetSide.Read(buffer); !errors.Is(err, io.EOF) {
t.Fatalf("late destination socket read error = %v, want %v", err, io.EOF)
}
}
func TestPortfwdOwnerDisconnectCancelsBlockedDial(t *testing.T) {
const tunnelID = uint64(0x7013)
connection := &transports.Connection{Send: make(chan *sliverpb.Envelope, 2)}
started := make(chan struct{})
dialer := portfwdDialFunc(func(ctx context.Context, _, _ string) (net.Conn, error) {
close(started)
<-ctx.Done()
return nil, ctx.Err()
})
done := runPortfwdHandler(t, connection, tunnelID, dialer, time.Hour)
waitPortfwdTestSignal(t, started, "owner-scoped destination dial")
connection.Cleanup()
waitPortfwdTestSignal(t, done, "owner-disconnected port-forward handler")
if active := connection.Tunnel(tunnelID); active != nil {
t.Fatalf("owner-disconnected destination dial published tunnel %p", active)
}
}
func TestPortfwdDestinationDialHasFiniteDeadline(t *testing.T) {
const tunnelID = uint64(0x7014)
connection := &transports.Connection{Send: make(chan *sliverpb.Envelope, 2)}
t.Cleanup(connection.Cleanup)
started := make(chan struct{})
dialer := portfwdDialFunc(func(ctx context.Context, _, _ string) (net.Conn, error) {
close(started)
if _, ok := ctx.Deadline(); !ok {
return nil, errors.New("destination dial context has no deadline")
}
<-ctx.Done()
return nil, ctx.Err()
})
done := runPortfwdHandler(t, connection, tunnelID, dialer, 20*time.Millisecond)
waitPortfwdTestSignal(t, started, "deadline-bound destination dial")
waitPortfwdTestSignal(t, done, "deadline-bound port-forward handler")
if active := connection.Tunnel(tunnelID); active != nil {
t.Fatalf("timed-out destination dial published tunnel %p", active)
}
}
func runPortfwdHandler(t *testing.T, connection *transports.Connection, tunnelID uint64, dialer portfwdDialFunc, timeout time.Duration) <-chan struct{} {
t.Helper()
data, err := proto.Marshal(&sliverpb.PortfwdReq{
Request: &commonpb.Request{SessionID: "portfwd-handler-test"},
Host: "127.0.0.1",
Port: 3389,
Protocol: sliverpb.PortFwdProtoTCP,
TunnelID: tunnelID,
})
if err != nil {
t.Fatalf("marshal port-forward request: %v", err)
}
done := make(chan struct{})
go func() {
defer close(done)
handlePortfwdReq(&sliverpb.Envelope{ID: int64(tunnelID), Data: data}, connection, dialer, timeout)
}()
return done
}
func waitPortfwdTestSignal(t *testing.T, signal <-chan struct{}, description string) {
t.Helper()
select {
case <-signal:
case <-time.After(time.Second):
t.Fatalf("timed out waiting for %s", description)
}
}
@@ -88,16 +88,23 @@ func ShellReqHandler(envelope *sliverpb.Envelope, connection *transports.Connect
systemShell.Stdout,
systemShell.Stderr,
)
connection.AddTunnel(tunnel)
session := shell.NewSession(systemShell)
if !connection.AddTunnel(tunnel) {
session.Stop()
tunnel.Close()
_ = systemShell.Wait()
shellResp, _ := proto.Marshal(&sliverpb.Shell{
Response: &commonpb.Response{Err: "shell tunnel ID is already active"},
})
reportError(envelope, connection, shellResp)
return
}
if !shell.RegisterSession(tunnel.ID, session) {
// Tunnel handlers are dispatched concurrently. A close can overtake this
// request while the process is starting; in that case registration stops
// the process and declines to publish a shell that can no longer be closed.
connection.RemoveTunnel(tunnel.ID)
tunnel.Close()
tunnelDataCache.DeleteTun(tunnel.ID)
connection.CloseTunnelRemote(tunnel)
_ = systemShell.Wait()
shellResp, _ := proto.Marshal(&sliverpb.Shell{
Response: &commonpb.Response{Err: "shell tunnel closed during startup"},
@@ -113,9 +120,15 @@ func ShellReqHandler(envelope *sliverpb.Envelope, connection *transports.Connect
Path: shellReq.Path,
TunnelID: shellReq.TunnelID,
})
connection.Send <- &sliverpb.Envelope{
if !connection.SendEnvelope(&sliverpb.Envelope{
ID: envelope.ID,
Data: shellResp,
}) {
shell.UnregisterSession(tunnel.ID)
session.Stop()
connection.CloseTunnelRemote(tunnel)
_ = systemShell.Wait()
return
}
var finalizeOnce sync.Once
@@ -126,18 +139,7 @@ func ShellReqHandler(envelope *sliverpb.Envelope, connection *transports.Connect
// {{end}}
shell.UnregisterSession(tunnel.ID)
connection.RemoveTunnel(tunnel.ID)
tunnel.Close()
tunnelDataCache.DeleteTun(tunnel.ID)
tunnelClose, _ := proto.Marshal(&sliverpb.TunnelData{
Closed: true,
TunnelID: tunnel.ID,
})
connection.Send <- &sliverpb.Envelope{
Type: sliverpb.MsgTunnelClose,
Data: tunnelClose,
}
connection.CloseTunnelLocal(tunnel)
})
}
@@ -11,8 +11,8 @@ import (
func ShellResizeReqHandler(envelope *sliverpb.Envelope, connection *transports.Connection) {
resp, _ := proto.Marshal(&commonpb.Empty{})
connection.Send <- &sliverpb.Envelope{
connection.SendEnvelope(&sliverpb.Envelope{
ID: envelope.ID,
Data: resp,
}
})
}
@@ -56,8 +56,8 @@ func ShellResizeReqHandler(envelope *sliverpb.Envelope, connection *transports.C
}
resp, _ := proto.Marshal(&commonpb.Empty{})
connection.Send <- &sliverpb.Envelope{
connection.SendEnvelope(&sliverpb.Envelope{
ID: envelope.ID,
Data: resp,
}
})
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -38,23 +38,26 @@ type tunnelWriter struct {
func (tw tunnelWriter) Write(data []byte) (int, error) {
n := len(data)
sequence := tw.tun.NextWriteSequence()
ack := tw.tun.ReadSequence()
data, err := proto.Marshal(&sliverpb.TunnelData{
Sequence: sequence,
Ack: ack,
TunnelID: tw.tun.ID,
Data: data,
err := tw.conn.QueueTunnelData(tw.tun, func(sequence uint64, ack uint64) (*sliverpb.Envelope, error) {
marshaled, marshalErr := proto.Marshal(&sliverpb.TunnelData{
Sequence: sequence,
Ack: ack,
TunnelID: tw.tun.ID,
Data: data,
})
// {{if .Config.Debug}}
log.Printf("[tunnelWriter] Write %d bytes (write seq: %d) ack: %d", n, sequence, ack)
// {{end}}
if marshalErr != nil {
return nil, marshalErr
}
return &sliverpb.Envelope{
Type: sliverpb.MsgTunnelData,
Data: marshaled,
}, nil
})
// {{if .Config.Debug}}
log.Printf("[tunnelWriter] Write %d bytes (write seq: %d) ack: %d", n, sequence, ack)
// {{end}}
if err != nil {
return 0, err
}
tw.conn.Send <- &sliverpb.Envelope{
Type: sliverpb.MsgTunnelData,
Data: data,
}
return n, nil
}
@@ -24,8 +24,8 @@ import (
)
func reportError(envelope *sliverpb.Envelope, connection *transports.Connection, data []byte) {
connection.Send <- &sliverpb.Envelope{
connection.SendEnvelope(&sliverpb.Envelope{
Data: data,
ID: envelope.ID,
}
})
}
@@ -4,6 +4,7 @@ import (
"bytes"
"fmt"
"io"
"sync"
// {{if .Config.Debug}}
"log"
@@ -64,10 +65,14 @@ func ExecWasmExtensionHandler(envelope *pb.Envelope, connection *transports.Conn
} else {
data, _ = runNonInteractive(req, wasmExtRuntime)
}
connection.Send <- &pb.Envelope{
if !connection.SendEnvelope(&pb.Envelope{
ID: envelope.ID,
Type: pb.MsgExecWasmExtension,
Data: data,
}) && req.Interactive {
if tunnel := connection.Tunnel(req.TunnelID); tunnel != nil {
connection.CloseTunnelRemote(tunnel)
}
}
}
@@ -143,21 +148,22 @@ func runInteractive(req *pb.ExecWasmExtensionReq, conn *transports.Connection, w
wasm.Stdout.Reader,
// wasm.Stderr.Reader,
)
conn.AddTunnel(tunnel)
if !conn.AddTunnel(tunnel) {
_ = wasm.Close()
return proto.Marshal(&pb.ExecWasmExtension{
Response: &commonpb.Response{Err: "Wasm tunnel ID is already active"},
})
}
// Cleanup function with arguments
var cleanupOnce sync.Once
cleanup := func(reason string, err error) {
// {{if .Config.Debug}}
log.Printf("[wasm] Closing tunnel request %d (%s). Err: %v", tunnel.ID, reason, err)
// {{end}}
tunnelClose, _ := proto.Marshal(&pb.TunnelData{
Closed: true,
TunnelID: tunnel.ID,
cleanupOnce.Do(func() {
// {{if .Config.Debug}}
log.Printf("[wasm] Closing tunnel request %d (%s). Err: %v", tunnel.ID, reason, err)
// {{end}}
conn.CloseTunnelLocal(tunnel)
})
conn.Send <- &pb.Envelope{
Type: pb.MsgTunnelClose,
Data: tunnelClose,
}
}
go func() (uint32, error) {
@@ -178,7 +184,6 @@ func runInteractive(req *pb.ExecWasmExtensionReq, conn *transports.Connection, w
wasm.Stdout.Writer.Write([]byte(fmt.Sprintf("\r\n*** exit code %d ***\r\n", exitCode)))
wasm.Stdout.Writer.Write([]byte("Wait 10 seconds and press <enter> to continue ...\r\n"))
wasm.Close()
tunnel.Close()
return exitCode, err
}()
@@ -198,14 +203,7 @@ func runInteractive(req *pb.ExecWasmExtensionReq, conn *transports.Connection, w
log.Printf("[wasm] tWriter: %v stream: %v", tWriter, stream)
// {{end}}
_, err := io.Copy(tWriter, stream)
if err != nil {
cleanup("io error", err)
return
}
if err == io.EOF {
cleanup("EOF", err)
return
}
cleanup("stream closed", err)
}(rc)
}
+1 -1
View File
@@ -39,7 +39,7 @@ var (
)
// CreateNamedPipePivotListener - Starts a named pipe listener
func CreateNamedPipePivotListener(address string, upstream chan<- *pb.Envelope, opts ...bool) (*PivotListener, error) {
func CreateNamedPipePivotListener(address string, upstream EnvelopeSender, opts ...bool) (*PivotListener, error) {
fullName := "\\\\.\\pipe\\" + strings.TrimPrefix(address, "\\\\.\\pipe\\")
sd := ""
if len(opts) > 0 {
+15 -6
View File
@@ -79,8 +79,13 @@ func generatePeerID() int64 {
return peerID
}
// EnvelopeSender queues an envelope for the active C2 connection. The bool is
// false once that connection can no longer accept traffic, allowing pivot
// producers to terminate instead of blocking forever on a retired send queue.
type EnvelopeSender func(*pb.Envelope) bool
// CreateListener - Generic interface to a start listener function
type CreateListener func(string, chan<- *pb.Envelope, ...bool) (*PivotListener, error)
type CreateListener func(string, EnvelopeSender, ...bool) (*PivotListener, error)
// GetListeners - Get a list of active listeners
func GetListeners() []*pb.PivotListener {
@@ -110,7 +115,7 @@ func RemoveListener(id uint32) {
}
// RestartAllListeners - Start all pivot listeners
func RestartAllListeners(send chan<- *pb.Envelope) {
func RestartAllListeners(send EnvelopeSender) {
stoppedPivotListeners.Range(func(key, value interface{}) bool {
stoppedListener := value.(*PivotListener)
if createListener, ok := SupportedPivotListeners[stoppedListener.Type]; ok {
@@ -215,7 +220,7 @@ type PivotListener struct {
Listener net.Listener
PivotConnections *sync.Map // PeerID (int64) -> NetConnPivot
BindAddress string
Upstream chan<- *pb.Envelope
Upstream EnvelopeSender
Options []bool
}
@@ -294,7 +299,7 @@ type NetConnPivot struct {
readDeadline time.Duration
writeDeadline time.Duration
upstream chan<- *pb.Envelope
upstream EnvelopeSender
Downstream chan *pb.Envelope
}
@@ -362,7 +367,9 @@ func (p *NetConnPivot) Start(pivots *sync.Map) {
Name: consts.SliverName,
})
envelope.Data, _ = proto.Marshal(peerEnvelope)
p.upstream <- envelope
if p.upstream == nil || !p.upstream(envelope) {
return
}
} else {
// {{if .Config.Debug}}
log.Printf("[pivot] received unknown message type (%d), dropping ...", envelope.Type)
@@ -376,12 +383,14 @@ func (p *NetConnPivot) Start(pivots *sync.Map) {
err := p.writeEnvelope(envelope)
if err != nil {
if p.downstreamPeerID != 0 {
p.upstream <- &pb.Envelope{
if p.upstream == nil || !p.upstream(&pb.Envelope{
Type: pb.MsgPivotPeerFailure,
Data: mustMarshal(&pb.PivotPeerFailure{
Type: pb.PeerFailureType_DISCONNECT,
PeerID: p.downstreamPeerID,
}),
}) {
return
}
}
return
+1 -1
View File
@@ -36,7 +36,7 @@ var (
)
// CreateTCPPivotListener - Start a TCP listener
func CreateTCPPivotListener(address string, upstream chan<- *pb.Envelope, opts ...bool) (*PivotListener, error) {
func CreateTCPPivotListener(address string, upstream EnvelopeSender, _ ...bool) (*PivotListener, error) {
// {{if .Config.Debug}}
log.Printf("Starting TCP pivot listener on %s", address)
// {{end}}
+79 -27
View File
@@ -49,10 +49,11 @@ var (
// PortfwdMeta - Metadata about a portfwd listener
type PortfwdMeta struct {
ID int
SessionID string
BindAddr string
RemoteAddr string
ID int
SessionID string
BindAddr string
RemoteAddr string
AuthorizationID string
}
// Portfwd - Tracks portfwd<->tcpproxy
@@ -60,14 +61,25 @@ type Portfwd struct {
ID int
TCPProxy *tcpproxy.Proxy
ChannelProxy *ChannelProxy
done chan struct{}
}
// Done is closed after this exact listener generation has been removed from
// the registry and its TCP proxy has been closed.
func (p *Portfwd) Done() <-chan struct{} {
if p == nil {
return nil
}
return p.done
}
// GetMetadata - Get metadata about the portfwd
func (p *Portfwd) GetMetadata() *PortfwdMeta {
return &PortfwdMeta{
ID: p.ID,
BindAddr: p.ChannelProxy.BindAddr,
RemoteAddr: p.ChannelProxy.RemoteAddr,
ID: p.ID,
BindAddr: p.ChannelProxy.BindAddr,
RemoteAddr: p.ChannelProxy.RemoteAddr,
AuthorizationID: p.ChannelProxy.AuthorizationID,
}
}
@@ -84,6 +96,7 @@ func (f *portfwds) Add(tcpProxy *tcpproxy.Proxy, channelProxy *ChannelProxy) *Po
ID: nextPortfwdID(),
TCPProxy: tcpProxy,
ChannelProxy: channelProxy,
done: make(chan struct{}),
}
f.forwards[portfwd.ID] = portfwd
return portfwd
@@ -92,13 +105,35 @@ func (f *portfwds) Add(tcpProxy *tcpproxy.Proxy, channelProxy *ChannelProxy) *Po
// Remove - Remove a TCP proxy instance
func (f *portfwds) Remove(portfwdID int) *Portfwd {
f.mutex.Lock()
defer f.mutex.Unlock()
if portfwd, ok := f.forwards[portfwdID]; ok {
portfwd.TCPProxy.Close()
portfwd, ok := f.forwards[portfwdID]
if ok {
delete(f.forwards, portfwdID)
return portfwd
}
return nil
f.mutex.Unlock()
if !ok {
return nil
}
portfwd.close()
return portfwd
}
// RemoveIf removes portfwdID only when it still refers to expected. C2
// teardown uses this compare-delete operation so cleanup for an old connection
// can never detach a newer listener generation.
func (f *portfwds) RemoveIf(portfwdID int, expected *Portfwd) *Portfwd {
if expected == nil {
return nil
}
f.mutex.Lock()
portfwd, ok := f.forwards[portfwdID]
if !ok || portfwd != expected {
f.mutex.Unlock()
return nil
}
delete(f.forwards, portfwdID)
f.mutex.Unlock()
portfwd.close()
return portfwd
}
// List - List all TCP proxy instances
@@ -112,16 +147,32 @@ func (f *portfwds) List() []*PortfwdMeta {
return portForwards
}
func (p *Portfwd) close() {
if p.TCPProxy != nil {
_ = p.TCPProxy.Close()
}
if p.done != nil {
close(p.done)
}
}
// ChannelProxy binds the Sliver Tunnel to a net.Conn object
// one ChannelProxy per port bind.
//
// Implements the Target interface from tcpproxy pkg
type tunnelConnection interface {
AddTunnel(*transports.Tunnel) bool
CloseTunnelLocal(*transports.Tunnel) bool
QueueTunnelData(*transports.Tunnel, transports.TunnelEnvelopeBuilder) error
}
type ChannelProxy struct {
Conn *transports.Connection
Conn tunnelConnection
//Session *clientpb.Session
BindAddr string
RemoteAddr string
AuthorizationID string
KeepAlivePeriod time.Duration
DialTimeout time.Duration
}
@@ -132,7 +183,7 @@ func (p *ChannelProxy) HandleConn(src net.Conn) {
log.Printf("[tcpproxy] Handling new connection")
// {{end}}
ctx := context.Background()
var cancelContext context.CancelFunc
cancelContext := func() {}
if p.DialTimeout >= 0 {
ctx, cancelContext = context.WithTimeout(ctx, p.dialTimeout())
}
@@ -161,32 +212,33 @@ func (p *ChannelProxy) HandleConn(src net.Conn) {
log.Printf("[tcpproxy] Created new tunnel with id %d", tId)
// {{end}}
tunnel := transports.NewTunnel(
tunnel := transports.NewReverseTunnel(
tId,
src,
src,
)
p.Conn.AddTunnel(tunnel)
if !p.Conn.AddTunnel(tunnel) {
tunnel.Close()
cancelContext()
return
}
cleanup := func(reason error) {
// {{if .Config.Debug}}
log.Printf("[portfwd] Closing tunnel %d (%s)", tunnel.ID, reason)
// {{end}}
tunnel := p.Conn.Tunnel(tunnel.ID)
if tunnel != nil {
p.Conn.RemoveTunnel(tunnel.ID)
}
src.Close()
p.Conn.CloseTunnelLocal(tunnel)
cancelContext()
}
go func() {
tWriter := tunnelWriter{
tun: tunnel,
conn: p.Conn,
host: p.Host(),
port: p.Port(),
protocol: sliverpb.PortFwdProtoTCP,
tunnelID: tId,
tun: tunnel,
conn: p.Conn,
host: p.Host(),
port: p.Port(),
protocol: sliverpb.PortFwdProtoTCP,
tunnelID: tId,
authorizationID: p.AuthorizationID,
}
// portfwd only uses one reader, hence the tunnel.Readers[0]
n, err := io.Copy(tWriter, tunnel.Readers[0])
+124
View File
@@ -0,0 +1,124 @@
package rportfwd
import (
"bytes"
"net"
"sync"
"testing"
"time"
"github.com/bishopfox/sliver/implant/sliver/transports"
"github.com/bishopfox/sliver/protobuf/sliverpb"
"google.golang.org/protobuf/proto"
)
func TestPortfwdsRemoveIfOnlyRemovesExactGeneration(t *testing.T) {
registry := &portfwds{
forwards: map[int]*Portfwd{},
mutex: &sync.RWMutex{},
}
stale := &Portfwd{ID: 7, done: make(chan struct{})}
current := &Portfwd{ID: 7, done: make(chan struct{})}
registry.forwards[current.ID] = current
if removed := registry.RemoveIf(current.ID, stale); removed != nil {
t.Fatalf("RemoveIf() removed stale generation %+v", removed)
}
if got := registry.forwards[current.ID]; got != current {
t.Fatalf("stale cleanup replaced current generation: got=%p want=%p", got, current)
}
select {
case <-current.Done():
t.Fatal("stale cleanup closed the current generation")
default:
}
if removed := registry.RemoveIf(current.ID, current); removed != current {
t.Fatalf("RemoveIf() = %p, want %p", removed, current)
}
if _, ok := registry.forwards[current.ID]; ok {
t.Fatal("exact generation remained in registry")
}
select {
case <-current.Done():
case <-time.After(time.Second):
t.Fatal("exact generation removal did not publish completion")
}
select {
case <-stale.Done():
t.Fatal("exact generation removal closed stale generation")
default:
}
}
type testTunnelConnection struct {
*transports.Connection
send chan *sliverpb.Envelope
}
func newTestTunnelConnection() *testTunnelConnection {
send := make(chan *sliverpb.Envelope, 4)
return &testTunnelConnection{
Connection: &transports.Connection{Send: send},
send: send,
}
}
func TestChannelProxySourceEOFSendsSequencedCloseAndDetaches(t *testing.T) {
connection := newTestTunnelConnection()
proxy := &ChannelProxy{
Conn: connection,
RemoteAddr: "127.0.0.1:443",
AuthorizationID: "server-issued",
DialTimeout: time.Second,
}
implantSide, clientSide := net.Pipe()
t.Cleanup(func() {
_ = implantSide.Close()
_ = clientSide.Close()
})
proxy.HandleConn(implantSide)
payload := []byte("final-payload")
if _, err := clientSide.Write(payload); err != nil {
t.Fatalf("write client payload: %v", err)
}
dataTransportEnvelope, dataEnvelope := receiveTunnelEnvelope(t, connection.send)
if dataTransportEnvelope.Type != sliverpb.MsgTunnelData || dataEnvelope.Sequence != 0 || !dataEnvelope.CreateReverse {
t.Fatalf("unexpected first tunnel envelope: %+v", dataEnvelope)
}
if !bytes.Equal(dataEnvelope.Data, payload) {
t.Fatalf("data payload = %q, want %q", dataEnvelope.Data, payload)
}
if err := clientSide.Close(); err != nil {
t.Fatalf("close client side: %v", err)
}
closeTransportEnvelope, closeEnvelope := receiveTunnelEnvelope(t, connection.send)
if closeTransportEnvelope.Type != sliverpb.MsgTunnelClose || !closeEnvelope.Closed || closeEnvelope.Sequence != 1 || closeEnvelope.TunnelID != dataEnvelope.TunnelID {
t.Fatalf("unexpected terminal close: %+v", closeEnvelope)
}
if connection.Tunnel(dataEnvelope.TunnelID) != nil {
t.Fatal("source EOF retained the local tunnel generation")
}
select {
case extra := <-connection.send:
t.Fatalf("source EOF emitted an extra envelope: %+v", extra)
case <-time.After(50 * time.Millisecond):
}
}
func receiveTunnelEnvelope(t *testing.T, envelopes <-chan *sliverpb.Envelope) (*sliverpb.Envelope, *sliverpb.TunnelData) {
t.Helper()
select {
case envelope := <-envelopes:
decoded := &sliverpb.TunnelData{}
if err := proto.Unmarshal(envelope.Data, decoded); err != nil {
t.Fatalf("decode tunnel envelope: %v", err)
}
return envelope, decoded
case <-time.After(time.Second):
t.Fatal("timed out waiting for tunnel envelope")
return nil, nil
}
}
+43 -31
View File
@@ -19,7 +19,6 @@ package rportfwd
*/
import (
// {{if .Config.Debug}}
"log"
// {{end}}
@@ -32,43 +31,56 @@ import (
// tunnelWriter - Sends data back to the server based on data read()
// I know the reader/writer stuff is a little hard to keep track of
type tunnelWriter struct {
tun *transports.Tunnel
conn *transports.Connection
host string
port uint32
protocol int
tunnelID uint64
tun *transports.Tunnel
conn tunnelConnection
host string
port uint32
protocol int
tunnelID uint64
authorizationID string
}
func (tw tunnelWriter) Write(data []byte) (int, error) {
n := len(data)
sequence := tw.tun.NextWriteSequence()
ack := tw.tun.ReadSequence()
createReverse := sequence == 0
rportfwdInfo := &sliverpb.RPortfwd{}
if createReverse {
rportfwdInfo.Host = tw.host
rportfwdInfo.Port = tw.port
rportfwdInfo.Protocol = int32(tw.protocol)
rportfwdInfo.TunnelID = tw.tunnelID
}
data, err := proto.Marshal(&sliverpb.TunnelData{
Sequence: sequence,
Ack: ack,
TunnelID: tw.tun.ID,
Data: data,
CreateReverse: createReverse,
Rportfwd: rportfwdInfo,
err := tw.conn.QueueTunnelData(tw.tun, func(sequence uint64, ack uint64) (*sliverpb.Envelope, error) {
createReverse := sequence == 0
rportfwdInfo := &sliverpb.RPortfwd{}
if createReverse {
if tw.authorizationID == "" {
setLegacyRportfwdAddress(rportfwdInfo, tw.host, tw.port)
}
rportfwdInfo.Protocol = int32(tw.protocol)
rportfwdInfo.TunnelID = tw.tunnelID
rportfwdInfo.AuthorizationID = tw.authorizationID
}
marshaled, marshalErr := proto.Marshal(&sliverpb.TunnelData{
Sequence: sequence,
Ack: ack,
TunnelID: tw.tun.ID,
Data: data,
CreateReverse: createReverse,
Rportfwd: rportfwdInfo,
})
// {{if .Config.Debug}}
log.Printf("[tunnelWriter] Write %d bytes (write seq: %d) ack: %d", n, sequence, ack)
// {{end}}
if marshalErr != nil {
return nil, marshalErr
}
return &sliverpb.Envelope{
Type: sliverpb.MsgTunnelData,
Data: marshaled,
}, nil
})
// {{if .Config.Debug}}
log.Printf("[tunnelWriter] Write %d bytes (write seq: %d) ack: %d", n, sequence, ack)
// {{end}}
if err != nil {
return 0, err
}
tw.conn.Send <- &sliverpb.Envelope{
Type: sliverpb.MsgTunnelData,
Data: data,
}
return n, nil
}
// setLegacyRportfwdAddress isolates deprecated fields to compatibility traffic
// from a teamserver that did not issue an authorization capability.
func setLegacyRportfwdAddress(info *sliverpb.RPortfwd, host string, port uint32) {
info.Host = host //nolint:staticcheck // Required for exact legacy wire compatibility.
info.Port = port //nolint:staticcheck // Required for exact legacy wire compatibility.
}
@@ -0,0 +1,101 @@
package rportfwd
import (
"testing"
"github.com/bishopfox/sliver/implant/sliver/transports"
"github.com/bishopfox/sliver/protobuf/sliverpb"
"google.golang.org/protobuf/proto"
)
func TestTunnelWriterIncludesAuthorizationIDOnCreateReverse(t *testing.T) {
const (
authorizationID = "server-issued-authorization"
tunnelID = uint64(42)
)
connection := &transports.Connection{Send: make(chan *sliverpb.Envelope, 2)}
tunnel := transports.NewTunnel(tunnelID, nil)
if !connection.AddTunnel(tunnel) {
t.Fatal("failed to publish test tunnel")
}
t.Cleanup(func() { connection.CloseTunnelRemote(tunnel) })
writer := tunnelWriter{
tun: tunnel,
conn: connection,
host: "implant-supplied.example",
port: 8443,
protocol: sliverpb.PortFwdProtoTCP,
tunnelID: tunnelID,
authorizationID: authorizationID,
}
if _, err := writer.Write([]byte("first")); err != nil {
t.Fatalf("first Write() error = %v", err)
}
first := decodeTunnelData(t, <-connection.Send)
if !first.CreateReverse {
t.Fatal("first tunnel frame did not request reverse tunnel creation")
}
if first.Rportfwd == nil {
t.Fatal("first tunnel frame has no reverse port forward metadata")
}
if got := first.Rportfwd.AuthorizationID; got != authorizationID {
t.Fatalf("AuthorizationID = %q, want %q", got, authorizationID)
}
if first.Rportfwd.Host != "" || first.Rportfwd.Port != 0 { //nolint:staticcheck // Verify deprecated fields are absent from authorized traffic.
t.Fatalf("authorized create frame included legacy destination %q:%d", first.Rportfwd.Host, first.Rportfwd.Port) //nolint:staticcheck // Test diagnostics for deprecated compatibility fields.
}
if _, err := writer.Write([]byte("second")); err != nil {
t.Fatalf("second Write() error = %v", err)
}
second := decodeTunnelData(t, <-connection.Send)
if second.CreateReverse {
t.Fatal("subsequent tunnel frame unexpectedly requested reverse tunnel creation")
}
}
func TestTunnelWriterIncludesLegacyAddressWithoutAuthorization(t *testing.T) {
const tunnelID = uint64(43)
connection := &transports.Connection{Send: make(chan *sliverpb.Envelope, 1)}
tunnel := transports.NewTunnel(tunnelID, nil)
if !connection.AddTunnel(tunnel) {
t.Fatal("failed to publish test tunnel")
}
t.Cleanup(func() { connection.CloseTunnelRemote(tunnel) })
writer := tunnelWriter{
tun: tunnel,
conn: connection,
host: "legacy.example",
port: 9443,
protocol: sliverpb.PortFwdProtoTCP,
tunnelID: tunnelID,
}
if _, err := writer.Write([]byte("legacy")); err != nil {
t.Fatalf("Write() error = %v", err)
}
first := decodeTunnelData(t, <-connection.Send)
if first.Rportfwd == nil {
t.Fatal("first tunnel frame has no reverse port forward metadata")
}
if got := first.Rportfwd.AuthorizationID; got != "" {
t.Fatalf("AuthorizationID = %q, want empty", got)
}
if first.Rportfwd.Host != "legacy.example" || first.Rportfwd.Port != 9443 { //nolint:staticcheck // Verify deprecated compatibility fields remain available.
t.Fatalf("legacy destination = %q:%d, want legacy.example:9443", first.Rportfwd.Host, first.Rportfwd.Port) //nolint:staticcheck // Test diagnostics for deprecated compatibility fields.
}
}
func decodeTunnelData(t *testing.T, envelope *sliverpb.Envelope) *sliverpb.TunnelData {
t.Helper()
if envelope.Type != sliverpb.MsgTunnelData {
t.Fatalf("envelope type = %d, want %d", envelope.Type, sliverpb.MsgTunnelData)
}
tunnelData := &sliverpb.TunnelData{}
if err := proto.Unmarshal(envelope.Data, tunnelData); err != nil {
t.Fatalf("decode tunnel data: %v", err)
}
return tunnelData
}
@@ -5,5 +5,7 @@ package runner
import "github.com/bishopfox/sliver/protobuf/sliverpb"
func implantCapabilities() uint64 {
return uint64(sliverpb.ImplantCapability_IMPLANT_CAPABILITY_BOF_V1)
return uint64(sliverpb.ImplantCapability_IMPLANT_CAPABILITY_BOF_V1) |
uint64(sliverpb.ImplantCapability_IMPLANT_CAPABILITY_TUNNEL_TERMINAL_V1) |
uint64(sliverpb.ImplantCapability_IMPLANT_CAPABILITY_SOCKS_FLOW_CONTROL_V1)
}
@@ -2,6 +2,9 @@
package runner
import "github.com/bishopfox/sliver/protobuf/sliverpb"
func implantCapabilities() uint64 {
return 0
return uint64(sliverpb.ImplantCapability_IMPLANT_CAPABILITY_TUNNEL_TERMINAL_V1) |
uint64(sliverpb.ImplantCapability_IMPLANT_CAPABILITY_SOCKS_FLOW_CONTROL_V1)
}
+28 -11
View File
@@ -643,7 +643,7 @@ func sessionMainLoop(connection *transports.Connection) error {
// {{end}}
return err
}
pivots.RestartAllListeners(connection.Send)
pivots.RestartAllListeners(connection.SendEnvelope)
defer pivots.StopAllListeners()
defer connection.Stop()
@@ -653,7 +653,9 @@ func sessionMainLoop(connection *transports.Connection) error {
register := registerSliver()
register.ActiveC2 = connection.URL()
register.ProxyURL = connection.ProxyURL()
connection.Send <- wrapEnvelope(sliverpb.MsgRegister, register) // Send registration information
if !connection.SendEnvelope(wrapEnvelope(sliverpb.MsgRegister, register)) {
return nil
}
pivotHandlers := handlers.GetPivotHandlers()
tunHandlers := handlers.GetTunnelHandlers()
@@ -661,8 +663,20 @@ func sessionMainLoop(connection *transports.Connection) error {
specialHandlers := handlers.GetKillHandlers()
rportfwdHandlers := handlers.GetRportFwdHandlers()
for envelope := range connection.Recv {
envelope := envelope
for {
var envelope *sliverpb.Envelope
select {
case received, ok := <-connection.Recv:
if !ok {
return nil
}
envelope = received
case <-connection.Done():
return nil
}
if envelope == nil {
continue
}
if _, ok := specialHandlers[envelope.Type]; ok {
// {{if .Config.Debug}}
log.Printf("[recv] specialHandler %d", envelope.Type)
@@ -689,6 +703,7 @@ func sessionMainLoop(connection *transports.Connection) error {
log.Printf("[recv] sysHandler %d", envelope.Type)
// {{end}}
responseID := envelope.ID
// {{if eq .Config.GOOS "windows" }}
go handlers.WrapperHandler(handler, envelope.Data, func(data []byte, err error) {
// {{if .Config.Debug}}
@@ -696,10 +711,10 @@ func sessionMainLoop(connection *transports.Connection) error {
log.Printf("[session] handler function returned an error: %s", err)
}
// {{end}}
connection.Send <- &sliverpb.Envelope{
ID: envelope.ID,
connection.SendEnvelope(&sliverpb.Envelope{
ID: responseID,
Data: data,
}
})
})
// {{else}}
go handler(envelope.Data, func(data []byte, err error) {
@@ -708,10 +723,10 @@ func sessionMainLoop(connection *transports.Connection) error {
log.Printf("[session] handler function returned an error: %s", err)
}
// {{end}}
connection.Send <- &sliverpb.Envelope{
ID: envelope.ID,
connection.SendEnvelope(&sliverpb.Envelope{
ID: responseID,
Data: data,
}
})
})
// {{end}}
} else if handler, ok := tunHandlers[envelope.Type]; ok {
@@ -725,10 +740,12 @@ func sessionMainLoop(connection *transports.Connection) error {
// {{if .Config.Debug}}
log.Printf("[recv] unknown envelope type %d", envelope.Type)
// {{end}}
connection.Send <- &sliverpb.Envelope{
if !connection.SendEnvelope(&sliverpb.Envelope{
ID: envelope.ID,
Data: nil,
UnknownMessageType: true,
}) {
return nil
}
}
}
+493 -19
View File
@@ -19,21 +19,90 @@ package transports
*/
import (
"context"
"net/url"
"sync"
"time"
pb "github.com/bishopfox/sliver/protobuf/sliverpb"
"google.golang.org/protobuf/proto"
)
const (
tunnelSendTimeout = 10 * time.Second
// Bound live tunnel state independently from the replay tombstone window.
// Retired IDs are remembered in FIFO order; once the window rotates, exact
// pointer checks still prevent stale local work from deleting a replacement,
// while random 64-bit wire IDs make an accidental post-window reuse remote.
maxLiveTunnelIDsPerConnection = 4096
maxRetiredTunnelIDsPerConnection = 4096
)
// TunnelAddResult distinguishes a replayed ID from exhaustion of the bounded
// live-tunnel domain. Capacity exhaustion rejects only the new tunnel and
// leaves the C2 connection available for existing and later work.
type TunnelAddResult uint8
// TunnelAdded and the related results describe a tunnel publication attempt.
const (
TunnelAdded TunnelAddResult = iota
TunnelAddDuplicate
TunnelAddCapacityExhausted
TunnelAddConnectionClosed
TunnelAddInvalid
TunnelAddSetupCanceled
)
// PendingTunnel owns a not-yet-published tunnel setup for one C2 connection.
// Its wire ID is reserved before any external resource is opened, so an
// overtaking close can cancel the exact setup and place the ID in the bounded
// replay window before a later setup can reuse it.
type PendingTunnel struct {
connection *Connection
tunnelID uint64
ctx context.Context
cancel context.CancelFunc
}
// Context is canceled when the setup is retired, its deadline expires, or its
// owning C2 connection closes.
func (p *PendingTunnel) Context() context.Context {
if p == nil || p.ctx == nil {
return context.Background()
}
return p.ctx
}
// Cancel retires this exact setup without affecting another tunnel ID.
func (p *PendingTunnel) Cancel() {
if p == nil {
return
}
if p.cancel != nil {
p.cancel()
}
if p.connection != nil {
p.connection.cancelPendingTunnelIf(p)
}
}
type Connection struct {
Send chan *pb.Envelope
Recv chan *pb.Envelope
IsOpen bool
ctrl chan struct{}
cleanup func()
once *sync.Once
tunnels map[uint64]*Tunnel
mutex *sync.RWMutex
Send chan *pb.Envelope
Recv chan *pb.Envelope
IsOpen bool
ctrl chan struct{}
cleanup func()
cleanupOnce sync.Once
lifecycleOnce sync.Once
stateOnce sync.Once
done chan struct{}
tunnels map[uint64]*Tunnel
pendingTunnels map[uint64]*PendingTunnel
retiredTunnels map[uint64]struct{}
retiredOrder []uint64
retiredCursor int
mutex *sync.RWMutex
uri *url.URL
proxyURL *url.URL
@@ -42,6 +111,77 @@ type Connection struct {
Stop Stop
}
// TunnelEnvelopeBuilder builds a sequenced tunnel envelope for transport.
type TunnelEnvelopeBuilder func(sequence uint64, ack uint64) (*pb.Envelope, error)
func (c *Connection) initializeLifecycle() {
c.lifecycleOnce.Do(func() {
c.done = make(chan struct{})
})
}
func (c *Connection) initializeTunnelState() {
c.stateOnce.Do(func() {
if c.mutex == nil {
c.mutex = &sync.RWMutex{}
}
if c.tunnels == nil {
c.tunnels = map[uint64]*Tunnel{}
}
if c.pendingTunnels == nil {
c.pendingTunnels = map[uint64]*PendingTunnel{}
}
if c.retiredTunnels == nil {
c.retiredTunnels = map[uint64]struct{}{}
}
})
}
// Done is closed before transport cleanup starts. Tunnel producers use it to
// abandon an outbound envelope when the C2 connection is no longer usable.
func (c *Connection) Done() <-chan struct{} {
c.initializeLifecycle()
return c.done
}
// SendEnvelope queues an envelope while the connection remains live.
func (c *Connection) SendEnvelope(envelope *pb.Envelope) bool {
if c == nil || envelope == nil || c.Send == nil {
return false
}
timer := time.NewTimer(tunnelSendTimeout)
defer timer.Stop()
select {
case c.Send <- envelope:
return true
case <-c.Done():
return false
case <-timer.C:
return false
}
}
// SendTunnelEnvelope bounds a producer on both the C2 and exact tunnel
// lifecycles. Closing one tunnel therefore unblocks its writers without waiting
// for an otherwise healthy connection to reconnect.
func (c *Connection) SendTunnelEnvelope(tunnel *Tunnel, envelope *pb.Envelope) bool {
if c == nil || tunnel == nil || envelope == nil || c.Send == nil {
return false
}
timer := time.NewTimer(tunnelSendTimeout)
defer timer.Stop()
select {
case c.Send <- envelope:
return true
case <-tunnel.Done():
return false
case <-c.Done():
return false
case <-timer.C:
return false
}
}
// URL - Get the c2 URL of the connection
func (c *Connection) URL() string {
if c.uri == nil {
@@ -60,8 +200,12 @@ func (c *Connection) ProxyURL() string {
// Cleanup - Execute cleanup once
func (c *Connection) Cleanup() {
c.once.Do(func() {
c.cleanup()
c.initializeLifecycle()
c.cleanupOnce.Do(func() {
close(c.done)
if c.cleanup != nil {
c.cleanup()
}
c.IsOpen = false
c.removeAndCloseAllTunnels()
})
@@ -69,41 +213,371 @@ func (c *Connection) Cleanup() {
// Tunnel - Add tunnel to mapping
func (c *Connection) Tunnel(ID uint64) *Tunnel {
if c == nil {
return nil
}
c.initializeTunnelState()
c.mutex.RLock()
defer c.mutex.RUnlock()
return c.tunnels[ID]
}
// AddTunnel - Add tunnel to mapping
func (c *Connection) AddTunnel(tun *Tunnel) {
// TryAddTunnel adds a tunnel generation to the connection. Recently retired
// wire IDs remain unavailable within a bounded replay window. Live-capacity
// exhaustion rejects this generation without failing unrelated C2 work.
func (c *Connection) TryAddTunnel(tun *Tunnel) TunnelAddResult {
if c == nil || tun == nil {
return TunnelAddInvalid
}
c.initializeLifecycle()
c.initializeTunnelState()
c.mutex.Lock()
if result := c.tunnelAdmissionResultLocked(tun.ID); result != TunnelAdded {
c.mutex.Unlock()
return result
}
c.publishTunnelLocked(tun)
c.mutex.Unlock()
return TunnelAdded
}
// tunnelAdmissionResultLocked validates one new wire generation. The caller
// must hold c.mutex so live, pending, retired, and capacity checks observe one
// consistent connection state.
func (c *Connection) tunnelAdmissionResultLocked(tunnelID uint64) TunnelAddResult {
select {
case <-c.done:
return TunnelAddConnectionClosed
default:
}
if c.tunnels[tunnelID] != nil || c.pendingTunnels[tunnelID] != nil {
return TunnelAddDuplicate
}
if _, retired := c.retiredTunnels[tunnelID]; retired {
return TunnelAddDuplicate
}
if len(c.tunnels)+len(c.pendingTunnels) >= maxLiveTunnelIDsPerConnection {
return TunnelAddCapacityExhausted
}
return TunnelAdded
}
// BeginTunnel reserves one tunnel generation before a handler opens an
// external resource. An overtaking peer close retires the wire ID into the
// bounded replay window and cancels the returned context. Timeout must be finite
// for network setup callers; non-positive values create an owner-cancelable
// reservation for callers that already impose a stricter deadline.
func (c *Connection) BeginTunnel(tunnelID uint64, timeout time.Duration) (*PendingTunnel, TunnelAddResult) {
if c == nil {
return nil, TunnelAddInvalid
}
c.initializeLifecycle()
c.initializeTunnelState()
var (
ctx context.Context
cancel context.CancelFunc
)
if timeout > 0 {
ctx, cancel = context.WithTimeout(context.Background(), timeout)
} else {
ctx, cancel = context.WithCancel(context.Background())
}
pending := &PendingTunnel{
connection: c,
tunnelID: tunnelID,
ctx: ctx,
cancel: cancel,
}
c.mutex.Lock()
if result := c.tunnelAdmissionResultLocked(tunnelID); result != TunnelAdded {
c.mutex.Unlock()
cancel()
return nil, result
}
c.pendingTunnels[tunnelID] = pending
c.mutex.Unlock()
// Context cancellation must detach a non-cooperative setup even when the
// handler has not returned from its dial call yet.
go func() {
select {
case <-c.Done():
pending.Cancel()
case <-ctx.Done():
pending.Cancel()
}
}()
return pending, TunnelAdded
}
// PublishTunnel atomically replaces an exact pending setup with its active
// tunnel. A canceled, timed-out, disconnected, or superseded setup can never
// publish a late socket.
func (c *Connection) PublishTunnel(pending *PendingTunnel, tun *Tunnel) TunnelAddResult {
if c == nil || pending == nil || tun == nil || pending.connection != c || pending.tunnelID != tun.ID {
return TunnelAddInvalid
}
c.initializeLifecycle()
c.initializeTunnelState()
c.mutex.Lock()
select {
case <-c.done:
c.mutex.Unlock()
pending.Cancel()
return TunnelAddConnectionClosed
default:
}
if c.pendingTunnels[tun.ID] != pending {
c.mutex.Unlock()
pending.Cancel()
return TunnelAddSetupCanceled
}
if pending.ctx.Err() != nil {
delete(c.pendingTunnels, tun.ID)
c.retireTunnelIDLocked(tun.ID)
c.mutex.Unlock()
pending.Cancel()
return TunnelAddSetupCanceled
}
if c.tunnels[tun.ID] != nil {
delete(c.pendingTunnels, tun.ID)
c.mutex.Unlock()
pending.Cancel()
return TunnelAddDuplicate
}
delete(c.pendingTunnels, tun.ID)
c.publishTunnelLocked(tun)
c.mutex.Unlock()
// Stop the setup deadline only after the active map owns the tunnel.
pending.cancel()
return TunnelAdded
}
// CancelPendingTunnel retires an unowned or pending wire ID and cancels the
// exact pending setup. If publication won the race, the active tunnel remains
// available for the caller to close normally.
func (c *Connection) CancelPendingTunnel(tunnelID uint64) bool {
if c == nil {
return false
}
c.initializeLifecycle()
c.initializeTunnelState()
var pending *PendingTunnel
c.mutex.Lock()
select {
case <-c.done:
c.mutex.Unlock()
return false
default:
}
if c.tunnels[tunnelID] != nil {
c.mutex.Unlock()
return false
}
pending = c.pendingTunnels[tunnelID]
if pending != nil {
delete(c.pendingTunnels, tunnelID)
c.retireTunnelIDLocked(tunnelID)
} else if _, retired := c.retiredTunnels[tunnelID]; !retired {
c.retireTunnelIDLocked(tunnelID)
}
c.mutex.Unlock()
if pending != nil {
pending.cancel()
return true
}
return false
}
func (c *Connection) cancelPendingTunnelIf(pending *PendingTunnel) bool {
if c == nil || pending == nil {
return false
}
c.initializeTunnelState()
c.mutex.Lock()
defer c.mutex.Unlock()
if c.pendingTunnels[pending.tunnelID] != pending {
return false
}
delete(c.pendingTunnels, pending.tunnelID)
c.retireTunnelIDLocked(pending.tunnelID)
return true
}
// retireTunnelIDLocked records one completed or canceled wire generation in a
// fixed-size FIFO replay window. The caller must hold c.mutex.
func (c *Connection) retireTunnelIDLocked(tunnelID uint64) {
if _, retired := c.retiredTunnels[tunnelID]; retired {
return
}
if len(c.retiredOrder) < maxRetiredTunnelIDsPerConnection {
c.retiredOrder = append(c.retiredOrder, tunnelID)
c.retiredTunnels[tunnelID] = struct{}{}
return
}
evicted := c.retiredOrder[c.retiredCursor]
delete(c.retiredTunnels, evicted)
c.retiredOrder[c.retiredCursor] = tunnelID
c.retiredCursor = (c.retiredCursor + 1) % maxRetiredTunnelIDsPerConnection
c.retiredTunnels[tunnelID] = struct{}{}
}
func (c *Connection) publishTunnelLocked(tun *Tunnel) {
tun.setPeerCloseNotifier(func(sequence uint64) error {
data, err := proto.Marshal(&pb.TunnelData{
Closed: true,
TunnelID: tun.ID,
Sequence: sequence,
})
if err != nil {
return err
}
if !c.SendTunnelEnvelope(tun, &pb.Envelope{Type: pb.MsgTunnelClose, Data: data}) {
return ErrTunnelClosed
}
return nil
})
c.tunnels[tun.ID] = tun
}
// AddTunnel is the compatibility boolean wrapper for callers that do not need
// to distinguish duplicate IDs, live-capacity rejection, or connection close.
func (c *Connection) AddTunnel(tun *Tunnel) bool {
return c.TryAddTunnel(tun) == TunnelAdded
}
func (c *Connection) ownsTunnel(tunnel *Tunnel) bool {
if c == nil || tunnel == nil {
return false
}
c.initializeTunnelState()
c.mutex.RLock()
defer c.mutex.RUnlock()
return c.tunnels[tunnel.ID] == tunnel
}
// QueueTunnelData combines exact-generation ownership, outbound sequencing,
// bounded delivery, and advancement after acceptance in one API.
func (c *Connection) QueueTunnelData(tunnel *Tunnel, build TunnelEnvelopeBuilder) error {
if c == nil || tunnel == nil || build == nil {
return ErrTunnelClosed
}
return tunnel.queueOutbound(func(sequence uint64) error {
if !c.ownsTunnel(tunnel) {
return ErrTunnelClosed
}
envelope, err := build(sequence, tunnel.ReadSequence())
if err != nil {
return err
}
if !c.SendTunnelEnvelope(tunnel, envelope) {
return ErrTunnelClosed
}
return nil
})
}
// QueueTunnelControl serializes resend/control frames with data and terminal
// close without consuming the next data sequence.
func (c *Connection) QueueTunnelControl(tunnel *Tunnel, build TunnelEnvelopeBuilder) error {
if c == nil || tunnel == nil || build == nil {
return ErrTunnelClosed
}
return tunnel.queueControl(func(sequence uint64, ack uint64) error {
if !c.ownsTunnel(tunnel) {
return ErrTunnelClosed
}
envelope, err := build(sequence, ack)
if err != nil {
return err
}
if !c.SendTunnelEnvelope(tunnel, envelope) {
return ErrTunnelClosed
}
return nil
})
}
// RemoveTunnel - Add tunnel to mapping
func (c *Connection) RemoveTunnel(ID uint64) {
if c == nil {
return
}
c.initializeTunnelState()
c.mutex.Lock()
defer c.mutex.Unlock()
if c.tunnels[ID] != nil {
delete(c.tunnels, ID)
c.retireTunnelIDLocked(ID)
}
}
// RemoveTunnelIf detaches ID only when it still refers to the expected tunnel.
// This prevents a stale local or peer close from deleting a reused generation.
func (c *Connection) RemoveTunnelIf(ID uint64, expected *Tunnel) bool {
if c == nil || expected == nil {
return false
}
c.initializeTunnelState()
c.mutex.Lock()
defer c.mutex.Unlock()
if c.tunnels[ID] != expected {
return false
}
delete(c.tunnels, ID)
c.retireTunnelIDLocked(ID)
return true
}
// CloseTunnelLocal atomically detaches one generation, emits its sequenced
// terminal notification exactly once, and fails the C2 closed if delivery is
// impossible. A disconnect then guarantees server-side relay cleanup.
func (c *Connection) CloseTunnelLocal(tunnel *Tunnel) bool {
if tunnel == nil || !c.ownsTunnel(tunnel) {
return false
}
initiated, err := tunnel.closeLocal()
if !initiated || !c.RemoveTunnelIf(tunnel.ID, tunnel) {
return false
}
if err != nil && !tunnel.PeerTeardownPending() {
c.Cleanup()
}
return true
}
// CloseTunnelRemote atomically detaches a peer-closed generation without
// echoing another terminal frame.
func (c *Connection) CloseTunnelRemote(tunnel *Tunnel) bool {
if tunnel == nil || !c.ownsTunnel(tunnel) {
return false
}
tunnel.closeRemote()
return c.RemoveTunnelIf(tunnel.ID, tunnel)
}
func (c *Connection) removeAndCloseAllTunnels() {
if c == nil {
return
}
c.initializeTunnelState()
c.mutex.Lock()
defer c.mutex.Unlock()
for id, pending := range c.pendingTunnels {
pending.cancel()
delete(c.pendingTunnels, id)
}
for id, tunnel := range c.tunnels {
tunnel.Close()
delete(c.tunnels, id)
}
}
func (c *Connection) RequestResend(data []byte) {
c.Send <- &pb.Envelope{
Type: pb.MsgTunnelData,
Data: data,
}
clear(c.retiredTunnels)
c.retiredOrder = nil
c.retiredCursor = 0
}
@@ -0,0 +1,368 @@
package transports
import (
"context"
"errors"
"io"
"sync"
"testing"
"time"
"github.com/bishopfox/sliver/protobuf/sliverpb"
)
type closeSignalWriteCloser struct {
closed chan struct{}
once sync.Once
}
func newCloseSignalWriteCloser() *closeSignalWriteCloser {
return &closeSignalWriteCloser{closed: make(chan struct{})}
}
func (*closeSignalWriteCloser) Write(data []byte) (int, error) { return len(data), nil }
func (c *closeSignalWriteCloser) Close() error {
c.once.Do(func() { close(c.closed) })
return nil
}
func TestNextSendEnvelopeSkipsNilAndStopsWithConnection(t *testing.T) {
t.Run("skips nil envelope", func(t *testing.T) {
connection := &Connection{}
send := make(chan *sliverpb.Envelope, 2)
send <- nil
want := &sliverpb.Envelope{Type: sliverpb.MsgTunnelData}
send <- want
got, ok := nextSendEnvelope(connection, send)
if !ok {
t.Fatal("nextSendEnvelope stopped while the connection was live")
}
if got != want {
t.Fatalf("nextSendEnvelope() = %p, want %p", got, want)
}
})
t.Run("stops on connection cleanup", func(t *testing.T) {
connection := &Connection{}
send := make(chan *sliverpb.Envelope)
result := make(chan bool, 1)
go func() {
_, ok := nextSendEnvelope(connection, send)
result <- ok
}()
connection.Cleanup()
select {
case ok := <-result:
if ok {
t.Fatal("nextSendEnvelope accepted an envelope after cleanup")
}
case <-time.After(time.Second):
t.Fatal("nextSendEnvelope remained blocked after cleanup")
}
})
}
func TestConnectionRejectsRetiredTunnelID(t *testing.T) {
connection := &Connection{Send: make(chan *sliverpb.Envelope, 1)}
first := NewTunnel(77, nopWriteCloser{Writer: io.Discard})
if result := connection.TryAddTunnel(first); result != TunnelAdded {
t.Fatalf("first tunnel generation result = %v, want %v", result, TunnelAdded)
}
if !connection.CloseTunnelRemote(first) {
t.Fatal("failed to close first tunnel generation")
}
second := NewTunnel(77, nopWriteCloser{Writer: io.Discard})
if result := connection.TryAddTunnel(second); result != TunnelAddDuplicate {
t.Fatalf("retired tunnel ID result = %v, want %v", result, TunnelAddDuplicate)
}
second.Close()
}
func TestConnectionPendingTunnelCloseRejectsLatePublication(t *testing.T) {
connection := &Connection{Send: make(chan *sliverpb.Envelope, 1)}
pending, result := connection.BeginTunnel(0x7711, time.Hour)
if result != TunnelAdded {
t.Fatalf("begin pending tunnel result = %v, want %v", result, TunnelAdded)
}
if !connection.CancelPendingTunnel(0x7711) {
t.Fatal("pending tunnel close did not cancel the setup")
}
select {
case <-pending.Context().Done():
if !errors.Is(pending.Context().Err(), context.Canceled) {
t.Fatalf("pending context error = %v, want %v", pending.Context().Err(), context.Canceled)
}
case <-time.After(time.Second):
t.Fatal("pending tunnel close did not cancel its setup context")
}
writer := newCloseSignalWriteCloser()
late := NewTunnel(0x7711, writer)
if result := connection.PublishTunnel(pending, late); result != TunnelAddSetupCanceled {
t.Fatalf("late publication result = %v, want %v", result, TunnelAddSetupCanceled)
}
late.Close()
select {
case <-writer.closed:
default:
t.Fatal("rejected late tunnel resource was not closed by its caller")
}
if active := connection.Tunnel(0x7711); active != nil {
t.Fatalf("late tunnel remained published: %p", active)
}
if replacement, result := connection.BeginTunnel(0x7711, time.Hour); result != TunnelAddDuplicate || replacement != nil {
t.Fatalf("retired pending ID replacement = %p, %v; want nil, %v", replacement, result, TunnelAddDuplicate)
}
}
func TestConnectionPendingTunnelOwnerDisconnectAndDeadlineCancel(t *testing.T) {
tests := []struct {
name string
timeout time.Duration
close bool
wantErr error
wantAdd TunnelAddResult
}{
{name: "owner disconnect", timeout: time.Hour, close: true, wantErr: context.Canceled, wantAdd: TunnelAddConnectionClosed},
{name: "setup deadline", timeout: 20 * time.Millisecond, wantErr: context.DeadlineExceeded, wantAdd: TunnelAddSetupCanceled},
}
for index, test := range tests {
t.Run(test.name, func(t *testing.T) {
connection := &Connection{Send: make(chan *sliverpb.Envelope, 1)}
tunnelID := uint64(0x7720 + index)
pending, result := connection.BeginTunnel(tunnelID, test.timeout)
if result != TunnelAdded {
t.Fatalf("begin pending tunnel result = %v, want %v", result, TunnelAdded)
}
if test.close {
connection.Cleanup()
}
select {
case <-pending.Context().Done():
if !errors.Is(pending.Context().Err(), test.wantErr) {
t.Fatalf("pending context error = %v, want %v", pending.Context().Err(), test.wantErr)
}
case <-time.After(time.Second):
t.Fatal("pending setup context was not canceled")
}
late := NewTunnel(tunnelID, nopWriteCloser{Writer: io.Discard})
if result := connection.PublishTunnel(pending, late); result != test.wantAdd {
t.Fatalf("late publication result = %v, want %v", result, test.wantAdd)
}
late.Close()
if active := connection.Tunnel(tunnelID); active != nil {
t.Fatalf("late tunnel remained published: %p", active)
}
})
}
}
// This lifecycle test intentionally exercises admission, retirement, replay,
// generation identity, and connection survival in one ordered scenario.
//
//nolint:gocyclo
func TestConnectionRetiredTunnelWindowRotatesWithoutClosingConnection(t *testing.T) {
cleanupCalled := make(chan struct{})
connection := &Connection{
Send: make(chan *sliverpb.Envelope, 1),
cleanup: func() {
close(cleanupCalled)
},
}
var first *Tunnel
total := maxRetiredTunnelIDsPerConnection + 64
for tunnelID := uint64(0); tunnelID < uint64(total); tunnelID++ {
tunnel := NewTunnel(tunnelID, nopWriteCloser{Writer: io.Discard})
if tunnelID == 0 {
first = tunnel
}
if result := connection.TryAddTunnel(tunnel); result != TunnelAdded {
t.Fatalf("add tunnel %d result = %v, want %v", tunnelID, result, TunnelAdded)
}
if !connection.CloseTunnelRemote(tunnel) {
t.Fatalf("failed to retire tunnel %d", tunnelID)
}
}
select {
case <-cleanupCalled:
t.Fatal("rotating retired tunnel IDs cleaned up the connection")
case <-connection.Done():
t.Fatal("rotating retired tunnel IDs closed the connection")
default:
}
connection.mutex.RLock()
retiredCount := len(connection.retiredTunnels)
retiredOrderCount := len(connection.retiredOrder)
connection.mutex.RUnlock()
if retiredCount != maxRetiredTunnelIDsPerConnection || retiredOrderCount != maxRetiredTunnelIDsPerConnection {
t.Fatalf(
"retired replay window = map:%d order:%d, want %d each",
retiredCount,
retiredOrderCount,
maxRetiredTunnelIDsPerConnection,
)
}
newestID := uint64(total - 1)
duplicate := NewTunnel(newestID, nopWriteCloser{Writer: io.Discard})
if result := connection.TryAddTunnel(duplicate); result != TunnelAddDuplicate {
t.Fatalf("recent retired tunnel ID result = %v, want %v", result, TunnelAddDuplicate)
}
duplicate.Close()
// ID zero has rotated out of the bounded wire replay window. A fresh random
// collision may reuse it, but stale in-process work still carries the exact
// old pointer and cannot remove the replacement generation.
replacement := NewTunnel(0, nopWriteCloser{Writer: io.Discard})
if result := connection.TryAddTunnel(replacement); result != TunnelAdded {
t.Fatalf("post-window replacement result = %v, want %v", result, TunnelAdded)
}
if connection.CloseTunnelRemote(first) {
t.Fatal("stale exact tunnel generation closed its replacement")
}
if active := connection.Tunnel(replacement.ID); active != replacement {
t.Fatalf("replacement tunnel = %p, want %p", active, replacement)
}
if !connection.CloseTunnelRemote(replacement) {
t.Fatal("failed to close replacement tunnel")
}
select {
case <-connection.Done():
t.Fatal("replacement lifecycle closed the connection")
case <-cleanupCalled:
t.Fatal("replacement lifecycle cleaned up the connection")
default:
}
}
func TestConnectionLiveTunnelCapacityRejectsOnlyNewGeneration(t *testing.T) {
cleanupCalled := make(chan struct{})
connection := &Connection{
Send: make(chan *sliverpb.Envelope, 1),
cleanup: func() {
close(cleanupCalled)
},
}
tunnels := make([]*Tunnel, 0, maxLiveTunnelIDsPerConnection)
for tunnelID := uint64(0); tunnelID < uint64(maxLiveTunnelIDsPerConnection); tunnelID++ {
tunnel := NewTunnel(tunnelID, nopWriteCloser{Writer: io.Discard})
if result := connection.TryAddTunnel(tunnel); result != TunnelAdded {
t.Fatalf("add live tunnel %d result = %v, want %v", tunnelID, result, TunnelAdded)
}
tunnels = append(tunnels, tunnel)
}
overflow := NewTunnel(uint64(maxLiveTunnelIDsPerConnection), nopWriteCloser{Writer: io.Discard})
if result := connection.TryAddTunnel(overflow); result != TunnelAddCapacityExhausted {
t.Fatalf("live capacity result = %v, want %v", result, TunnelAddCapacityExhausted)
}
overflow.Close()
select {
case <-connection.Done():
t.Fatal("live capacity rejection closed the connection")
case <-cleanupCalled:
t.Fatal("live capacity rejection cleaned up the connection")
default:
}
if !connection.CloseTunnelRemote(tunnels[0]) {
t.Fatal("failed to retire one live tunnel")
}
replacement := NewTunnel(uint64(maxLiveTunnelIDsPerConnection), nopWriteCloser{Writer: io.Discard})
if result := connection.TryAddTunnel(replacement); result != TunnelAdded {
t.Fatalf("add after live capacity release = %v, want %v", result, TunnelAdded)
}
connection.Cleanup()
}
func TestConnectionTunnelCloseUnblocksSaturatedDataSend(t *testing.T) {
connection := &Connection{Send: make(chan *sliverpb.Envelope)}
tunnel := NewTunnel(78, nopWriteCloser{Writer: io.Discard})
if !connection.AddTunnel(tunnel) {
t.Fatal("failed to add tunnel")
}
result := make(chan error, 1)
go func() {
result <- connection.QueueTunnelData(tunnel, func(uint64, uint64) (*sliverpb.Envelope, error) {
return &sliverpb.Envelope{Type: sliverpb.MsgTunnelData}, nil
})
}()
select {
case err := <-result:
t.Fatalf("saturated send returned before close: %v", err)
case <-time.After(25 * time.Millisecond):
}
if !connection.CloseTunnelRemote(tunnel) {
t.Fatal("failed to close saturated tunnel")
}
select {
case err := <-result:
if !errors.Is(err, ErrTunnelClosed) {
t.Fatalf("QueueTunnelData() error = %v, want %v", err, ErrTunnelClosed)
}
case <-time.After(time.Second):
t.Fatal("tunnel close did not unblock saturated send")
}
}
func TestConnectionSimultaneousPeerCloseDoesNotFailC2(t *testing.T) {
connection := &Connection{Send: make(chan *sliverpb.Envelope)}
tunnel := NewTunnel(79, nopWriteCloser{Writer: io.Discard})
if !connection.AddTunnel(tunnel) {
t.Fatal("failed to add tunnel")
}
notifierStarted := make(chan struct{})
tunnel.setPeerCloseNotifier(func(uint64) error {
close(notifierStarted)
<-tunnel.Done()
return ErrTunnelClosed
})
localResult := make(chan bool, 1)
go func() {
localResult <- connection.CloseTunnelLocal(tunnel)
}()
select {
case <-notifierStarted:
case <-time.After(time.Second):
t.Fatal("local terminal notifier did not start")
}
remoteResult := make(chan bool, 1)
go func() {
remoteResult <- connection.CloseTunnelRemote(tunnel)
}()
select {
case <-localResult:
case <-time.After(time.Second):
t.Fatal("local close did not finish after peer teardown")
}
select {
case <-remoteResult:
case <-time.After(time.Second):
t.Fatal("remote close did not finish")
}
if active := connection.Tunnel(tunnel.ID); active != nil {
t.Fatalf("simultaneously closed tunnel remained published: %p", active)
}
if !tunnel.PeerTeardownPending() {
t.Fatal("remote close did not publish peer ownership")
}
if tunnel.PeerClosePending() {
t.Fatal("unsequenced remote close installed a terminal sequence")
}
select {
case <-connection.Done():
t.Fatal("normal simultaneous peer close failed the C2 connection")
default:
}
}
@@ -532,11 +532,20 @@ func (s *SliverHTTPClient) WriteEnvelope(envelope *pb.Envelope) error {
log.Printf("[http] POST request completed")
// {{end}}
if err != nil {
if resp != nil {
err = errors.Join(err, drainAndCloseHTTPResponse(resp))
}
// {{if .Config.Debug}}
log.Printf("[http] request failed %v", err)
// {{end}}
return err
}
if resp == nil {
return errors.New("empty HTTP POST response")
}
if err := drainAndCloseHTTPResponse(resp); err != nil {
return fmt.Errorf("consume HTTP POST response: %w", err)
}
if resp.StatusCode != http.StatusAccepted {
// {{if .Config.Debug}}
log.Printf("[http] non-202 response (%d): %v", resp.StatusCode, resp)
@@ -546,6 +555,15 @@ func (s *SliverHTTPClient) WriteEnvelope(envelope *pb.Envelope) error {
return nil
}
func drainAndCloseHTTPResponse(resp *http.Response) error {
if resp == nil || resp.Body == nil {
return nil
}
_, readErr := io.Copy(io.Discard, resp.Body)
closeErr := resp.Body.Close()
return errors.Join(readErr, closeErr)
}
func (s *SliverHTTPClient) CloseSession() error {
if s.Closed {
return nil
@@ -0,0 +1,119 @@
package httpclient
import (
"bytes"
"errors"
"io"
"net/http"
"testing"
"github.com/bishopfox/sliver/implant/sliver/cryptography"
"github.com/bishopfox/sliver/protobuf/sliverpb"
)
type staticHTTPDriver struct {
response *http.Response
err error
}
func (driver *staticHTTPDriver) Do(*http.Request) (*http.Response, error) {
return driver.response, driver.err
}
type trackingHTTPBody struct {
reader io.Reader
readToEOF bool
closeCalls int
closeErr error
}
func (body *trackingHTTPBody) Read(buffer []byte) (int, error) {
count, err := body.reader.Read(buffer)
if errors.Is(err, io.EOF) {
body.readToEOF = true
}
return count, err
}
func (body *trackingHTTPBody) Close() error {
body.closeCalls++
return body.closeErr
}
func newHTTPWriteTestClient(response *http.Response) *SliverHTTPClient {
key := [32]byte{1, 2, 3, 4}
return &SliverHTTPClient{
Origin: "http://127.0.0.1",
driver: &staticHTTPDriver{response: response},
SessionCtx: cryptography.NewCipherContext(key),
SessionID: "test-session",
Options: &HTTPOptions{},
}
}
func TestWriteEnvelopeDrainsAndClosesResponseBody(t *testing.T) {
tests := []struct {
name string
status int
wantErr error
}{
{name: "accepted", status: http.StatusAccepted},
{name: "rejected", status: http.StatusInternalServerError, wantErr: ErrStatusCodeUnexpected},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
body := &trackingHTTPBody{reader: bytes.NewBufferString("response-body")}
client := newHTTPWriteTestClient(&http.Response{StatusCode: test.status, Body: body})
err := client.WriteEnvelope(&sliverpb.Envelope{Type: sliverpb.MsgPing})
if !errors.Is(err, test.wantErr) {
t.Fatalf("WriteEnvelope() error = %v, want %v", err, test.wantErr)
}
if !body.readToEOF {
t.Fatal("WriteEnvelope did not drain the HTTP response body")
}
if body.closeCalls != 1 {
t.Fatalf("HTTP response body closed %d times, want 1", body.closeCalls)
}
})
}
}
func TestWriteEnvelopeClosesResponseBodyAfterDrainFailure(t *testing.T) {
drainErr := errors.New("response read failed")
body := &trackingHTTPBody{reader: errorHTTPReader{err: drainErr}}
client := newHTTPWriteTestClient(&http.Response{StatusCode: http.StatusAccepted, Body: body})
err := client.WriteEnvelope(&sliverpb.Envelope{Type: sliverpb.MsgPing})
if !errors.Is(err, drainErr) {
t.Fatalf("WriteEnvelope() error = %v, want %v", err, drainErr)
}
if body.closeCalls != 1 {
t.Fatalf("HTTP response body closed %d times after drain failure, want 1", body.closeCalls)
}
}
func TestWriteEnvelopeClosesResponseBodyWhenDriverReturnsResponseAndError(t *testing.T) {
driverErr := errors.New("HTTP driver failed")
body := &trackingHTTPBody{reader: bytes.NewBufferString("error-response")}
client := newHTTPWriteTestClient(&http.Response{StatusCode: http.StatusBadGateway, Body: body})
client.driver.(*staticHTTPDriver).err = driverErr
err := client.WriteEnvelope(&sliverpb.Envelope{Type: sliverpb.MsgPing})
if !errors.Is(err, driverErr) {
t.Fatalf("WriteEnvelope() error = %v, want %v", err, driverErr)
}
if !body.readToEOF {
t.Fatal("WriteEnvelope did not drain the HTTP error response body")
}
if body.closeCalls != 1 {
t.Fatalf("HTTP error response body closed %d times, want 1", body.closeCalls)
}
}
type errorHTTPReader struct {
err error
}
func (reader errorHTTPReader) Read([]byte) (int, error) {
return 0, reader.err
}
+120 -27
View File
@@ -69,9 +69,8 @@ import (
// {{if not .Config.IncludeNamePipe}}
"net/url"
"sync"
pb "github.com/bishopfox/sliver/protobuf/sliverpb"
// {{end}}
pb "github.com/bishopfox/sliver/protobuf/sliverpb"
"io"
"time"
@@ -85,6 +84,40 @@ var (
type Start func() error
type Stop func() error
// nextSendEnvelope waits for the next usable transport envelope or the owning
// connection to end. Producer channels intentionally remain open across
// cleanup, so transport senders must use the connection lifecycle instead of
// ranging over Send.
func nextSendEnvelope(connection *Connection, send <-chan *pb.Envelope) (*pb.Envelope, bool) {
if connection == nil {
return nil, false
}
for {
select {
case <-connection.Done():
return nil, false
default:
}
select {
case envelope, ok := <-send:
if !ok {
return nil, false
}
select {
case <-connection.Done():
return nil, false
default:
}
if envelope == nil {
continue
}
return envelope, true
case <-connection.Done():
return nil, false
}
}
}
// StartConnectionLoop - Starts the main connection loop
func StartConnectionLoop(abort <-chan struct{}, temporaryC2 ...string) <-chan *Connection {
@@ -214,7 +247,6 @@ func mtlsConnect(uri *url.URL) (*Connection, error) {
ctrl: ctrl,
tunnels: map[uint64]*Tunnel{},
mutex: &sync.RWMutex{},
once: &sync.Once{},
IsOpen: false,
uri: uri,
@@ -230,7 +262,6 @@ func mtlsConnect(uri *url.URL) (*Connection, error) {
if conn != nil {
conn.Close()
}
close(recv)
},
}
@@ -443,7 +474,6 @@ func wgConnect(uri *url.URL) (*Connection, error) {
ctrl: ctrl,
tunnels: map[uint64]*Tunnel{},
mutex: &sync.RWMutex{},
once: &sync.Once{},
uri: uri,
IsOpen: false,
cleanup: func() {
@@ -452,7 +482,6 @@ func wgConnect(uri *url.URL) (*Connection, error) {
// {{end}}
close(done)
closeTransport()
close(recv)
},
}
@@ -623,6 +652,53 @@ func wgConnect(uri *url.URL) (*Connection, error) {
// {{end}} -IncludeWG
// {{if .Config.IncludeHTTP}}
const httpEnvelopeWriteWindow = 64
type httpEnvelopeWriter interface {
WriteEnvelope(*pb.Envelope) error
}
// sendHTTPEnvelopes permits concurrent POSTs without allowing one stalled
// request to be overtaken by an unbounded number of later envelopes. A slot is
// retired in source order: request base+window cannot launch until base has
// completed. The server acknowledges a POST only after synchronous dispatch,
// so this also bounds handler-level reordering.
func sendHTTPEnvelopes(connection *Connection, send <-chan *pb.Envelope, writer httpEnvelopeWriter, window int) {
if connection == nil || writer == nil {
return
}
if window <= 0 {
window = 1
}
retirements := make([]<-chan struct{}, 0, window)
for {
if len(retirements) == window {
select {
case <-connection.Done():
return
case <-retirements[0]:
retirements = retirements[1:]
}
}
envelope, ok := nextSendEnvelope(connection, send)
if !ok {
return
}
// {{if .Config.Debug}}
log.Printf("[http] send envelope ...")
// {{end}}
retired := make(chan struct{})
retirements = append(retirements, retired)
go func(envelope *pb.Envelope, retired chan<- struct{}) {
defer close(retired)
if err := writer.WriteEnvelope(envelope); err != nil {
connection.Cleanup()
}
}(envelope, retired)
}
}
func httpConnect(uri *url.URL) (*Connection, error) {
send := make(chan *pb.Envelope)
recv := make(chan *pb.Envelope)
@@ -633,7 +709,6 @@ func httpConnect(uri *url.URL) (*Connection, error) {
ctrl: ctrl,
tunnels: map[uint64]*Tunnel{},
mutex: &sync.RWMutex{},
once: &sync.Once{},
uri: uri,
IsOpen: false,
cleanup: func() {
@@ -641,7 +716,6 @@ func httpConnect(uri *url.URL) (*Connection, error) {
log.Printf("[http] lost connection, cleanup...")
// {{end}}
ctrl <- struct{}{}
close(recv)
},
}
@@ -670,12 +744,7 @@ func httpConnect(uri *url.URL) (*Connection, error) {
go func() {
defer connection.Cleanup()
for envelope := range send {
// {{if .Config.Debug}}
log.Printf("[http] send envelope ...")
// {{end}}
go client.WriteEnvelope(envelope)
}
sendHTTPEnvelopes(connection, send, client, httpEnvelopeWriteWindow)
}()
go func() {
@@ -697,7 +766,11 @@ func httpConnect(uri *url.URL) (*Connection, error) {
case nil:
errCount = 0
if envelope != nil {
recv <- envelope
select {
case recv <- envelope:
case <-connection.Done():
return
}
}
case *url.Error:
errCount++
@@ -754,14 +827,12 @@ func dnsConnect(uri *url.URL) (*Connection, error) {
ctrl: ctrl,
tunnels: map[uint64]*Tunnel{},
mutex: &sync.RWMutex{},
once: &sync.Once{},
IsOpen: true,
cleanup: func() {
// {{if .Config.Debug}}
log.Printf("[dns] lost connection, cleanup...")
// {{end}}
ctrl <- struct{}{} // Stop polling
close(recv)
},
}
@@ -789,8 +860,14 @@ func dnsConnect(uri *url.URL) (*Connection, error) {
go func() {
defer connection.Cleanup()
for envelope := range send {
client.WriteEnvelope(envelope)
for {
envelope, ok := nextSendEnvelope(connection, send)
if !ok {
return
}
if err := client.WriteEnvelope(envelope); err != nil {
return
}
}
}()
@@ -807,7 +884,11 @@ func dnsConnect(uri *url.URL) (*Connection, error) {
case nil:
errCount = 0
if envelope != nil {
recv <- envelope
select {
case recv <- envelope:
case <-connection.Done():
return
}
}
time.Sleep(time.Millisecond * 250)
case dnsclient.ErrTimeout:
@@ -863,7 +944,6 @@ func tcpPivotConnect(uri *url.URL) (*Connection, error) {
ctrl: ctrl,
tunnels: map[uint64]*Tunnel{},
mutex: &sync.RWMutex{},
once: &sync.Once{},
IsOpen: true,
cleanup: func() {
// {{if .Config.Debug}}
@@ -871,7 +951,6 @@ func tcpPivotConnect(uri *url.URL) (*Connection, error) {
// {{end}}
pingCtrl <- struct{}{}
ctrl <- struct{}{}
close(recv)
},
}
@@ -908,9 +987,11 @@ func tcpPivotConnect(uri *url.URL) (*Connection, error) {
data, _ := proto.Marshal(&pb.PivotPing{
Nonce: uint32(time.Now().UnixNano()),
})
connection.Send <- &pb.Envelope{
if !connection.SendEnvelope(&pb.Envelope{
Type: pb.MsgPivotPeerPing,
Data: data,
}) {
return
}
// {{if .Config.Debug}}
log.Printf("[tcp pivot] server ping...")
@@ -918,9 +999,11 @@ func tcpPivotConnect(uri *url.URL) (*Connection, error) {
data, _ = proto.Marshal(&pb.PivotPing{
Nonce: uint32(time.Now().UnixNano()),
})
connection.Send <- &pb.Envelope{
if !connection.SendEnvelope(&pb.Envelope{
Type: pb.MsgPivotServerPing,
Data: data,
}) {
return
}
}
}
@@ -930,11 +1013,17 @@ func tcpPivotConnect(uri *url.URL) (*Connection, error) {
defer func() {
connection.Cleanup()
}()
for envelope := range send {
for {
envelope, ok := nextSendEnvelope(connection, send)
if !ok {
return
}
// {{if .Config.Debug}}
log.Printf("[tcp pivot] send loop envelope type %d\n", envelope.Type)
// {{end}}
pivot.WriteEnvelope(envelope)
if err := pivot.WriteEnvelope(envelope); err != nil {
return
}
}
}()
@@ -967,7 +1056,11 @@ func tcpPivotConnect(uri *url.URL) (*Connection, error) {
continue
}
if err == nil {
recv <- envelope
select {
case recv <- envelope:
case <-connection.Done():
return
}
// {{if .Config.Debug}}
log.Printf("[tcp pivot] Receive loop envelope type %d\n", envelope.Type)
// {{end}}
@@ -0,0 +1,122 @@
package transports
import (
"errors"
"sync"
"testing"
"time"
"github.com/bishopfox/sliver/protobuf/sliverpb"
)
type controlledHTTPEnvelopeWriter struct {
started chan int64
completed chan int64
firstRelease <-chan struct{}
fail error
}
func (writer *controlledHTTPEnvelopeWriter) WriteEnvelope(envelope *sliverpb.Envelope) error {
writer.started <- envelope.ID
if envelope.ID == 0 && writer.firstRelease != nil {
<-writer.firstRelease
}
writer.completed <- envelope.ID
return writer.fail
}
// The scenario synchronizes a full request window, oldest-request retirement,
// and scheduler shutdown in one ordered-transport lifecycle.
//
//nolint:gocyclo
func TestSendHTTPEnvelopesRetiresOldestBeforeAdvancingWindow(t *testing.T) {
connection := &Connection{}
t.Cleanup(connection.Cleanup)
send := make(chan *sliverpb.Envelope, httpEnvelopeWriteWindow+1)
for ordinal := 0; ordinal <= httpEnvelopeWriteWindow; ordinal++ {
send <- &sliverpb.Envelope{ID: int64(ordinal)}
}
close(send)
firstRelease := make(chan struct{})
var releaseOnce sync.Once
releaseFirst := func() { releaseOnce.Do(func() { close(firstRelease) }) }
t.Cleanup(releaseFirst)
writer := &controlledHTTPEnvelopeWriter{
started: make(chan int64, httpEnvelopeWriteWindow+1),
completed: make(chan int64, httpEnvelopeWriteWindow+1),
firstRelease: firstRelease,
}
done := make(chan struct{})
go func() {
defer close(done)
sendHTTPEnvelopes(connection, send, writer, httpEnvelopeWriteWindow)
}()
started := map[int64]struct{}{}
for len(started) < httpEnvelopeWriteWindow {
select {
case ordinal := <-writer.started:
if ordinal >= int64(httpEnvelopeWriteWindow) {
t.Fatalf("HTTP envelope ordinal %d launched before oldest retirement", ordinal)
}
started[ordinal] = struct{}{}
case <-time.After(time.Second):
t.Fatalf("only %d HTTP envelope writes started", len(started))
}
}
select {
case ordinal := <-writer.started:
t.Fatalf("HTTP envelope ordinal %d launched while ordinal 0 was still active", ordinal)
case <-time.After(50 * time.Millisecond):
}
releaseFirst()
select {
case ordinal := <-writer.started:
if ordinal != int64(httpEnvelopeWriteWindow) {
t.Fatalf("first write after retirement = %d, want %d", ordinal, httpEnvelopeWriteWindow)
}
case <-time.After(time.Second):
t.Fatalf("HTTP envelope ordinal %d did not launch after oldest retirement", httpEnvelopeWriteWindow)
}
select {
case <-done:
case <-time.After(time.Second):
t.Fatal("HTTP envelope scheduler did not stop after its source closed")
}
for completed := 0; completed <= httpEnvelopeWriteWindow; completed++ {
select {
case <-writer.completed:
case <-time.After(time.Second):
t.Fatalf("only %d HTTP envelope writes completed", completed)
}
}
}
func TestSendHTTPEnvelopesWriteFailureClosesConnection(t *testing.T) {
connection := &Connection{}
send := make(chan *sliverpb.Envelope, 1)
send <- &sliverpb.Envelope{ID: 1}
writer := &controlledHTTPEnvelopeWriter{
started: make(chan int64, 1),
completed: make(chan int64, 1),
fail: errors.New("write failed"),
}
done := make(chan struct{})
go func() {
defer close(done)
sendHTTPEnvelopes(connection, send, writer, httpEnvelopeWriteWindow)
}()
select {
case <-connection.Done():
case <-time.After(time.Second):
t.Fatal("HTTP envelope write failure did not close the connection")
}
select {
case <-done:
case <-time.After(time.Second):
t.Fatal("HTTP envelope scheduler survived connection cleanup")
}
}
+27 -11
View File
@@ -48,16 +48,13 @@ func namedPipeConnect(uri *url.URL) (*Connection, error) {
ctrl: ctrl,
tunnels: map[uint64]*Tunnel{},
mutex: &sync.RWMutex{},
once: &sync.Once{},
IsOpen: true,
cleanup: func() {
// {{if .Config.Debug}}
log.Printf("[namedpipe] lost connection, cleanup...")
// {{end}}
close(send)
ctrl <- struct{}{}
pingCtrl <- struct{}{}
close(recv)
},
}
@@ -87,9 +84,11 @@ func namedPipeConnect(uri *url.URL) (*Connection, error) {
data, _ := proto.Marshal(&pb.PivotPing{
Nonce: uint32(time.Now().UnixNano()),
})
connection.Send <- &pb.Envelope{
if !connection.SendEnvelope(&pb.Envelope{
Type: pb.MsgPivotPeerPing,
Data: data,
}) {
return
}
// {{if .Config.Debug}}
log.Printf("[namedpipe] server ping...")
@@ -97,9 +96,11 @@ func namedPipeConnect(uri *url.URL) (*Connection, error) {
data, _ = proto.Marshal(&pb.PivotPing{
Nonce: uint32(time.Now().UnixNano()),
})
connection.Send <- &pb.Envelope{
if !connection.SendEnvelope(&pb.Envelope{
Type: pb.MsgPivotServerPing,
Data: data,
}) {
return
}
}
}
@@ -109,11 +110,22 @@ func namedPipeConnect(uri *url.URL) (*Connection, error) {
defer func() {
connection.Cleanup()
}()
for envelope := range send {
// {{if .Config.Debug}}
log.Printf("[namedpipe] send loop envelope type %d\n", envelope.Type)
// {{end}}
pivot.WriteEnvelope(envelope)
for {
select {
case envelope := <-send:
if envelope == nil {
continue
}
// {{if .Config.Debug}}
log.Printf("[namedpipe] send loop envelope type %d\n", envelope.Type)
// {{end}}
if err := pivot.WriteEnvelope(envelope); err != nil {
connection.Cleanup()
return
}
case <-connection.Done():
return
}
}
}()
@@ -143,7 +155,11 @@ func namedPipeConnect(uri *url.URL) (*Connection, error) {
continue
}
if err == nil {
recv <- envelope
select {
case recv <- envelope:
case <-connection.Done():
return
}
// {{if .Config.Debug}}
log.Printf("[namedpipe] Receive loop envelope type %d\n", envelope.Type)
// {{end}}
+286 -13
View File
@@ -1,8 +1,31 @@
package transports
import (
"errors"
"fmt"
"io"
"sync"
"sync/atomic"
"time"
"github.com/bishopfox/sliver/protobuf/sliverpb"
)
// ErrTunnelClosed and the related errors report rejected tunnel operations.
var (
ErrTunnelClosed = errors.New("tunnel is closed")
ErrTunnelTerminalSequence = errors.New("tunnel terminal sequence is invalid")
ErrTunnelFrameTooLarge = errors.New("tunnel frame exceeds the size limit")
ErrTunnelSequenceWindow = errors.New("tunnel sequence exceeds the pending window")
ErrTunnelPendingBytes = errors.New("tunnel pending data exceeds the byte limit")
)
const (
// MTLS and WireGuard accept up to 128 concurrent streams. The receive window
// must admit that much legitimate handler reordering while remaining bounded.
maxTunnelPendingFrames = 128
maxTunnelFrameBytes = sliverpb.MaxTunnelFrameBytes
maxTunnelPendingBytes = maxTunnelPendingFrames * maxTunnelFrameBytes
)
// Tunnel - Duplex byte read/write
@@ -15,20 +38,218 @@ type Tunnel struct {
Writer io.WriteCloser
writeSequence uint64
outboundMutex sync.Mutex
outboundClose bool
mutex *sync.RWMutex
closeOnce sync.Once
mutex *sync.RWMutex
inboundMutex sync.Mutex
pendingInbound map[uint64][]byte
pendingBytes int
closeOnce sync.Once
done chan struct{}
peerCloseSet atomic.Bool
peerTeardown atomic.Bool
peerCloseSequence uint64
peerCloseTimerOnce sync.Once
peerCloseNotifier func(uint64) error
reverse bool
}
// NewReverseTunnel marks a generation as reverse-port-forward traffic. Reverse
// relays use reliable transports and bounded reordering, not the legacy generic
// tunnel resend/cache protocol.
func NewReverseTunnel(id uint64, writer io.WriteCloser, readers ...io.ReadCloser) *Tunnel {
tunnel := NewTunnel(id, writer, readers...)
tunnel.reverse = true
return tunnel
}
// IsReverse reports whether this tunnel carries reverse-port-forward traffic.
func (c *Tunnel) IsReverse() bool {
return c != nil && c.reverse
}
func NewTunnel(id uint64, writer io.WriteCloser, readers ...io.ReadCloser) *Tunnel {
return &Tunnel{
ID: id,
Readers: readers,
Writer: writer,
mutex: &sync.RWMutex{},
ID: id,
Readers: readers,
Writer: writer,
mutex: &sync.RWMutex{},
pendingInbound: map[uint64][]byte{},
done: make(chan struct{}),
}
}
// Done is closed when this exact tunnel generation is detached.
func (c *Tunnel) Done() <-chan struct{} {
return c.done
}
// SetPeerCloseNotifier installs the bounded transport callback used when this
// tunnel generation closes locally. It must be set before publication.
func (c *Tunnel) setPeerCloseNotifier(notifier func(uint64) error) {
c.peerCloseNotifier = notifier
}
// QueueOutbound serializes data frames with the terminal close notification.
// A sequence is advanced only after the transport accepted the frame.
func (c *Tunnel) queueOutbound(send func(uint64) error) error {
if send == nil {
return ErrTunnelClosed
}
c.outboundMutex.Lock()
defer c.outboundMutex.Unlock()
if c.outboundClose {
return ErrTunnelClosed
}
select {
case <-c.done:
return ErrTunnelClosed
default:
}
sequence := c.WriteSequence()
if err := send(sequence); err != nil {
return err
}
c.incWriteSequence()
return nil
}
// queueControl serializes a non-data control frame (currently resend) with
// data and terminal close, without consuming a data sequence number.
func (c *Tunnel) queueControl(send func(uint64, uint64) error) error {
if send == nil {
return ErrTunnelClosed
}
c.outboundMutex.Lock()
defer c.outboundMutex.Unlock()
if c.outboundClose {
return ErrTunnelClosed
}
select {
case <-c.done:
return ErrTunnelClosed
default:
}
return send(c.WriteSequence(), c.ReadSequence())
}
// CloseLocal emits one exclusive terminal sequence after every accepted data
// frame, then closes this generation. If the peer already initiated teardown,
// it closes without echoing another terminal frame.
func (c *Tunnel) closeLocal() (bool, error) {
c.outboundMutex.Lock()
if c.outboundClose {
c.outboundMutex.Unlock()
return false, nil
}
select {
case <-c.done:
c.outboundMutex.Unlock()
return false, nil
default:
}
c.outboundClose = true
sequence := c.WriteSequence()
notifier := c.peerCloseNotifier
var err error
if notifier != nil && !c.PeerTeardownPending() {
err = notifier(sequence)
}
c.outboundMutex.Unlock()
c.Close()
return true, err
}
// CloseRemote closes a generation without echoing a close to its peer.
func (c *Tunnel) closeRemote() {
// Publish peer ownership before Close unblocks a local terminal notifier.
// A simultaneous local close must treat ErrTunnelClosed from that notifier
// as normal peer teardown, not as a transport delivery failure.
c.peerTeardown.Store(true)
c.Close()
c.outboundMutex.Lock()
c.outboundClose = true
c.outboundMutex.Unlock()
}
// MarkPeerClose records an exclusive final inbound sequence. A zero sequence
// received after data preserves legacy close semantics and closes immediately.
func (c *Tunnel) MarkPeerClose(sequence uint64) (bool, error) {
c.inboundMutex.Lock()
defer c.inboundMutex.Unlock()
select {
case <-c.done:
return true, ErrTunnelClosed
default:
}
expected := c.ReadSequence()
if sequence == 0 {
if c.peerCloseSet.Load() && c.peerCloseSequence != expected {
return false, ErrTunnelTerminalSequence
}
c.peerCloseSequence = expected
c.peerTeardown.Store(true)
c.peerCloseSet.Store(true)
return true, nil
}
if sequence < expected || sequence-expected > maxTunnelPendingFrames {
return false, ErrTunnelTerminalSequence
}
if c.peerCloseSet.Load() && c.peerCloseSequence != sequence {
return false, ErrTunnelTerminalSequence
}
for pendingSequence := range c.pendingInbound {
if pendingSequence >= sequence {
return false, ErrTunnelTerminalSequence
}
}
c.peerCloseSequence = sequence
c.peerTeardown.Store(true)
c.peerCloseSet.Store(true)
return expected >= sequence, nil
}
// PeerClosePending reports whether the peer supplied a terminal sequence.
func (c *Tunnel) PeerClosePending() bool {
return c.peerCloseSet.Load()
}
// PeerTeardownPending reports that the peer owns teardown, whether it supplied
// a sequenced terminal or the remote-close actor is already closing resources.
// It is deliberately separate from PeerClosePending: only a validated terminal
// may constrain inbound sequence acceptance.
func (c *Tunnel) PeerTeardownPending() bool {
return c.peerTeardown.Load() || c.peerCloseSet.Load()
}
// PeerCloseReady reports whether all frames preceding the peer terminal arrived.
func (c *Tunnel) PeerCloseReady() bool {
c.inboundMutex.Lock()
defer c.inboundMutex.Unlock()
return c.peerCloseSet.Load() && c.ReadSequence() >= c.peerCloseSequence
}
// StartPeerCloseDeadline runs expired once if a terminal frame remains
// incomplete. Closing the tunnel cancels the deadline.
func (c *Tunnel) StartPeerCloseDeadline(timeout time.Duration, expired func()) {
if expired == nil {
return
}
c.peerCloseTimerOnce.Do(func() {
go func() {
timer := time.NewTimer(timeout)
defer timer.Stop()
select {
case <-c.Done():
case <-timer.C:
expired()
}
}()
})
}
func (c *Tunnel) ReadSequence() uint64 {
c.mutex.RLock()
defer c.mutex.RUnlock()
@@ -43,16 +264,67 @@ func (c *Tunnel) WriteSequence() uint64 {
return c.writeSequence
}
// NextWriteSequence atomically reserves the next outbound sequence number.
// A shell can have independent stdout and stderr readers writing concurrently,
// so reading and incrementing the counter must be a single operation.
func (c *Tunnel) NextWriteSequence() uint64 {
// ProcessInbound serializes one generation's complete accept/cache/drain
// transaction. Pending frames live on the generation rather than in an ID-only
// global cache, so stale handlers cannot corrupt a reused numeric tunnel ID.
func (c *Tunnel) ProcessInbound(sequence uint64, data []byte, write func([]byte) error) (int, error) {
if write == nil {
return 0, errors.New("tunnel write function is nil")
}
if len(data) > maxTunnelFrameBytes {
return 0, fmt.Errorf("%w: got %d bytes, limit %d", ErrTunnelFrameTooLarge, len(data), maxTunnelFrameBytes)
}
c.inboundMutex.Lock()
defer c.inboundMutex.Unlock()
select {
case <-c.done:
return len(c.pendingInbound), ErrTunnelClosed
default:
}
if c.pendingInbound == nil {
c.pendingInbound = map[uint64][]byte{}
}
expected := c.ReadSequence()
if c.peerCloseSet.Load() && sequence >= c.peerCloseSequence {
return len(c.pendingInbound), ErrTunnelTerminalSequence
}
if sequence < expected {
return len(c.pendingInbound), nil
}
if sequence-expected >= maxTunnelPendingFrames {
return len(c.pendingInbound), fmt.Errorf("%w: got %d, expected %d", ErrTunnelSequenceWindow, sequence, expected)
}
if _, duplicate := c.pendingInbound[sequence]; !duplicate {
if c.pendingBytes+len(data) > maxTunnelPendingBytes {
return len(c.pendingInbound), ErrTunnelPendingBytes
}
payload := append([]byte(nil), data...)
c.pendingInbound[sequence] = payload
c.pendingBytes += len(payload)
}
for {
payload, ok := c.pendingInbound[expected]
if !ok {
break
}
if err := write(payload); err != nil {
return len(c.pendingInbound), err
}
delete(c.pendingInbound, expected)
c.pendingBytes -= len(payload)
c.IncReadSequence()
expected++
}
return len(c.pendingInbound), nil
}
func (c *Tunnel) incWriteSequence() {
c.mutex.Lock()
defer c.mutex.Unlock()
sequence := c.writeSequence
c.writeSequence++
return sequence
}
// IncReadSequence advances the expected inbound sequence number.
@@ -66,6 +338,7 @@ func (c *Tunnel) IncReadSequence() {
// Close - close tunnel reader and writer
func (c *Tunnel) Close() {
c.closeOnce.Do(func() {
close(c.done)
for _, rc := range c.Readers {
if rc != nil {
_ = rc.Close()
+137 -2
View File
@@ -1,13 +1,16 @@
package transports
import (
"bytes"
"errors"
"io"
"sort"
"sync"
"sync/atomic"
"testing"
)
func TestTunnelNextWriteSequenceConcurrent(t *testing.T) {
func TestTunnelQueueOutboundSequencesConcurrentWriters(t *testing.T) {
const writers = 128
tunnel := NewTunnel(1, nopWriteCloser{Writer: io.Discard})
@@ -23,7 +26,12 @@ func TestTunnelNextWriteSequenceConcurrent(t *testing.T) {
defer done.Done()
ready.Done()
<-start
sequences[index] = tunnel.NextWriteSequence()
if err := tunnel.queueOutbound(func(sequence uint64) error {
sequences[index] = sequence
return nil
}); err != nil {
t.Errorf("queueOutbound(): %v", err)
}
}(index)
}
@@ -39,6 +47,133 @@ func TestTunnelNextWriteSequenceConcurrent(t *testing.T) {
}
}
func TestTunnelPeerCloseWaitsForExclusiveTerminalSequence(t *testing.T) {
tunnel := NewTunnel(1, nopWriteCloser{Writer: io.Discard})
ready, err := tunnel.MarkPeerClose(2)
if err != nil || ready {
t.Fatalf("MarkPeerClose(2) = (%v, %v), want (false, nil)", ready, err)
}
if _, err := tunnel.ProcessInbound(1, []byte("second"), func([]byte) error { return nil }); err != nil {
t.Fatalf("sequence before terminal rejected: %v", err)
}
if tunnel.PeerCloseReady() {
t.Fatal("terminal close became ready before the final frame")
}
var output bytes.Buffer
if _, err := tunnel.ProcessInbound(0, []byte("first"), func(data []byte) error {
_, writeErr := output.Write(data)
return writeErr
}); err != nil {
t.Fatalf("process final frames: %v", err)
}
if !tunnel.PeerCloseReady() {
t.Fatal("terminal close did not become ready after all frames")
}
if _, err := tunnel.ProcessInbound(2, []byte("late"), func([]byte) error { return nil }); !errors.Is(err, ErrTunnelTerminalSequence) {
t.Fatalf("sequence at terminal error = %v, want %v", err, ErrTunnelTerminalSequence)
}
}
func TestTunnelAcceptsFullTransportReorderWindowBeforeTerminal(t *testing.T) {
tunnel := NewTunnel(2, nopWriteCloser{Writer: io.Discard})
ready, err := tunnel.MarkPeerClose(maxTunnelPendingFrames)
if err != nil || ready {
t.Fatalf("MarkPeerClose(window) = (%v, %v), want (false, nil)", ready, err)
}
var output bytes.Buffer
for sequence := maxTunnelPendingFrames; sequence > 0; sequence-- {
value := byte(sequence - 1)
if _, err := tunnel.ProcessInbound(uint64(sequence-1), []byte{value}, func(data []byte) error {
_, writeErr := output.Write(data)
return writeErr
}); err != nil {
t.Fatalf("ProcessInbound(%d): %v", sequence-1, err)
}
}
if !tunnel.PeerCloseReady() {
t.Fatal("terminal did not become ready after a full reverse-order window")
}
if output.Len() != maxTunnelPendingFrames {
t.Fatalf("output length = %d, want %d", output.Len(), maxTunnelPendingFrames)
}
for index, value := range output.Bytes() {
if value != byte(index) {
t.Fatalf("output[%d] = %d, want %d", index, value, byte(index))
}
}
}
func TestTunnelLegacyZeroCloseIsImmediateWithPendingFrames(t *testing.T) {
tunnel := NewTunnel(4, nopWriteCloser{Writer: io.Discard})
if pending, err := tunnel.ProcessInbound(1, []byte("pending"), func([]byte) error { return nil }); err != nil || pending != 1 {
t.Fatalf("ProcessInbound(1) = (%d, %v), want (1, nil)", pending, err)
}
ready, err := tunnel.MarkPeerClose(0)
if err != nil || !ready {
t.Fatalf("MarkPeerClose(0) = (%v, %v), want (true, nil)", ready, err)
}
}
func TestTunnelRemoteTeardownMarkerDoesNotInstallTerminal(t *testing.T) {
tunnel := NewTunnel(5, nopWriteCloser{Writer: io.Discard})
// Reproduce the closeRemote linearization window after peer ownership is
// published but before Done is closed.
tunnel.peerTeardown.Store(true)
var output bytes.Buffer
pending, err := tunnel.ProcessInbound(0, []byte("in-flight"), func(payload []byte) error {
_, writeErr := output.Write(payload)
return writeErr
})
if err != nil {
t.Fatalf("ProcessInbound() during remote teardown marker: %v", err)
}
if pending != 0 || output.String() != "in-flight" {
t.Fatalf("ProcessInbound() = pending %d, output %q", pending, output.String())
}
if !tunnel.PeerTeardownPending() {
t.Fatal("remote teardown ownership was not published")
}
if tunnel.PeerClosePending() {
t.Fatal("remote teardown marker installed a terminal sequence")
}
tunnel.Close()
}
func TestTunnelCloseLocalSequencesAndNotifiesExactlyOnce(t *testing.T) {
tunnel := NewTunnel(3, nopWriteCloser{Writer: io.Discard})
var notifications atomic.Int32
var terminal atomic.Uint64
tunnel.setPeerCloseNotifier(func(sequence uint64) error {
notifications.Add(1)
terminal.Store(sequence)
return nil
})
if err := tunnel.queueOutbound(func(sequence uint64) error {
if sequence != 0 {
t.Fatalf("first sequence = %d, want 0", sequence)
}
return nil
}); err != nil {
t.Fatalf("queueOutbound(): %v", err)
}
var closes sync.WaitGroup
closes.Add(16)
for range 16 {
go func() {
defer closes.Done()
_, _ = tunnel.closeLocal()
}()
}
closes.Wait()
if got := notifications.Load(); got != 1 {
t.Fatalf("notifications = %d, want 1", got)
}
if got := terminal.Load(); got != 1 {
t.Fatalf("terminal sequence = %d, want 1", got)
}
}
type nopWriteCloser struct {
io.Writer
}
+17
View File
@@ -5,3 +5,20 @@ package sliverpb
// CapabilityBOFV1 indicates that an implant can execute BOFs using the
// built-in BOF v1 request fields on CallExtensionReq.
const CapabilityBOFV1 uint64 = uint64(ImplantCapability_IMPLANT_CAPABILITY_BOF_V1)
// CapabilityTunnelTerminalV1 indicates that an implant waits for every data
// frame below a TunnelClose.Sequence exclusive terminal before detaching the
// tunnel generation.
const CapabilityTunnelTerminalV1 uint64 = uint64(ImplantCapability_IMPLANT_CAPABILITY_TUNNEL_TERMINAL_V1)
// CapabilitySocksFlowControlV1 indicates that a SOCKS endpoint supports the
// cumulative acknowledgements and fixed send window carried by SocksData.
const CapabilitySocksFlowControlV1 uint64 = uint64(ImplantCapability_IMPLANT_CAPABILITY_SOCKS_FLOW_CONTROL_V1)
// SocksFlowControlWindowV1 is the maximum number of unacknowledged data frames
// allowed in either direction of a V1 flow-controlled SOCKS tunnel.
const SocksFlowControlWindowV1 = 64
// SocksFlowControlAckBatchV1 controls how frequently receivers emit cumulative
// acknowledgements while a stream remains active.
const SocksFlowControlAckBatchV1 = 16
+4
View File
@@ -362,6 +362,10 @@ const (
// Constants to replace enums
const (
// MaxTunnelFrameBytes is the shared per-frame payload limit for generic and
// reverse tunnels on both the server and generated implant build paths.
MaxTunnelFrameBytes = 64 * 1024
// Port forward protocols
PortFwdProtoTCP = 1
PortFwdProtoUDP = 2
+125 -52
View File
@@ -26,8 +26,10 @@ const (
type ImplantCapability int32
const (
ImplantCapability_IMPLANT_CAPABILITY_NONE ImplantCapability = 0
ImplantCapability_IMPLANT_CAPABILITY_BOF_V1 ImplantCapability = 1
ImplantCapability_IMPLANT_CAPABILITY_NONE ImplantCapability = 0
ImplantCapability_IMPLANT_CAPABILITY_BOF_V1 ImplantCapability = 1
ImplantCapability_IMPLANT_CAPABILITY_TUNNEL_TERMINAL_V1 ImplantCapability = 2
ImplantCapability_IMPLANT_CAPABILITY_SOCKS_FLOW_CONTROL_V1 ImplantCapability = 4
)
// Enum value maps for ImplantCapability.
@@ -35,10 +37,14 @@ var (
ImplantCapability_name = map[int32]string{
0: "IMPLANT_CAPABILITY_NONE",
1: "IMPLANT_CAPABILITY_BOF_V1",
2: "IMPLANT_CAPABILITY_TUNNEL_TERMINAL_V1",
4: "IMPLANT_CAPABILITY_SOCKS_FLOW_CONTROL_V1",
}
ImplantCapability_value = map[string]int32{
"IMPLANT_CAPABILITY_NONE": 0,
"IMPLANT_CAPABILITY_BOF_V1": 1,
"IMPLANT_CAPABILITY_NONE": 0,
"IMPLANT_CAPABILITY_BOF_V1": 1,
"IMPLANT_CAPABILITY_TUNNEL_TERMINAL_V1": 2,
"IMPLANT_CAPABILITY_SOCKS_FLOW_CONTROL_V1": 4,
}
)
@@ -7773,6 +7779,7 @@ type Socks struct {
state protoimpl.MessageState `protogen:"open.v1"`
TunnelID uint64 `protobuf:"varint,8,opt,name=TunnelID,proto3" json:"TunnelID,omitempty"`
SessionID string `protobuf:"bytes,9,opt,name=SessionID,proto3" json:"SessionID,omitempty"`
Capabilities uint64 `protobuf:"varint,10,opt,name=Capabilities,proto3" json:"Capabilities,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -7821,15 +7828,27 @@ func (x *Socks) GetSessionID() string {
return ""
}
func (x *Socks) GetCapabilities() uint64 {
if x != nil {
return x.Capabilities
}
return 0
}
type SocksData struct {
state protoimpl.MessageState `protogen:"open.v1"`
Data []byte `protobuf:"bytes,1,opt,name=Data,proto3" json:"Data,omitempty"`
CloseConn bool `protobuf:"varint,2,opt,name=CloseConn,proto3" json:"CloseConn,omitempty"`
Username string `protobuf:"bytes,3,opt,name=Username,proto3" json:"Username,omitempty"`
Password string `protobuf:"bytes,4,opt,name=Password,proto3" json:"Password,omitempty"`
Sequence uint64 `protobuf:"varint,5,opt,name=Sequence,proto3" json:"Sequence,omitempty"`
TunnelID uint64 `protobuf:"varint,8,opt,name=TunnelID,proto3" json:"TunnelID,omitempty"`
Request *commonpb.Request `protobuf:"bytes,9,opt,name=Request,proto3" json:"Request,omitempty"`
state protoimpl.MessageState `protogen:"open.v1"`
Data []byte `protobuf:"bytes,1,opt,name=Data,proto3" json:"Data,omitempty"`
CloseConn bool `protobuf:"varint,2,opt,name=CloseConn,proto3" json:"CloseConn,omitempty"`
Username string `protobuf:"bytes,3,opt,name=Username,proto3" json:"Username,omitempty"`
Password string `protobuf:"bytes,4,opt,name=Password,proto3" json:"Password,omitempty"`
Sequence uint64 `protobuf:"varint,5,opt,name=Sequence,proto3" json:"Sequence,omitempty"`
// Ack is the next sequence after the last frame fully consumed by the
// receiving SOCKS endpoint. It is meaningful only when flow control was
// negotiated for this tunnel.
Ack uint64 `protobuf:"varint,6,opt,name=Ack,proto3" json:"Ack,omitempty"`
Capabilities uint64 `protobuf:"varint,7,opt,name=Capabilities,proto3" json:"Capabilities,omitempty"`
TunnelID uint64 `protobuf:"varint,8,opt,name=TunnelID,proto3" json:"TunnelID,omitempty"`
Request *commonpb.Request `protobuf:"bytes,9,opt,name=Request,proto3" json:"Request,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -7899,6 +7918,20 @@ func (x *SocksData) GetSequence() uint64 {
return 0
}
func (x *SocksData) GetAck() uint64 {
if x != nil {
return x.Ack
}
return 0
}
func (x *SocksData) GetCapabilities() uint64 {
if x != nil {
return x.Capabilities
}
return 0
}
func (x *SocksData) GetTunnelID() uint64 {
if x != nil {
return x.TunnelID
@@ -10264,15 +10297,16 @@ func (x *RportFwdStopListenerReq) GetRequest() *commonpb.Request {
}
type RportFwdStartListenerReq struct {
state protoimpl.MessageState `protogen:"open.v1"`
BindAddress string `protobuf:"bytes,1,opt,name=BindAddress,proto3" json:"BindAddress,omitempty"`
BindPort uint32 `protobuf:"varint,2,opt,name=BindPort,proto3" json:"BindPort,omitempty"`
ForwardPort uint32 `protobuf:"varint,3,opt,name=ForwardPort,proto3" json:"ForwardPort,omitempty"`
ForwardAddress string `protobuf:"bytes,4,opt,name=ForwardAddress,proto3" json:"ForwardAddress,omitempty"`
KeepAlive int32 `protobuf:"varint,5,opt,name=KeepAlive,proto3" json:"KeepAlive,omitempty"`
Request *commonpb.Request `protobuf:"bytes,9,opt,name=Request,proto3" json:"Request,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
BindAddress string `protobuf:"bytes,1,opt,name=BindAddress,proto3" json:"BindAddress,omitempty"`
BindPort uint32 `protobuf:"varint,2,opt,name=BindPort,proto3" json:"BindPort,omitempty"`
ForwardPort uint32 `protobuf:"varint,3,opt,name=ForwardPort,proto3" json:"ForwardPort,omitempty"`
ForwardAddress string `protobuf:"bytes,4,opt,name=ForwardAddress,proto3" json:"ForwardAddress,omitempty"`
KeepAlive int32 `protobuf:"varint,5,opt,name=KeepAlive,proto3" json:"KeepAlive,omitempty"`
AuthorizationID string `protobuf:"bytes,6,opt,name=AuthorizationID,proto3" json:"AuthorizationID,omitempty"`
Request *commonpb.Request `protobuf:"bytes,9,opt,name=Request,proto3" json:"Request,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *RportFwdStartListenerReq) Reset() {
@@ -10340,6 +10374,13 @@ func (x *RportFwdStartListenerReq) GetKeepAlive() int32 {
return 0
}
func (x *RportFwdStartListenerReq) GetAuthorizationID() string {
if x != nil {
return x.AuthorizationID
}
return ""
}
func (x *RportFwdStartListenerReq) GetRequest() *commonpb.Request {
if x != nil {
return x.Request
@@ -10348,15 +10389,16 @@ func (x *RportFwdStartListenerReq) GetRequest() *commonpb.Request {
}
type RportFwdListener struct {
state protoimpl.MessageState `protogen:"open.v1"`
ID uint32 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"`
BindAddress string `protobuf:"bytes,2,opt,name=BindAddress,proto3" json:"BindAddress,omitempty"`
BindPort uint32 `protobuf:"varint,3,opt,name=BindPort,proto3" json:"BindPort,omitempty"`
ForwardAddress string `protobuf:"bytes,4,opt,name=ForwardAddress,proto3" json:"ForwardAddress,omitempty"`
ForwardPort uint32 `protobuf:"varint,5,opt,name=ForwardPort,proto3" json:"ForwardPort,omitempty"`
Response *commonpb.Response `protobuf:"bytes,9,opt,name=Response,proto3" json:"Response,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
ID uint32 `protobuf:"varint,1,opt,name=ID,proto3" json:"ID,omitempty"`
BindAddress string `protobuf:"bytes,2,opt,name=BindAddress,proto3" json:"BindAddress,omitempty"`
BindPort uint32 `protobuf:"varint,3,opt,name=BindPort,proto3" json:"BindPort,omitempty"`
ForwardAddress string `protobuf:"bytes,4,opt,name=ForwardAddress,proto3" json:"ForwardAddress,omitempty"`
ForwardPort uint32 `protobuf:"varint,5,opt,name=ForwardPort,proto3" json:"ForwardPort,omitempty"`
AuthorizationID string `protobuf:"bytes,6,opt,name=AuthorizationID,proto3" json:"AuthorizationID,omitempty"`
Response *commonpb.Response `protobuf:"bytes,9,opt,name=Response,proto3" json:"Response,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *RportFwdListener) Reset() {
@@ -10424,6 +10466,13 @@ func (x *RportFwdListener) GetForwardPort() uint32 {
return 0
}
func (x *RportFwdListener) GetAuthorizationID() string {
if x != nil {
return x.AuthorizationID
}
return ""
}
func (x *RportFwdListener) GetResponse() *commonpb.Response {
if x != nil {
return x.Response
@@ -10528,14 +10577,20 @@ func (x *RportFwdListenersReq) GetRequest() *commonpb.Request {
}
type RPortfwd struct {
state protoimpl.MessageState `protogen:"open.v1"`
Port uint32 `protobuf:"varint,1,opt,name=Port,proto3" json:"Port,omitempty"`
Protocol int32 `protobuf:"varint,2,opt,name=Protocol,proto3" json:"Protocol,omitempty"`
Host string `protobuf:"bytes,3,opt,name=Host,proto3" json:"Host,omitempty"`
TunnelID uint64 `protobuf:"varint,8,opt,name=TunnelID,proto3" json:"TunnelID,omitempty"` // Bind to this tunnel
Response *commonpb.Response `protobuf:"bytes,9,opt,name=Response,proto3" json:"Response,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
// Legacy wire metadata only. The teamserver must resolve AuthorizationID to
// a server-owned destination and must never dial Host or Port directly.
//
// Deprecated: Marked as deprecated in sliverpb/sliver.proto.
Port uint32 `protobuf:"varint,1,opt,name=Port,proto3" json:"Port,omitempty"`
Protocol int32 `protobuf:"varint,2,opt,name=Protocol,proto3" json:"Protocol,omitempty"`
// Deprecated: Marked as deprecated in sliverpb/sliver.proto.
Host string `protobuf:"bytes,3,opt,name=Host,proto3" json:"Host,omitempty"`
AuthorizationID string `protobuf:"bytes,4,opt,name=AuthorizationID,proto3" json:"AuthorizationID,omitempty"`
TunnelID uint64 `protobuf:"varint,8,opt,name=TunnelID,proto3" json:"TunnelID,omitempty"` // Bind to this tunnel
Response *commonpb.Response `protobuf:"bytes,9,opt,name=Response,proto3" json:"Response,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *RPortfwd) Reset() {
@@ -10568,6 +10623,7 @@ func (*RPortfwd) Descriptor() ([]byte, []int) {
return file_sliverpb_sliver_proto_rawDescGZIP(), []int{159}
}
// Deprecated: Marked as deprecated in sliverpb/sliver.proto.
func (x *RPortfwd) GetPort() uint32 {
if x != nil {
return x.Port
@@ -10582,6 +10638,7 @@ func (x *RPortfwd) GetProtocol() int32 {
return 0
}
// Deprecated: Marked as deprecated in sliverpb/sliver.proto.
func (x *RPortfwd) GetHost() string {
if x != nil {
return x.Host
@@ -10589,6 +10646,13 @@ func (x *RPortfwd) GetHost() string {
return ""
}
func (x *RPortfwd) GetAuthorizationID() string {
if x != nil {
return x.AuthorizationID
}
return ""
}
func (x *RPortfwd) GetTunnelID() uint64 {
if x != nil {
return x.TunnelID
@@ -12783,16 +12847,20 @@ const file_sliverpb_sliver_proto_rawDesc = "" +
"\bProtocol\x18\x02 \x01(\x05R\bProtocol\x12\x12\n" +
"\x04Host\x18\x03 \x01(\tR\x04Host\x12\x1e\n" +
"\bTunnelID\x18\b \x01(\x04B\x020\x01R\bTunnelID\x12.\n" +
"\bResponse\x18\t \x01(\v2\x12.commonpb.ResponseR\bResponse\"E\n" +
"\bResponse\x18\t \x01(\v2\x12.commonpb.ResponseR\bResponse\"i\n" +
"\x05Socks\x12\x1e\n" +
"\bTunnelID\x18\b \x01(\x04B\x020\x01R\bTunnelID\x12\x1c\n" +
"\tSessionID\x18\t \x01(\tR\tSessionID\"\xde\x01\n" +
"\tSessionID\x18\t \x01(\tR\tSessionID\x12\"\n" +
"\fCapabilities\x18\n" +
" \x01(\x04R\fCapabilities\"\x94\x02\n" +
"\tSocksData\x12\x12\n" +
"\x04Data\x18\x01 \x01(\fR\x04Data\x12\x1c\n" +
"\tCloseConn\x18\x02 \x01(\bR\tCloseConn\x12\x1a\n" +
"\bUsername\x18\x03 \x01(\tR\bUsername\x12\x1a\n" +
"\bPassword\x18\x04 \x01(\tR\bPassword\x12\x1a\n" +
"\bSequence\x18\x05 \x01(\x04R\bSequence\x12\x1e\n" +
"\bSequence\x18\x05 \x01(\x04R\bSequence\x12\x10\n" +
"\x03Ack\x18\x06 \x01(\x04R\x03Ack\x12\"\n" +
"\fCapabilities\x18\a \x01(\x04R\fCapabilities\x12\x1e\n" +
"\bTunnelID\x18\b \x01(\x04B\x020\x01R\bTunnelID\x12+\n" +
"\aRequest\x18\t \x01(\v2\x11.commonpb.RequestR\aRequest\"\xa9\x01\n" +
"\x15PivotStartListenerReq\x12'\n" +
@@ -12960,30 +13028,33 @@ const file_sliverpb_sliver_proto_rawDesc = "" +
"\bResponse\x18\t \x01(\v2\x12.commonpb.ResponseR\bResponse\"V\n" +
"\x17RportFwdStopListenerReq\x12\x0e\n" +
"\x02ID\x18\x01 \x01(\rR\x02ID\x12+\n" +
"\aRequest\x18\t \x01(\v2\x11.commonpb.RequestR\aRequest\"\xed\x01\n" +
"\aRequest\x18\t \x01(\v2\x11.commonpb.RequestR\aRequest\"\x97\x02\n" +
"\x18RportFwdStartListenerReq\x12 \n" +
"\vBindAddress\x18\x01 \x01(\tR\vBindAddress\x12\x1a\n" +
"\bBindPort\x18\x02 \x01(\rR\bBindPort\x12 \n" +
"\vForwardPort\x18\x03 \x01(\rR\vForwardPort\x12&\n" +
"\x0eForwardAddress\x18\x04 \x01(\tR\x0eForwardAddress\x12\x1c\n" +
"\tKeepAlive\x18\x05 \x01(\x05R\tKeepAlive\x12+\n" +
"\aRequest\x18\t \x01(\v2\x11.commonpb.RequestR\aRequest\"\xda\x01\n" +
"\tKeepAlive\x18\x05 \x01(\x05R\tKeepAlive\x12(\n" +
"\x0fAuthorizationID\x18\x06 \x01(\tR\x0fAuthorizationID\x12+\n" +
"\aRequest\x18\t \x01(\v2\x11.commonpb.RequestR\aRequest\"\x84\x02\n" +
"\x10RportFwdListener\x12\x0e\n" +
"\x02ID\x18\x01 \x01(\rR\x02ID\x12 \n" +
"\vBindAddress\x18\x02 \x01(\tR\vBindAddress\x12\x1a\n" +
"\bBindPort\x18\x03 \x01(\rR\bBindPort\x12&\n" +
"\x0eForwardAddress\x18\x04 \x01(\tR\x0eForwardAddress\x12 \n" +
"\vForwardPort\x18\x05 \x01(\rR\vForwardPort\x12.\n" +
"\vForwardPort\x18\x05 \x01(\rR\vForwardPort\x12(\n" +
"\x0fAuthorizationID\x18\x06 \x01(\tR\x0fAuthorizationID\x12.\n" +
"\bResponse\x18\t \x01(\v2\x12.commonpb.ResponseR\bResponse\"}\n" +
"\x11RportFwdListeners\x128\n" +
"\tListeners\x18\x01 \x03(\v2\x1a.sliverpb.RportFwdListenerR\tListeners\x12.\n" +
"\bResponse\x18\t \x01(\v2\x12.commonpb.ResponseR\bResponse\"C\n" +
"\x14RportFwdListenersReq\x12+\n" +
"\aRequest\x18\t \x01(\v2\x11.commonpb.RequestR\aRequest\"\x9e\x01\n" +
"\bRPortfwd\x12\x12\n" +
"\x04Port\x18\x01 \x01(\rR\x04Port\x12\x1a\n" +
"\bProtocol\x18\x02 \x01(\x05R\bProtocol\x12\x12\n" +
"\x04Host\x18\x03 \x01(\tR\x04Host\x12\x1e\n" +
"\aRequest\x18\t \x01(\v2\x11.commonpb.RequestR\aRequest\"\xd0\x01\n" +
"\bRPortfwd\x12\x16\n" +
"\x04Port\x18\x01 \x01(\rB\x02\x18\x01R\x04Port\x12\x1a\n" +
"\bProtocol\x18\x02 \x01(\x05R\bProtocol\x12\x16\n" +
"\x04Host\x18\x03 \x01(\tB\x02\x18\x01R\x04Host\x12(\n" +
"\x0fAuthorizationID\x18\x04 \x01(\tR\x0fAuthorizationID\x12\x1e\n" +
"\bTunnelID\x18\b \x01(\x04B\x020\x01R\bTunnelID\x12.\n" +
"\bResponse\x18\t \x01(\v2\x12.commonpb.ResponseR\bResponse\"\x9e\x01\n" +
"\vRPortfwdReq\x12\x12\n" +
@@ -13089,10 +13160,12 @@ const file_sliverpb_sliver_proto_rawDesc = "" +
"\aRequest\x18\t \x01(\v2\x11.commonpb.RequestR\aRequest\"3\n" +
"\tBOFOutput\x12\x12\n" +
"\x04Type\x18\x01 \x01(\x05R\x04Type\x12\x12\n" +
"\x04Data\x18\x02 \x01(\fR\x04Data*O\n" +
"\x04Data\x18\x02 \x01(\fR\x04Data*\xa8\x01\n" +
"\x11ImplantCapability\x12\x1b\n" +
"\x17IMPLANT_CAPABILITY_NONE\x10\x00\x12\x1d\n" +
"\x19IMPLANT_CAPABILITY_BOF_V1\x10\x01*I\n" +
"\x19IMPLANT_CAPABILITY_BOF_V1\x10\x01\x12)\n" +
"%IMPLANT_CAPABILITY_TUNNEL_TERMINAL_V1\x10\x02\x12,\n" +
"(IMPLANT_CAPABILITY_SOCKS_FLOW_CONTROL_V1\x10\x04*I\n" +
"\fRegistryType\x12\v\n" +
"\aUnknown\x10\x00\x12\n" +
"\n" +
+16 -3
View File
@@ -8,6 +8,8 @@ import "commonpb/common.proto";
enum ImplantCapability {
IMPLANT_CAPABILITY_NONE = 0;
IMPLANT_CAPABILITY_BOF_V1 = 1;
IMPLANT_CAPABILITY_TUNNEL_TERMINAL_V1 = 2;
IMPLANT_CAPABILITY_SOCKS_FLOW_CONTROL_V1 = 4;
}
/*
@@ -910,6 +912,7 @@ message Socks {
uint64 TunnelID = 8 [jstype = JS_STRING];
string SessionID = 9;
uint64 Capabilities = 10;
}
message SocksData {
@@ -918,6 +921,11 @@ message SocksData {
string Username = 3;
string Password = 4;
uint64 Sequence = 5;
// Ack is the next sequence after the last frame fully consumed by the
// receiving SOCKS endpoint. It is meaningful only when flow control was
// negotiated for this tunnel.
uint64 Ack = 6;
uint64 Capabilities = 7;
uint64 TunnelID = 8 [jstype = JS_STRING];
commonpb.Request Request = 9;
@@ -1201,7 +1209,8 @@ message RportFwdStartListenerReq {
uint32 BindPort = 2;
uint32 ForwardPort = 3;
string ForwardAddress = 4;
int32 KeepAlive = 5;
int32 KeepAlive = 5;
string AuthorizationID = 6;
commonpb.Request Request = 9;
}
@@ -1212,6 +1221,7 @@ message RportFwdListener {
uint32 BindPort = 3;
string ForwardAddress =4;
uint32 ForwardPort = 5;
string AuthorizationID = 6;
commonpb.Response Response = 9;
}
@@ -1230,9 +1240,12 @@ message RportFwdListenersReq {
message RPortfwd {
uint32 Port = 1;
// Legacy wire metadata only. The teamserver must resolve AuthorizationID to
// a server-owned destination and must never dial Host or Port directly.
uint32 Port = 1 [deprecated = true];
int32 Protocol = 2;
string Host = 3;
string Host = 3 [deprecated = true];
string AuthorizationID = 4;
uint64 TunnelID = 8 [jstype = JS_STRING]; // Bind to this tunnel
commonpb.Response Response = 9;
+10
View File
@@ -276,6 +276,11 @@ func SetupGoPath(goPathSrc string, includeDNS bool) error {
setupLog.Info("Static asset not found: constants.go")
return err
}
sliverpbCapabilitiesSrc, err := protobufs.FS.ReadFile("sliverpb/capabilities.go")
if err != nil {
setupLog.Info("Static asset not found: capabilities.go")
return err
}
sliverpbGoSrc = xorPBRawBytes(sliverpbGoSrc)
sliverpbGoSrc = stripSliverpb(sliverpbGoSrc)
sliverpbDir := filepath.Join(goPathSrc, "protobuf", "sliverpb")
@@ -283,6 +288,11 @@ func SetupGoPath(goPathSrc string, includeDNS bool) error {
os.MkdirAll(sliverpbDir, 0700)
os.WriteFile(filepath.Join(sliverpbDir, "sliver.pb.go"), sliverpbGoSrc, 0600)
os.WriteFile(filepath.Join(sliverpbDir, "constants.go"), sliverpbConstSrc, 0600)
err = os.WriteFile(filepath.Join(sliverpbDir, "capabilities.go"), sliverpbCapabilitiesSrc, 0600)
if err != nil {
setupLog.Errorf("Failed to write capabilities.go: %s", err)
return err
}
// Common PB
commonpbSrc, err := protobufs.FS.ReadFile("commonpb/common.pb.go")
+29
View File
@@ -0,0 +1,29 @@
package assets
import (
"bytes"
"os"
"path/filepath"
"testing"
"github.com/bishopfox/sliver/protobuf"
)
func TestSetupGoPathCopiesSliverpbCapabilities(t *testing.T) {
destination := t.TempDir()
if err := SetupGoPath(destination, false); err != nil {
t.Fatalf("SetupGoPath() error = %v", err)
}
want, err := protobufs.FS.ReadFile("sliverpb/capabilities.go")
if err != nil {
t.Fatalf("read embedded capabilities.go: %v", err)
}
got, err := os.ReadFile(filepath.Join(destination, "protobuf", "sliverpb", "capabilities.go"))
if err != nil {
t.Fatalf("read extracted capabilities.go: %v", err)
}
if !bytes.Equal(got, want) {
t.Fatal("extracted capabilities.go does not match the embedded source")
}
}
+465
View File
@@ -0,0 +1,465 @@
package c2
import (
"bytes"
"errors"
"io"
"net"
"net/http"
"net/http/httptest"
"sync"
"sync/atomic"
"testing"
"time"
implantCrypto "github.com/bishopfox/sliver/implant/sliver/cryptography"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/protobuf/sliverpb"
"github.com/bishopfox/sliver/server/core"
"github.com/bishopfox/sliver/server/core/rtunnels"
serverCrypto "github.com/bishopfox/sliver/server/cryptography"
serverHandlers "github.com/bishopfox/sliver/server/handlers"
"github.com/hashicorp/yamux"
)
type lifecycleWriteCloser struct {
closed atomic.Bool
}
type lifecycleIdentityEncoder struct{}
func (lifecycleIdentityEncoder) Encode(data []byte) ([]byte, error) { return data, nil }
func (lifecycleIdentityEncoder) Decode(data []byte) ([]byte, error) { return data, nil }
func (writer *lifecycleWriteCloser) Write(data []byte) (int, error) {
return len(data), nil
}
func (writer *lifecycleWriteCloser) Close() error {
writer.closed.Store(true)
return nil
}
func addLifecycleReverseState(t *testing.T, sessionID string) (rtunnels.AuthorizationID, *rtunnels.RTunnel, *lifecycleWriteCloser) {
t.Helper()
authorizationID, err := rtunnels.DefaultRegistry.BeginSpec(sessionID, "127.0.0.1:4443", "127.0.0.1:4444", 0)
if err != nil {
t.Fatalf("begin reverse authorization: %s", err)
}
if err := rtunnels.DefaultRegistry.Activate(sessionID, authorizationID, 1); err != nil {
t.Fatalf("activate reverse authorization: %s", err)
}
writer := &lifecycleWriteCloser{}
tunnel := rtunnels.NewAuthorizedRTunnel(core.NewTunnelID(), sessionID, authorizationID, writer)
if !rtunnels.TryAddRTunnel(tunnel) {
t.Fatal("failed to add reverse tunnel")
}
t.Cleanup(func() {
rtunnels.DefaultRegistry.RevokeSession(sessionID)
rtunnels.CloseSession(sessionID)
})
return authorizationID, tunnel, writer
}
func assertLifecycleReverseStateClosed(t *testing.T, sessionID string, authorizationID rtunnels.AuthorizationID, tunnel *rtunnels.RTunnel, writer *lifecycleWriteCloser) {
t.Helper()
if _, ok := rtunnels.DefaultRegistry.Lookup(sessionID, authorizationID); ok {
t.Fatal("reverse authorization survived connection cleanup")
}
if authorizations := rtunnels.DefaultRegistry.List(sessionID); len(authorizations) != 0 {
t.Fatalf("%d reverse authorizations survived connection cleanup", len(authorizations))
}
if got := rtunnels.GetRTunnel(tunnel.ID); got != nil {
t.Fatal("reverse tunnel survived connection cleanup")
}
select {
case <-tunnel.Done():
default:
t.Fatal("reverse tunnel Done was not closed")
}
if !writer.closed.Load() {
t.Fatal("reverse tunnel writer was not closed")
}
}
func TestHTTPCloseHandlerClosesCoreAndReverseState(t *testing.T) {
connection := core.NewImplantConnection("http", "test")
coreSession := core.NewSession(connection)
core.Sessions.Add(coreSession)
t.Cleanup(func() { core.Sessions.Remove(coreSession.ID) })
var cleanupCalls atomic.Int32
if !connection.SetCleanup(func() {
cleanupCalls.Add(1)
core.Sessions.Remove(coreSession.ID)
}) {
t.Fatal("failed to install HTTP cleanup")
}
authorizationID, tunnel, writer := addLifecycleReverseState(t, coreSession.ID)
httpSession := &HTTPSession{ID: connection.ID, ImplantConn: connection}
server := &SliverHTTPC2{
HTTPSessions: &HTTPSessions{
active: map[string]*HTTPSession{},
mutex: &sync.RWMutex{},
},
}
server.HTTPSessions.Add(httpSession)
recorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, "/close", nil)
server.closeHandler(recorder, request, httpSession)
if recorder.Code != http.StatusAccepted {
t.Fatalf("close status = %d, want %d", recorder.Code, http.StatusAccepted)
}
if got := server.HTTPSessions.Get(httpSession.ID); got != nil {
t.Fatal("HTTP session survived close handler")
}
if got := core.Sessions.Get(coreSession.ID); got != nil {
t.Fatal("core session survived close handler")
}
select {
case <-connection.Done():
default:
t.Fatal("implant connection Done was not closed")
}
assertLifecycleReverseStateClosed(t, coreSession.ID, authorizationID, tunnel, writer)
server.closeHandler(httptest.NewRecorder(), request, httpSession)
if got := cleanupCalls.Load(); got != 1 {
t.Fatalf("cleanup called %d times, want 1", got)
}
}
func TestHTTPConnectionCloseRemovesTransportSessionAndPoll(t *testing.T) {
connection := core.NewImplantConnection("http", "test")
t.Cleanup(connection.Close)
httpSession := &HTTPSession{ID: connection.ID, ImplantConn: connection}
server := &SliverHTTPC2{
ServerConf: &clientpb.HTTPListenerReq{
LongPollTimeout: int64(time.Hour),
},
HTTPSessions: &HTTPSessions{
active: map[string]*HTTPSession{},
mutex: &sync.RWMutex{},
},
}
server.addHTTPSession(httpSession)
pollDone := make(chan struct{})
go func() {
defer close(pollDone)
server.pollHandler(
httptest.NewRecorder(),
httptest.NewRequest(http.MethodGet, "/poll", nil),
httpSession,
lifecycleIdentityEncoder{},
)
}()
deadline := time.Now().Add(time.Second)
for connection.GetLastMessage().IsZero() && time.Now().Before(deadline) {
time.Sleep(time.Millisecond)
}
if connection.GetLastMessage().IsZero() {
t.Fatal("HTTP poll did not enter the long-poll path before close")
}
connection.Close()
select {
case <-pollDone:
case <-time.After(time.Second):
t.Fatal("HTTP poll survived implant connection close")
}
deadline = time.Now().Add(time.Second)
for server.HTTPSessions.Get(httpSession.ID) != nil && time.Now().Before(deadline) {
time.Sleep(time.Millisecond)
}
if got := server.HTTPSessions.Get(httpSession.ID); got != nil {
t.Fatal("HTTP transport session survived implant connection close")
}
request := httptest.NewRequest(http.MethodGet, "/session", nil)
request.AddCookie(&http.Cookie{Value: httpSession.ID})
if got := server.getHTTPSession(request); got != nil {
t.Fatal("closed HTTP transport session was accepted")
}
}
func TestYamuxTransportStopsDispatchAfterImplantConnectionClose(t *testing.T) {
tests := []struct {
name string
handler func(net.Conn, *core.ImplantConnection)
}{
{name: "mtls", handler: handleSliverConnectionYamux},
{name: "wireguard", handler: handleWGSliverConnectionYamux},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
serverSide, implantSide := net.Pipe()
t.Cleanup(func() {
_ = serverSide.Close()
_ = implantSide.Close()
})
connection := core.NewImplantConnection(test.name, "test")
returned := make(chan struct{})
go func() {
defer close(returned)
test.handler(serverSide, connection)
}()
clientSession, err := yamux.Client(implantSide, nil)
if err != nil {
t.Fatalf("create yamux client: %s", err)
}
t.Cleanup(func() { _ = clientSession.Close() })
connection.Close()
select {
case <-returned:
case <-time.After(time.Second):
t.Fatal("yamux transport survived implant connection close")
}
type openResult struct {
connection net.Conn
err error
}
opened := make(chan openResult, 1)
go func() {
stream, openErr := clientSession.Open()
opened <- openResult{connection: stream, err: openErr}
}()
select {
case result := <-opened:
if result.connection != nil {
_ = result.connection.Close()
}
if result.err == nil {
t.Fatal("yamux transport accepted a stream after implant connection close")
}
case <-time.After(time.Second):
t.Fatal("yamux stream open did not return after implant connection close")
}
})
}
}
//nolint:gocyclo // This table-driven lifecycle test keeps both transports on the same synchronized scenario.
func TestYamuxInFlightStreamDoesNotDispatchAfterImplantConnectionClose(t *testing.T) {
const messageType = uint32(0x7ffffffe)
frame, keyID, publicKey := newImplantSignedEnvelopeFrame(t, "lifecycle-dispatch-peer", &sliverpb.Envelope{
Type: messageType,
Data: []byte("must-not-dispatch"),
})
tests := []struct {
name string
handler func(net.Conn, *core.ImplantConnection, map[uint32]serverHandlers.ServerHandler, func(), func())
}{
{name: "mtls", handler: handleSliverConnectionYamuxWithDispatch},
{name: "wireguard", handler: handleWGSliverConnectionYamuxWithDispatch},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
clearMTLSImplantSigKeyCache(t)
mtlsImplantSigKeyCache.Store(keyID, publicKey)
t.Cleanup(func() { mtlsImplantSigKeyCache.Delete(keyID) })
serverSide, implantSide := net.Pipe()
t.Cleanup(func() {
_ = serverSide.Close()
_ = implantSide.Close()
})
connection := core.NewImplantConnection(test.name, "test")
t.Cleanup(connection.Close)
var handlerCalls atomic.Int32
handlers := map[uint32]serverHandlers.ServerHandler{
messageType: func(*core.ImplantConnection, []byte) *sliverpb.Envelope {
handlerCalls.Add(1)
return nil
},
}
readComplete := make(chan struct{})
releaseDispatch := make(chan struct{})
dispatchReleased := make(chan struct{})
streamFinished := make(chan struct{})
beforeDispatch := func() {
close(readComplete)
<-releaseDispatch
close(dispatchReleased)
}
returned := make(chan struct{})
go func() {
defer close(returned)
test.handler(serverSide, connection, handlers, beforeDispatch, func() { close(streamFinished) })
}()
clientSession, err := yamux.Client(implantSide, nil)
if err != nil {
t.Fatalf("create yamux client: %s", err)
}
t.Cleanup(func() { _ = clientSession.Close() })
type openResult struct {
stream net.Conn
err error
}
opened := make(chan openResult, 1)
go func() {
stream, openErr := clientSession.Open()
opened <- openResult{stream: stream, err: openErr}
}()
var stream net.Conn
select {
case result := <-opened:
if result.err != nil {
t.Fatalf("open yamux stream: %v", result.err)
}
stream = result.stream
case <-time.After(time.Second):
t.Fatal("opening in-flight yamux stream timed out")
}
t.Cleanup(func() { _ = stream.Close() })
_ = stream.SetWriteDeadline(time.Now().Add(time.Second))
writeDone := make(chan error, 1)
go func() {
_, writeErr := io.Copy(stream, bytes.NewReader(frame))
writeDone <- writeErr
}()
select {
case <-readComplete:
case writeErr := <-writeDone:
if writeErr != nil {
t.Fatalf("write in-flight yamux envelope: %v", writeErr)
}
select {
case <-readComplete:
case <-time.After(time.Second):
t.Fatal("server read did not reach the pre-dispatch barrier")
}
case <-time.After(time.Second):
t.Fatal("server did not read the in-flight yamux envelope")
}
connection.Close()
close(releaseDispatch)
select {
case <-dispatchReleased:
case <-time.After(time.Second):
t.Fatal("pre-dispatch barrier did not release")
}
select {
case <-streamFinished:
case <-time.After(time.Second):
t.Fatal("in-flight yamux stream did not finish after close")
}
select {
case <-returned:
case <-time.After(time.Second):
t.Fatal("yamux transport survived in-flight close")
}
if got := handlerCalls.Load(); got != 0 {
t.Fatalf("in-flight handler ran %d times after implant connection close", got)
}
})
}
}
func TestDNSSessionOutgoingRetentionLimitsAndClear(t *testing.T) {
key := implantCrypto.RandomSymmetricKey()
dnsSession := newTestDNSSession(0x123456)
dnsSession.CipherCtx = serverCrypto.NewCipherContext(key)
dnsSession.maxOutgoingMessages = 1
dnsSession.maxOutgoingBytes = 1024 * 1024
first := &sliverpb.Envelope{Type: sliverpb.MsgPing, Data: []byte("first")}
if err := dnsSession.StageOutgoingEnvelope(first); err != nil {
t.Fatalf("stage first envelope: %s", err)
}
if dnsSession.outgoingBytes == 0 {
t.Fatal("staged envelope did not consume byte budget")
}
if len(dnsSession.outgoingMsgIDs) != 1 {
t.Fatalf("staged message IDs = %d, want 1", len(dnsSession.outgoingMsgIDs))
}
firstID := dnsSession.outgoingMsgIDs[0]
if err := dnsSession.StageOutgoingEnvelope(&sliverpb.Envelope{Type: sliverpb.MsgPing}); !errors.Is(err, ErrDNSOutgoingLimit) {
t.Fatalf("second stage error = %v, want %v", err, ErrDNSOutgoingLimit)
}
dnsSession.ClearOutgoingEnvelope(firstID)
if dnsSession.outgoingBytes != 0 {
t.Fatalf("outgoing bytes after clear = %d, want 0", dnsSession.outgoingBytes)
}
if len(dnsSession.outgoingBuffers) != 0 || len(dnsSession.outgoingMsgIDs) != 0 {
t.Fatal("clear retained outgoing message state")
}
if err := dnsSession.StageOutgoingEnvelope(&sliverpb.Envelope{Type: sliverpb.MsgPing}); err != nil {
t.Fatalf("stage after clear: %s", err)
}
byteLimited := newTestDNSSession(0x654321)
byteLimited.CipherCtx = serverCrypto.NewCipherContext(key)
byteLimited.maxOutgoingMessages = 10
byteLimited.maxOutgoingBytes = 1
if err := byteLimited.StageOutgoingEnvelope(first); !errors.Is(err, ErrDNSOutgoingLimit) {
t.Fatalf("byte-limited stage error = %v, want %v", err, ErrDNSOutgoingLimit)
}
if byteLimited.outgoingBytes != 0 || len(byteLimited.outgoingBuffers) != 0 {
t.Fatal("rejected byte-limited envelope consumed retention budget")
}
}
func TestDNSNoPollBackpressureClosesSessionAndReverseState(t *testing.T) {
server := newTestDNSServer()
dnsSession := newTestDNSSession(0x456789)
dnsSession.CipherCtx = serverCrypto.NewCipherContext(implantCrypto.RandomSymmetricKey())
dnsSession.ImplantConn = core.NewImplantConnection("dns", "test")
dnsSession.maxOutgoingMessages = 2
dnsSession.maxOutgoingBytes = 1024 * 1024
server.sessions.Store(dnsSession.ID, dnsSession)
coreSession := core.NewSession(dnsSession.ImplantConn)
core.Sessions.Add(coreSession)
t.Cleanup(func() {
server.closeDNSSession(dnsSession)
core.Sessions.Remove(coreSession.ID)
})
cleanupComplete := make(chan struct{})
if !dnsSession.ImplantConn.SetCleanup(func() {
defer close(cleanupComplete)
core.Sessions.Remove(coreSession.ID)
}) {
t.Fatal("failed to install DNS cleanup")
}
authorizationID, tunnel, writer := addLifecycleReverseState(t, coreSession.ID)
server.startDNSSessionSendLoop(dnsSession)
for index := range 3 {
envelope := &sliverpb.Envelope{Type: sliverpb.MsgPing, Data: []byte{byte(index)}}
select {
case dnsSession.ImplantConn.Send <- envelope:
case <-time.After(time.Second):
t.Fatalf("send envelope %d blocked", index)
}
}
select {
case <-dnsSession.ImplantConn.Done():
case <-time.After(2 * time.Second):
t.Fatal("DNS session did not close after outgoing retention limit")
}
select {
case <-cleanupComplete:
case <-time.After(2 * time.Second):
t.Fatal("DNS session cleanup did not complete")
}
if _, ok := server.sessions.Load(dnsSession.ID); ok {
t.Fatal("DNS transport session survived outgoing retention limit")
}
if got := core.Sessions.Get(coreSession.ID); got != nil {
t.Fatal("core session survived DNS outgoing retention limit")
}
if dnsSession.outgoingBytes != 0 || len(dnsSession.outgoingBuffers) != 0 || len(dnsSession.outgoingMsgIDs) != 0 {
t.Fatal("DNS session retained outgoing state after close")
}
assertLifecycleReverseStateClosed(t, coreSession.ID, authorizationID, tunnel, writer)
}
+194 -43
View File
@@ -76,6 +76,13 @@ const (
// INIT carries only key-exchange material and should remain small.
defaultMaxDNSInitSize = 16 * 1024
// An implant must poll and clear staged messages to release them. Bound the
// retained queue so an unreachable or malicious implant cannot make the
// teamserver retain outgoing envelopes indefinitely.
defaultMaxDNSOutgoingMessages = 128
defaultMaxDNSOutgoingBytes = 16 * 1024 * 1024
dnsMessageIDCount = 255
)
var (
@@ -83,6 +90,7 @@ var (
implantBase64 = encoders.Base64{} // Implant's version of base64 with custom alphabet
ErrInvalidMsg = errors.New("invalid dns message")
ErrNoOutgoingMessages = errors.New("no outgoing messages")
ErrDNSOutgoingLimit = errors.New("dns outgoing message retention limit reached")
ErrMsgTooLong = errors.New("too much data to encode")
ErrMsgTooShort = errors.New("too little data to encode")
)
@@ -121,8 +129,12 @@ type DNSSession struct {
dnsIdMsgIdMap map[uint32]uint32
outgoingMsgIDs []uint32
outgoingBuffers map[uint32][]byte
outgoingBytes int
outgoingMutex *sync.RWMutex
maxOutgoingMessages int
maxOutgoingBytes int
incomingEnvelopes map[uint32]*PendingEnvelope
incomingMutex *sync.Mutex
msgCount uint32
@@ -138,7 +150,32 @@ func (s *DNSSession) msgID(id uint32) uint32 {
func (s *DNSSession) nextMsgID() uint32 {
s.msgCount++
return s.msgID(s.msgCount % 255)
return s.msgID(s.msgCount % dnsMessageIDCount)
}
func (s *DNSSession) outgoingLimits() (int, int) {
maxMessages := s.maxOutgoingMessages
if maxMessages <= 0 {
maxMessages = defaultMaxDNSOutgoingMessages
}
maxBytes := s.maxOutgoingBytes
if maxBytes <= 0 {
maxBytes = defaultMaxDNSOutgoingBytes
}
return maxMessages, maxBytes
}
// nextAvailableMsgIDLocked returns an unused message ID. DNS only allocates
// eight bits to this value, so a long-lived session must not overwrite a
// retained buffer when the counter wraps.
func (s *DNSSession) nextAvailableMsgIDLocked() (uint32, bool) {
for range dnsMessageIDCount {
msgID := s.nextMsgID()
if _, retained := s.outgoingBuffers[msgID]; !retained {
return msgID, true
}
}
return 0, false
}
// StageOutgoingEnvelope - Stage an outgoing envelope
@@ -157,9 +194,17 @@ func (s *DNSSession) StageOutgoingEnvelope(envelope *sliverpb.Envelope) error {
s.outgoingMutex.Lock()
defer s.outgoingMutex.Unlock()
msgID := s.nextMsgID()
maxMessages, maxBytes := s.outgoingLimits()
if len(s.outgoingBuffers) >= maxMessages || len(ciphertext) > maxBytes-s.outgoingBytes {
return ErrDNSOutgoingLimit
}
msgID, ok := s.nextAvailableMsgIDLocked()
if !ok {
return ErrDNSOutgoingLimit
}
s.outgoingMsgIDs = append(s.outgoingMsgIDs, msgID)
s.outgoingBuffers[msgID] = ciphertext
s.outgoingBytes += len(ciphertext)
dnsLog.Debugf("Staged outgoing envelope successfully (%d bytes)", len(ciphertext))
return nil
}
@@ -205,7 +250,51 @@ func (s *DNSSession) OutgoingRead(msgID uint32, start uint32, stop uint32) ([]by
func (s *DNSSession) ClearOutgoingEnvelope(msgID uint32) {
s.outgoingMutex.Lock()
defer s.outgoingMutex.Unlock()
ciphertext, ok := s.outgoingBuffers[msgID]
if !ok {
return
}
delete(s.outgoingBuffers, msgID)
s.outgoingBytes -= len(ciphertext)
if s.outgoingBytes < 0 {
s.outgoingBytes = 0
}
queued := s.outgoingMsgIDs[:0]
for _, queuedID := range s.outgoingMsgIDs {
if queuedID != msgID {
queued = append(queued, queuedID)
}
}
s.outgoingMsgIDs = queued
for dnsID, mappedMsgID := range s.dnsIdMsgIdMap {
if mappedMsgID == msgID {
delete(s.dnsIdMsgIdMap, dnsID)
}
}
}
func (s *DNSSession) clearOutgoingEnvelopes() {
s.outgoingMutex.Lock()
defer s.outgoingMutex.Unlock()
clear(s.dnsIdMsgIdMap)
clear(s.outgoingBuffers)
s.outgoingMsgIDs = nil
s.outgoingBytes = 0
}
func (s *DNSSession) lookupOutgoingEnvelope(dnsID uint32) (uint32, uint32, bool) {
s.outgoingMutex.RLock()
defer s.outgoingMutex.RUnlock()
msgID, ok := s.dnsIdMsgIdMap[dnsID]
if !ok {
return 0, 0, false
}
ciphertext, ok := s.outgoingBuffers[msgID]
if !ok {
return 0, 0, false
}
return msgID, uint32(len(ciphertext)), true
}
// IncomingPendingEnvelope - Get a pending message linked list, creates one if it doesn't exist
@@ -229,6 +318,14 @@ func (s *DNSSession) IncomingPendingEnvelope(msgID uint32, size uint32) *Pending
// ForwardCompletedEnvelope - Reassembles and forwards envelopes to core
func (s *DNSSession) ForwardCompletedEnvelope(msgID uint32, pending *PendingEnvelope) {
defer recoverAndLogPanic(dnsLog.Errorf, "dns ForwardCompletedEnvelope")
if s.ImplantConn == nil {
return
}
select {
case <-s.ImplantConn.Done():
return
default:
}
dnsLog.Debugf("[dns] dns session id: %d, msg id: %d completed message", s.ID, msgID)
s.incomingMutex.Lock()
@@ -251,19 +348,22 @@ func (s *DNSSession) ForwardCompletedEnvelope(msgID uint32, pending *PendingEnve
dnsLog.Errorf("Failed to unmarshal message %d: %s", msgID, err)
return
}
select {
case <-s.ImplantConn.Done():
return
default:
}
s.ImplantConn.UpdateLastMessage()
handlers := sliverHandlers.GetHandlers()
if envelope.ID != 0 {
s.ImplantConn.RespMutex.RLock()
defer s.ImplantConn.RespMutex.RUnlock()
if resp, ok := s.ImplantConn.Resp[envelope.ID]; ok {
resp <- envelope
}
s.ImplantConn.DeliverResponse(envelope)
} else if handler, ok := handlers[envelope.Type]; ok {
respEnvelope := handler(s.ImplantConn, envelope.Data)
if respEnvelope != nil {
s.ImplantConn.Send <- respEnvelope
if err := s.ImplantConn.SendEnvelope(respEnvelope, core.DefaultImplantSendTimeout); err != nil {
s.ImplantConn.Close()
}
}
}
}
@@ -369,6 +469,44 @@ func (s *SliverDNSServer) ListenAndServe() error {
return s.server.ListenAndServe()
}
func (s *SliverDNSServer) closeDNSSession(dnsSession *DNSSession) {
if dnsSession == nil {
return
}
s.sessions.CompareAndDelete(dnsSession.ID, dnsSession)
dnsSession.clearOutgoingEnvelopes()
if dnsSession.ImplantConn != nil {
dnsSession.ImplantConn.Close()
}
}
func (s *SliverDNSServer) startDNSSessionSendLoop(dnsSession *DNSSession) {
go func() {
defer s.closeDNSSession(dnsSession)
defer recoverAndLogPanic(dnsLog.Errorf, "dns session send loop")
dnsLog.Debugf("[dns] starting implant conn send loop")
for {
select {
case <-dnsSession.ImplantConn.Done():
dnsLog.Debugf("[dns] closing implant conn send loop")
return
case envelope, ok := <-dnsSession.ImplantConn.Send:
if !ok {
return
}
if err := dnsSession.StageOutgoingEnvelope(envelope); err != nil {
if errors.Is(err, ErrDNSOutgoingLimit) {
dnsLog.Warnf("[dns] closing session %d after outgoing retention limit: %s", dnsSession.ID, err)
return
}
dnsLog.Errorf("[dns] failed to stage outgoing envelope: %s", err)
}
}
}
}()
}
func (s *SliverDNSServer) startInitGC() {
// Tests construct SliverDNSServer directly; only start GC for real listeners.
if s.gcStop != nil {
@@ -493,22 +631,31 @@ func (s *SliverDNSServer) handleC2(domain string, req *dns.Msg) *dns.Msg {
dnsLog.Warnf("[dns] session not found for id %v (%v)", msg.ID, msg.ID&sessionIDBitMask)
return s.nameErrorResp(req)
}
if session, ok := sessionValue.(*DNSSession); ok && session != nil {
session.touch(time.Now())
dnsSession, ok := sessionValue.(*DNSSession)
if !ok || dnsSession == nil || dnsSession.ImplantConn == nil {
dnsLog.Warnf("[dns] invalid session for id %v", msg.ID&sessionIDBitMask)
return s.nameErrorResp(req)
}
select {
case <-dnsSession.ImplantConn.Done():
s.closeDNSSession(dnsSession)
return s.nameErrorResp(req)
default:
}
dnsSession.touch(time.Now())
// Msg Type -> Handler
switch msg.Type {
case dnspb.DNSMessageType_NOP:
return s.handleNOP(domain, msg, checksum, req)
case dnspb.DNSMessageType_POLL:
return s.handlePoll(domain, msg, checksum, req)
return s.handlePoll(domain, dnsSession, msg, checksum, req)
case dnspb.DNSMessageType_DATA_FROM_IMPLANT:
return s.handleDataFromImplant(domain, msg, checksum, req)
return s.handleDataFromImplant(domain, dnsSession, msg, checksum, req)
case dnspb.DNSMessageType_DATA_TO_IMPLANT:
return s.handleDataToImplant(domain, msg, checksum, req)
return s.handleDataToImplant(domain, dnsSession, msg, checksum, req)
case dnspb.DNSMessageType_CLEAR:
return s.handleClear(domain, msg, checksum, req)
return s.handleClear(domain, dnsSession, msg, checksum, req)
}
return nil
}
@@ -689,20 +836,13 @@ func (s *SliverDNSServer) handleDNSSessionInit(domain string, msg *dnspb.DNSMess
return s.refusedErrorResp(req)
}
go func() {
defer recoverAndLogPanic(dnsLog.Errorf, "dns session send loop")
dnsLog.Debugf("[dns] starting implant conn send loop")
for envelope := range dnsSession.ImplantConn.Send {
dnsSession.StageOutgoingEnvelope(envelope)
}
dnsLog.Debugf("[dns] closing implant conn send loop")
}()
s.startDNSSessionSendLoop(dnsSession)
buf := make([]byte, 4)
binary.LittleEndian.PutUint32(buf, dnsSession.ID)
respData, err := dnsSession.CipherCtx.Encrypt(buf)
if err != nil {
dnsLog.Errorf("[session init] failed to encrypt msg with session key: %s", err)
s.closeDNSSession(dnsSession)
return s.refusedErrorResp(req)
}
@@ -749,10 +889,14 @@ func (s *SliverDNSServer) handleDNSSessionInit(domain string, msg *dnspb.DNSMess
return resp
}
func (s *SliverDNSServer) handlePoll(domain string, msg *dnspb.DNSMessage, checksum uint32, req *dns.Msg) *dns.Msg {
func (s *SliverDNSServer) handlePoll(_ string, dnsSession *DNSSession, msg *dnspb.DNSMessage, _ uint32, req *dns.Msg) *dns.Msg {
dnsLog.Debugf("[poll] with dns session id %d", msg.ID&sessionIDBitMask)
loadSession, _ := s.sessions.Load(msg.ID & sessionIDBitMask)
dnsSession := loadSession.(*DNSSession)
select {
case <-dnsSession.ImplantConn.Done():
s.closeDNSSession(dnsSession)
return s.nameErrorResp(req)
default:
}
dnsSession.touch(time.Now())
msgID, msgLen, err := dnsSession.PopOutgoingMsgID(msg)
@@ -764,17 +908,12 @@ func (s *SliverDNSServer) handlePoll(domain string, msg *dnspb.DNSMessage, check
msgLen = 0
msgID = 0
dnsLog.Debugf("[poll] error: %s", err)
oldID, ok := dnsSession.dnsIdMsgIdMap[msg.ID]
oldID, oldLen, ok := dnsSession.lookupOutgoingEnvelope(msg.ID)
if !ok {
dnsLog.Debugf("[poll] no msg id for given request")
} else {
ciphertext, ok := dnsSession.outgoingBuffers[oldID]
if !ok {
dnsLog.Debugf("[poll] no msg for given id")
} else {
msgLen = uint32(len(ciphertext))
msgID = oldID
}
msgLen = oldLen
msgID = oldID
}
}
@@ -831,10 +970,14 @@ func (s *SliverDNSServer) handlePoll(domain string, msg *dnspb.DNSMessage, check
return resp
}
func (s *SliverDNSServer) handleDataFromImplant(domain string, msg *dnspb.DNSMessage, checksum uint32, req *dns.Msg) *dns.Msg {
func (s *SliverDNSServer) handleDataFromImplant(_ string, dnsSession *DNSSession, msg *dnspb.DNSMessage, checksum uint32, req *dns.Msg) *dns.Msg {
dnsLog.Debugf("[from implant] dns session id %d", msg.ID&sessionIDBitMask)
loadSession, _ := s.sessions.Load(msg.ID & sessionIDBitMask)
dnsSession := loadSession.(*DNSSession)
select {
case <-dnsSession.ImplantConn.Done():
s.closeDNSSession(dnsSession)
return s.nameErrorResp(req)
default:
}
dnsSession.touch(time.Now())
dnsLog.Debugf("[from implant] msg id: %d, size: %d", msg.ID, msg.Size)
pending := dnsSession.IncomingPendingEnvelope(msg.ID, msg.Size)
@@ -865,10 +1008,14 @@ func (s *SliverDNSServer) handleDataFromImplant(domain string, msg *dnspb.DNSMes
return resp
}
func (s *SliverDNSServer) handleDataToImplant(domain string, msg *dnspb.DNSMessage, checksum uint32, req *dns.Msg) *dns.Msg {
func (s *SliverDNSServer) handleDataToImplant(_ string, dnsSession *DNSSession, msg *dnspb.DNSMessage, _ uint32, req *dns.Msg) *dns.Msg {
dnsLog.Debugf("[to implant] dns session id %d", msg.ID&sessionIDBitMask)
loadSession, _ := s.sessions.Load(msg.ID & sessionIDBitMask)
dnsSession := loadSession.(*DNSSession)
select {
case <-dnsSession.ImplantConn.Done():
s.closeDNSSession(dnsSession)
return s.nameErrorResp(req)
default:
}
dnsSession.touch(time.Now())
data, err := dnsSession.OutgoingRead(msg.ID, msg.Start, msg.Stop)
@@ -925,10 +1072,14 @@ func (s *SliverDNSServer) handleDataToImplant(domain string, msg *dnspb.DNSMessa
return resp
}
func (s *SliverDNSServer) handleClear(domain string, msg *dnspb.DNSMessage, checksum uint32, req *dns.Msg) *dns.Msg {
func (s *SliverDNSServer) handleClear(_ string, dnsSession *DNSSession, msg *dnspb.DNSMessage, checksum uint32, req *dns.Msg) *dns.Msg {
dnsLog.Debugf("[clear] dns session id %d", msg.ID&sessionIDBitMask)
loadSession, _ := s.sessions.Load(msg.ID & sessionIDBitMask)
dnsSession := loadSession.(*DNSSession)
select {
case <-dnsSession.ImplantConn.Done():
s.closeDNSSession(dnsSession)
return s.nameErrorResp(req)
default:
}
dnsSession.touch(time.Now())
dnsSession.ClearOutgoingEnvelope(msg.ID)
+1
View File
@@ -885,6 +885,7 @@ func newTestDNSServer() *SliverDNSServer {
func newTestDNSSession(sessionID uint32) *DNSSession {
return &DNSSession{
ID: sessionID,
ImplantConn: core.NewImplantConnection("dns", "test"),
dnsIdMsgIdMap: map[uint32]uint32{},
outgoingMsgIDs: []uint32{},
outgoingBuffers: map[uint32][]byte{},
+74 -10
View File
@@ -116,6 +116,31 @@ func (s *HTTPSessions) Remove(sessionID string) {
delete(s.active, sessionID)
}
// RemoveIf removes sessionID only when it still names the expected transport
// session. A delayed Done watcher must never delete a newer session that reused
// the same identifier.
func (s *HTTPSessions) RemoveIf(sessionID string, expected *HTTPSession) bool {
s.mutex.Lock()
defer s.mutex.Unlock()
if s.active[sessionID] != expected {
return false
}
delete(s.active, sessionID)
return true
}
// addHTTPSession publishes a transport session and binds its lifetime to the
// underlying implant connection. Core rejection paths close ImplantConn; the
// transport entry must disappear at the same boundary so a stale cookie cannot
// keep dispatching envelopes after authorization cleanup.
func (s *SliverHTTPC2) addHTTPSession(session *HTTPSession) {
s.HTTPSessions.Add(session)
go func() {
<-session.ImplantConn.Done()
s.HTTPSessions.RemoveIf(session.ID, session)
}()
}
// HTTPHandler - Path mapped to a handler function
type HTTPHandler func(resp http.ResponseWriter, req *http.Request)
@@ -534,6 +559,7 @@ func (s *SliverHTTPC2) authenticatedHandler(resp http.ResponseWriter, req *http.
encoder, err := getEncoder(req.URL, c2Config)
if err != nil {
s.closeHandler(resp, req, session)
return
}
if req.Method == http.MethodPost {
@@ -625,12 +651,13 @@ func (s *SliverHTTPC2) startSessionHandler(resp http.ResponseWriter, req *http.R
httpSession.CipherCtx = cryptography.NewCipherContext(sKey)
httpSession.ImplantConn = core.NewImplantConnection("http(s)", getRemoteAddr(req))
httpSession.C2Profile = implantConfig.HTTPC2ConfigName
s.HTTPSessions.Add(httpSession)
s.addHTTPSession(httpSession)
httpLog.Infof("Started new session with http session id: %s", httpSession.ID)
responseCiphertext, err := httpSession.CipherCtx.Encrypt([]byte(httpSession.ID))
if err != nil {
httpLog.Info("Failed to encrypt session identifier")
httpSession.ImplantConn.Close()
s.defaultHandler(resp, req)
return
}
@@ -647,7 +674,17 @@ func (s *SliverHTTPC2) startSessionHandler(resp http.ResponseWriter, req *http.R
}
func (s *SliverHTTPC2) sessionHandler(resp http.ResponseWriter, req *http.Request, httpSession *HTTPSession, c2profile *clientpb.HTTPC2Config, encoder sliverEncoders.Encoder) {
s.sessionHandlerWithHandlers(resp, req, httpSession, c2profile, encoder, sliverHandlers.GetHandlers())
}
func (s *SliverHTTPC2) sessionHandlerWithHandlers(resp http.ResponseWriter, req *http.Request, httpSession *HTTPSession, c2profile *clientpb.HTTPC2Config, encoder sliverEncoders.Encoder, handlers map[uint32]sliverHandlers.ServerHandler) {
httpLog.Debug("Session request")
select {
case <-httpSession.ImplantConn.Done():
s.HTTPSessions.RemoveIf(httpSession.ID, httpSession)
return
default:
}
httpSession.ImplantConn.UpdateLastMessage()
plaintext, err := s.readReqBody(httpSession, resp, req, c2profile, encoder)
@@ -663,23 +700,29 @@ func (s *SliverHTTPC2) sessionHandler(resp http.ResponseWriter, req *http.Reques
s.defaultHandler(resp, req)
return
}
select {
case <-httpSession.ImplantConn.Done():
s.HTTPSessions.RemoveIf(httpSession.ID, httpSession)
return
default:
}
resp.WriteHeader(http.StatusAccepted)
handlers := sliverHandlers.GetHandlers()
if envelope.ID != 0 {
httpSession.ImplantConn.RespMutex.RLock()
defer httpSession.ImplantConn.RespMutex.RUnlock()
if resp, ok := httpSession.ImplantConn.Resp[envelope.ID]; ok {
resp <- envelope
}
httpSession.ImplantConn.DeliverResponse(envelope)
} else if handler, ok := handlers[envelope.Type]; ok {
respEnvelope := handler(httpSession.ImplantConn, envelope.Data)
if respEnvelope != nil {
go func() {
httpSession.ImplantConn.Send <- respEnvelope
if err := httpSession.ImplantConn.SendEnvelope(respEnvelope, core.DefaultImplantSendTimeout); err != nil {
httpSession.ImplantConn.Close()
}
}()
}
}
// A successful POST is also the implant's transport-level retirement
// acknowledgement. Emit it only after synchronous dispatch has admitted the
// envelope so the implant can bound how far later concurrent POSTs overtake it.
resp.WriteHeader(http.StatusAccepted)
}
func (s *SliverHTTPC2) pollHandler(resp http.ResponseWriter, req *http.Request, httpSession *HTTPSession, encoder sliverEncoders.Encoder) {
@@ -687,7 +730,19 @@ func (s *SliverHTTPC2) pollHandler(resp http.ResponseWriter, req *http.Request,
httpSession.ImplantConn.UpdateLastMessage()
select {
case <-httpSession.ImplantConn.Done():
s.HTTPSessions.RemoveIf(httpSession.ID, httpSession)
return
case envelope := <-httpSession.ImplantConn.Send:
select {
case <-httpSession.ImplantConn.Done():
s.HTTPSessions.RemoveIf(httpSession.ID, httpSession)
return
default:
}
if envelope == nil {
return
}
resp.WriteHeader(http.StatusOK)
envelopeData, _ := proto.Marshal(envelope)
ciphertext, err := httpSession.CipherCtx.Encrypt(envelopeData)
@@ -761,7 +816,10 @@ func (s *SliverHTTPC2) closeHandler(resp http.ResponseWriter, req *http.Request,
cookie.MaxAge = -1
http.SetCookie(resp, cookie)
}
s.HTTPSessions.Remove(httpSession.ID)
s.HTTPSessions.RemoveIf(httpSession.ID, httpSession)
if httpSession.ImplantConn != nil {
httpSession.ImplantConn.Close()
}
resp.WriteHeader(http.StatusAccepted)
}
@@ -808,6 +866,12 @@ func (s *SliverHTTPC2) getHTTPSession(req *http.Request) *HTTPSession {
for _, cookie := range req.Cookies() {
httpSession := s.HTTPSessions.Get(cookie.Value)
if httpSession != nil {
select {
case <-httpSession.ImplantConn.Done():
s.HTTPSessions.RemoveIf(httpSession.ID, httpSession)
return nil
default:
}
httpSession.ImplantConn.UpdateLastMessage()
return httpSession
}
+123
View File
@@ -0,0 +1,123 @@
package c2
import (
"bytes"
"net/http"
"net/http/httptest"
"sync"
"testing"
"time"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/protobuf/sliverpb"
"github.com/bishopfox/sliver/server/core"
serverCrypto "github.com/bishopfox/sliver/server/cryptography"
serverHandlers "github.com/bishopfox/sliver/server/handlers"
"google.golang.org/protobuf/proto"
)
type orderingResponseWriter struct {
header http.Header
status chan int
once sync.Once
}
type orderingIdentityEncoder struct{}
func (orderingIdentityEncoder) Encode(data []byte) ([]byte, error) { return data, nil }
func (orderingIdentityEncoder) Decode(data []byte) ([]byte, error) { return data, nil }
func newOrderingResponseWriter() *orderingResponseWriter {
return &orderingResponseWriter{
header: http.Header{},
status: make(chan int, 1),
}
}
func (writer *orderingResponseWriter) Header() http.Header {
return writer.header
}
func (writer *orderingResponseWriter) WriteHeader(status int) {
writer.once.Do(func() { writer.status <- status })
}
func (writer *orderingResponseWriter) Write(data []byte) (int, error) {
writer.WriteHeader(http.StatusOK)
return len(data), nil
}
func TestHTTPSessionAcknowledgesPostAfterSynchronousDispatch(t *testing.T) {
key := [32]byte{9, 8, 7, 6}
httpSession := &HTTPSession{
ID: "ordering-session",
ImplantConn: core.NewImplantConnection("http(s)", "test"),
CipherCtx: serverCrypto.NewCipherContext(key),
}
t.Cleanup(httpSession.ImplantConn.Close)
server := &SliverHTTPC2{
HTTPSessions: &HTTPSessions{active: map[string]*HTTPSession{}, mutex: &sync.RWMutex{}},
}
envelopeData, err := proto.Marshal(&sliverpb.Envelope{Type: sliverpb.MsgPing})
if err != nil {
t.Fatalf("marshal HTTP test envelope: %v", err)
}
ciphertext, err := serverCrypto.Encrypt(key, envelopeData)
if err != nil {
t.Fatalf("encrypt HTTP test envelope: %v", err)
}
request := httptest.NewRequest(http.MethodPost, "/session", bytes.NewReader(ciphertext))
response := newOrderingResponseWriter()
dispatchStarted := make(chan struct{})
dispatchRelease := make(chan struct{})
var releaseOnce sync.Once
release := func() { releaseOnce.Do(func() { close(dispatchRelease) }) }
t.Cleanup(release)
handlers := map[uint32]serverHandlers.ServerHandler{
sliverpb.MsgPing: func(*core.ImplantConnection, []byte) *sliverpb.Envelope {
close(dispatchStarted)
<-dispatchRelease
return nil
},
}
done := make(chan struct{})
go func() {
defer close(done)
server.sessionHandlerWithHandlers(
response,
request,
httpSession,
&clientpb.HTTPC2Config{},
orderingIdentityEncoder{},
handlers,
)
}()
select {
case <-dispatchStarted:
case <-time.After(time.Second):
t.Fatal("HTTP session handler did not begin envelope dispatch")
}
select {
case status := <-response.status:
t.Fatalf("HTTP session acknowledged status %d before dispatch completed", status)
case <-time.After(50 * time.Millisecond):
}
release()
select {
case status := <-response.status:
if status != http.StatusAccepted {
t.Fatalf("HTTP session acknowledgement = %d, want %d", status, http.StatusAccepted)
}
case <-time.After(time.Second):
t.Fatal("HTTP session did not acknowledge completed dispatch")
}
select {
case <-done:
case <-time.After(time.Second):
t.Fatal("HTTP session handler did not return after dispatch")
}
}
+47 -10
View File
@@ -150,7 +150,7 @@ func handleSliverConnection(conn net.Conn) {
defer func() {
mtlsLog.Debugf("mtls connection closing")
conn.Close()
implantConn.Cleanup()
implantConn.Close()
}()
br := bufio.NewReader(conn)
@@ -178,6 +178,11 @@ func (c *mtlsBufferedConn) Read(p []byte) (int, error) {
}
func handleSliverConnectionYamux(conn net.Conn, implantConn *core.ImplantConnection) {
handleSliverConnectionYamuxWithDispatch(conn, implantConn, serverHandlers.GetHandlers(), nil, nil)
}
//nolint:gocyclo // The transport loop keeps stream admission, shutdown, dispatch, and response handling together.
func handleSliverConnectionYamuxWithDispatch(conn net.Conn, implantConn *core.ImplantConnection, handlers map[uint32]serverHandlers.ServerHandler, beforeDispatch func(), afterStream func()) {
defer recoverAndLogPanic(mtlsLog.Errorf, "mtls handleSliverConnectionYamux")
session, err := yamux.Server(conn, nil)
@@ -195,11 +200,16 @@ func handleSliverConnectionYamux(conn net.Conn, implantConn *core.ImplantConnect
session.Close()
})
}
go func() {
select {
case <-implantConn.Done():
closeDone()
case <-done:
}
}()
streamSem := make(chan struct{}, mtlsYamuxMaxConcurrentStreams)
sendSem := make(chan struct{}, mtlsYamuxMaxConcurrentSends)
handlers := serverHandlers.GetHandlers()
go func() {
defer closeDone()
defer recoverAndLogPanic(mtlsLog.Errorf, "mtls yamux accept loop")
@@ -223,6 +233,9 @@ func handleSliverConnectionYamux(conn net.Conn, implantConn *core.ImplantConnect
defer func() {
<-streamSem
}()
if afterStream != nil {
defer afterStream()
}
defer recoverAndLogPanic(mtlsLog.Errorf, "mtls yamux stream")
defer stream.Close()
@@ -232,15 +245,18 @@ func handleSliverConnectionYamux(conn net.Conn, implantConn *core.ImplantConnect
closeDone()
return
}
if beforeDispatch != nil {
beforeDispatch()
}
select {
case <-implantConn.Done():
return
default:
}
implantConn.UpdateLastMessage()
if envelope.ID != 0 {
implantConn.RespMutex.RLock()
resp, ok := implantConn.Resp[envelope.ID]
implantConn.RespMutex.RUnlock()
if ok {
resp <- envelope
}
implantConn.DeliverResponse(envelope)
return
}
@@ -248,10 +264,19 @@ func handleSliverConnectionYamux(conn net.Conn, implantConn *core.ImplantConnect
mtlsLog.Debugf("Received new mtls message type %d, data: %s", envelope.Type, envelope.Data)
go func(envelope *sliverpb.Envelope) {
defer recoverAndLogPanic(mtlsLog.Errorf, "mtls message handler")
select {
case <-implantConn.Done():
return
case <-done:
return
default:
}
respEnvelope := handler(implantConn, envelope.Data)
if respEnvelope != nil {
implantConn.Send <- respEnvelope
if err := implantConn.SendEnvelope(respEnvelope, core.DefaultImplantSendTimeout); err != nil {
implantConn.Close()
}
}
}(envelope)
}
@@ -264,7 +289,19 @@ func handleSliverConnectionYamux(conn net.Conn, implantConn *core.ImplantConnect
defer recoverAndLogPanic(mtlsLog.Errorf, "mtls yamux sender loop")
for {
select {
case <-implantConn.Done():
return
case envelope := <-implantConn.Send:
select {
case <-implantConn.Done():
return
case <-done:
return
default:
}
if envelope == nil {
continue
}
select {
case sendSem <- struct{}{}:
case <-done:
@@ -0,0 +1,557 @@
//go:build server && go_sqlite && sliver_e2e
package c2_test
import (
"bytes"
"context"
"errors"
"io"
"net"
"os"
"runtime"
"strconv"
"sync"
"sync/atomic"
"testing"
"time"
implantMTLS "github.com/bishopfox/sliver/implant/sliver/transports/mtls"
"github.com/bishopfox/sliver/protobuf/commonpb"
"github.com/bishopfox/sliver/protobuf/rpcpb"
"github.com/bishopfox/sliver/protobuf/sliverpb"
"github.com/bishopfox/sliver/server/c2"
"github.com/bishopfox/sliver/server/core"
"github.com/bishopfox/sliver/server/core/rtunnels"
"github.com/bishopfox/sliver/server/transport"
"github.com/google/uuid"
"github.com/hashicorp/yamux"
"google.golang.org/protobuf/proto"
)
const rportAuthorizationE2ETimeout = 15 * time.Second
// TestMTLSYamux_ReversePortForwardAuthorizationCannotBePoisonedOrReplayed
// exercises the production RPC, C2 dispatch, cross-session listener isolation,
// authorization broker, TCP dial, and relay path with protocol peers that are
// intentionally not the production implant implementation. This matters for
// GHSA-w4h3-gj8x-mqr5: an honest implant cannot reproduce destination poisoning
// in a regression test.
func TestMTLSYamux_ReversePortForwardAuthorizationCannotBePoisonedOrReplayed(t *testing.T) {
authorizedPayload := []byte("operator-authorized-target\x00\xff")
authorizedTarget := startRportAuthorizationTarget(t, authorizedPayload, true)
poisonTarget := startRportAuthorizationTarget(t, nil, false)
grpcServer, grpcListener, err := transport.LocalListener()
if err != nil {
t.Fatalf("start local grpc listener: %v", err)
}
t.Cleanup(func() {
grpcServer.Stop()
_ = grpcListener.Close()
})
owner := startRportProtocolPeer(t, "authorization-owner", poisonTarget.address(), 73)
attacker := startRportProtocolPeer(t, "authorization-attacker", "", 0)
ownerSession := waitForRportProtocolSession(t, owner.registrationUUID)
attackerSession := waitForRportProtocolSession(t, attacker.registrationUUID)
if ownerSession.ID == attackerSession.ID || ownerSession.Connection.ID == attackerSession.Connection.ID {
t.Fatalf("protocol peers did not receive independent connection/session identities: owner=%s/%s attacker=%s/%s",
ownerSession.ID, ownerSession.Connection.ID, attackerSession.ID, attackerSession.Connection.ID)
}
t.Cleanup(func() {
rtunnels.DefaultRegistry.RevokeSession(ownerSession.ID)
rtunnels.CloseSession(ownerSession.ID)
rtunnels.DefaultRegistry.RevokeSession(attackerSession.ID)
rtunnels.CloseSession(attackerSession.ID)
core.Sessions.Remove(ownerSession.ID)
core.Sessions.Remove(attackerSession.ID)
})
rpcConnection, err := dialBufConn(context.Background(), grpcListener)
if err != nil {
t.Fatalf("dial grpc/bufconn: %v", err)
}
t.Cleanup(func() { _ = rpcConnection.Close() })
rpcClient := rpcpb.NewSliverRPCClient(rpcConnection)
operatorBindAddress := "127.0.0.1:31337"
ctx, cancel := context.WithTimeout(context.Background(), rportAuthorizationE2ETimeout)
listener, err := rpcClient.StartRportFwdListener(ctx, &sliverpb.RportFwdStartListenerReq{
BindAddress: operatorBindAddress,
ForwardAddress: authorizedTarget.address(),
KeepAlive: 5,
Request: &commonpb.Request{
SessionID: ownerSession.ID,
Timeout: int64(10 * time.Second),
},
})
cancel()
if err != nil {
t.Fatalf("start reverse port forward through production RPC/C2 path: %v", err)
}
startRequest := receiveRportStartRequest(t, owner.startRequests)
if startRequest.GetForwardAddress() != authorizedTarget.address() {
t.Fatalf("implant received forward address %q, want operator target %q", startRequest.GetForwardAddress(), authorizedTarget.address())
}
if startRequest.GetAuthorizationID() == "" {
t.Fatal("implant did not receive a server-issued authorization ID")
}
if listener.GetAuthorizationID() != startRequest.GetAuthorizationID() {
t.Fatalf("RPC authorization ID = %q, want server-issued %q", listener.GetAuthorizationID(), startRequest.GetAuthorizationID())
}
if listener.GetForwardAddress() != authorizedTarget.address() {
t.Fatalf("implant response poisoned RPC destination: got %q, want %q", listener.GetForwardAddress(), authorizedTarget.address())
}
if listener.GetForwardAddress() == poisonTarget.address() {
t.Fatalf("RPC returned implant-controlled destination %q", listener.GetForwardAddress())
}
ctx, cancel = context.WithTimeout(context.Background(), rportAuthorizationE2ETimeout)
inventory, err := rpcClient.GetRportFwdListeners(ctx, &sliverpb.RportFwdListenersReq{
Request: &commonpb.Request{SessionID: ownerSession.ID},
})
cancel()
if err != nil {
t.Fatalf("get authoritative reverse port forward inventory: %v", err)
}
if len(inventory.GetListeners()) != 1 || inventory.GetListeners()[0].GetForwardAddress() != authorizedTarget.address() {
t.Fatalf("authoritative inventory = %+v, want only operator target %q", inventory.GetListeners(), authorizedTarget.address())
}
// Listener IDs are implant-local numeric handles and can collide across
// sessions. A stop issued through another valid session must be routed only
// to that session's implant. It may succeed as a best-effort legacy cleanup,
// but it must never revoke or mutate the owner's server-side authorization.
ctx, cancel = context.WithTimeout(context.Background(), rportAuthorizationE2ETimeout)
attackerStopResponse, err := rpcClient.StopRportFwdListener(ctx, &sliverpb.RportFwdStopListenerReq{
ID: listener.GetID(),
Request: &commonpb.Request{
SessionID: attackerSession.ID,
Timeout: int64(10 * time.Second),
},
})
cancel()
if err != nil {
t.Fatalf("stop owner listener ID through valid attacker session: %v", err)
}
attackerStopRequest := receiveRportStopRequest(t, attacker.stopRequests)
if attackerStopRequest.GetID() != listener.GetID() {
t.Fatalf("attacker implant stop ID = %d, want owner numeric ID %d", attackerStopRequest.GetID(), listener.GetID())
}
if attackerStopResponse.GetID() != listener.GetID() || attackerStopResponse.GetBindAddress() != "" || attackerStopResponse.GetForwardAddress() != "" || attackerStopResponse.GetAuthorizationID() != "" {
t.Fatalf("cross-session cleanup response retained metadata or wrong ID: %+v", attackerStopResponse)
}
ownerAuthorization, ownerStillActive := rtunnels.DefaultRegistry.LookupListener(ownerSession.ID, listener.GetID())
if !ownerStillActive {
t.Fatal("valid attacker session revoked the owner's listener authorization")
}
if ownerAuthorization.State != rtunnels.AuthorizationActive || ownerAuthorization.AuthorizationID.String() != listener.GetAuthorizationID() || ownerAuthorization.Address != authorizedTarget.address() {
t.Fatalf("valid attacker session mutated owner authorization: %+v", ownerAuthorization)
}
if _, attackerAuthorizationCreated := rtunnels.DefaultRegistry.LookupListener(attackerSession.ID, listener.GetID()); attackerAuthorizationCreated {
t.Fatal("cross-session cleanup created an attacker authorization")
}
poisonHost, poisonPort := splitRportTargetAddress(t, poisonTarget.address())
const replayTunnelID = uint64(0x7a110001)
attacker.sendTunnelData(t, &sliverpb.TunnelData{
TunnelID: replayTunnelID,
CreateReverse: true,
Data: []byte("cross-session-capability-replay"),
Rportfwd: &sliverpb.RPortfwd{
AuthorizationID: listener.GetAuthorizationID(),
Host: poisonHost,
Port: poisonPort,
},
})
replayRejection := receiveRportTunnelMessage(t, attacker.tunnelCloses, replayTunnelID)
if !replayRejection.GetClosed() {
t.Fatalf("cross-session capability replay response = %+v, want closed rejection", replayRejection)
}
if got := authorizedTarget.accepted.Load(); got != 0 {
t.Fatalf("cross-session capability replay dialed operator target %d times", got)
}
if got := poisonTarget.accepted.Load(); got != 0 {
t.Fatalf("cross-session capability replay dialed implant target %d times", got)
}
// The owner now sends exactly the pair of malicious fields from the
// advisory: its listener response already claimed target B, and this create
// message also claims target B. The server must resolve the capability's
// immutable target A, never either implant-controlled value.
const ownerTunnelID = uint64(0x7a110002)
owner.sendTunnelData(t, &sliverpb.TunnelData{
TunnelID: ownerTunnelID,
CreateReverse: true,
Data: authorizedPayload,
Rportfwd: &sliverpb.RPortfwd{
AuthorizationID: listener.GetAuthorizationID(),
Host: poisonHost,
Port: poisonPort,
},
})
receivedAtAuthorizedTarget := receiveRportTargetPayload(t, authorizedTarget)
if !bytes.Equal(receivedAtAuthorizedTarget, authorizedPayload) {
t.Fatalf("authorized target payload = %x, want %x", receivedAtAuthorizedTarget, authorizedPayload)
}
relayedBack := receiveRportTunnelMessage(t, owner.tunnelData, ownerTunnelID)
if !bytes.Equal(relayedBack.GetData(), authorizedPayload) {
t.Fatalf("relayed response = %x, want %x", relayedBack.GetData(), authorizedPayload)
}
if got := authorizedTarget.accepted.Load(); got != 1 {
t.Fatalf("operator target dial count = %d, want 1", got)
}
if got := poisonTarget.accepted.Load(); got != 0 {
t.Fatalf("implant-controlled target dial count = %d, want 0", got)
}
}
type rportProtocolPeer struct {
rawConnection net.Conn
mux *yamux.Session
registrationUUID string
poisonAddress string
listenerID uint32
startRequests chan *sliverpb.RportFwdStartListenerReq
stopRequests chan *sliverpb.RportFwdStopListenerReq
tunnelData chan *sliverpb.TunnelData
tunnelCloses chan *sliverpb.TunnelData
done chan struct{}
}
func startRportProtocolPeer(t *testing.T, name string, poisonAddress string, listenerID uint32) *rportProtocolPeer {
t.Helper()
serverConnection, implantConnection := net.Pipe()
go c2.HandleSliverConnectionForTest(serverConnection)
if _, err := implantConnection.Write([]byte(implantMTLS.YamuxPreface)); err != nil {
_ = implantConnection.Close()
t.Fatalf("write yamux preface for %s: %v", name, err)
}
config := yamux.DefaultConfig()
config.LogOutput = io.Discard
muxSession, err := yamux.Client(implantConnection, config)
if err != nil {
_ = implantConnection.Close()
t.Fatalf("start yamux protocol peer %s: %v", name, err)
}
peer := &rportProtocolPeer{
rawConnection: implantConnection,
mux: muxSession,
registrationUUID: uuid.NewString(),
poisonAddress: poisonAddress,
listenerID: listenerID,
startRequests: make(chan *sliverpb.RportFwdStartListenerReq, 1),
stopRequests: make(chan *sliverpb.RportFwdStopListenerReq, 1),
tunnelData: make(chan *sliverpb.TunnelData, 8),
tunnelCloses: make(chan *sliverpb.TunnelData, 8),
done: make(chan struct{}),
}
registerData, err := proto.Marshal(&sliverpb.Register{
Name: name,
Hostname: "localhost",
Uuid: peer.registrationUUID,
Username: "protocol-test",
Os: runtime.GOOS,
Arch: runtime.GOARCH,
Pid: int32(os.Getpid()),
Filename: "rportfwd-malicious-protocol-peer",
ActiveC2: "mtls://protocol-test",
Version: "protocol-test",
Locale: "en_US",
Capabilities: sliverpb.CapabilityTunnelTerminalV1,
})
if err != nil {
peer.close()
t.Fatalf("marshal %s registration: %v", name, err)
}
if err := sendYamuxEnvelope(peer.mux, &sliverpb.Envelope{Type: sliverpb.MsgRegister, Data: registerData}); err != nil {
peer.close()
t.Fatalf("send %s registration: %v", name, err)
}
go peer.acceptLoop()
t.Cleanup(peer.close)
return peer
}
func (peer *rportProtocolPeer) acceptLoop() {
defer close(peer.done)
for {
stream, err := peer.mux.Accept()
if err != nil {
return
}
go peer.handleServerEnvelope(stream)
}
}
func (peer *rportProtocolPeer) handleServerEnvelope(stream net.Conn) {
defer stream.Close()
envelope, err := implantMTLS.ReadEnvelope(stream)
if err != nil || envelope == nil {
return
}
switch envelope.Type {
case sliverpb.MsgRportFwdStartListenerReq:
request := &sliverpb.RportFwdStartListenerReq{}
if proto.Unmarshal(envelope.Data, request) != nil {
return
}
select {
case peer.startRequests <- request:
default:
}
responseData, marshalErr := proto.Marshal(&sliverpb.RportFwdListener{
ID: peer.listenerID,
BindAddress: "implant-controlled-bind:1",
ForwardAddress: peer.poisonAddress,
AuthorizationID: request.GetAuthorizationID(),
Response: &commonpb.Response{},
})
if marshalErr == nil {
_ = sendYamuxEnvelope(peer.mux, &sliverpb.Envelope{ID: envelope.ID, Data: responseData})
}
case sliverpb.MsgRportFwdStopListenerReq:
request := &sliverpb.RportFwdStopListenerReq{}
if proto.Unmarshal(envelope.Data, request) != nil {
return
}
select {
case peer.stopRequests <- request:
default:
}
responseData, marshalErr := proto.Marshal(&sliverpb.RportFwdListener{
ID: request.GetID(),
Response: &commonpb.Response{},
})
if marshalErr == nil {
_ = sendYamuxEnvelope(peer.mux, &sliverpb.Envelope{ID: envelope.ID, Data: responseData})
}
case sliverpb.MsgTunnelData:
data := &sliverpb.TunnelData{}
if proto.Unmarshal(envelope.Data, data) == nil {
select {
case peer.tunnelData <- data:
default:
}
}
case sliverpb.MsgTunnelClose:
data := &sliverpb.TunnelData{}
if proto.Unmarshal(envelope.Data, data) == nil {
select {
case peer.tunnelCloses <- data:
default:
}
}
default:
if envelope.ID != 0 {
_ = sendYamuxEnvelope(peer.mux, &sliverpb.Envelope{ID: envelope.ID, UnknownMessageType: true})
}
}
}
func (peer *rportProtocolPeer) sendTunnelData(t *testing.T, tunnelData *sliverpb.TunnelData) {
t.Helper()
data, err := proto.Marshal(tunnelData)
if err != nil {
t.Fatalf("marshal tunnel data: %v", err)
}
if err := sendYamuxEnvelope(peer.mux, &sliverpb.Envelope{Type: sliverpb.MsgTunnelData, Data: data}); err != nil {
t.Fatalf("send tunnel data: %v", err)
}
}
func (peer *rportProtocolPeer) close() {
_ = peer.mux.Close()
_ = peer.rawConnection.Close()
select {
case <-peer.done:
case <-time.After(5 * time.Second):
}
}
func waitForRportProtocolSession(t *testing.T, registrationUUID string) *core.Session {
t.Helper()
deadline := time.Now().Add(rportAuthorizationE2ETimeout)
for time.Now().Before(deadline) {
for _, session := range core.Sessions.All() {
if session.UUID == registrationUUID {
return session
}
}
time.Sleep(10 * time.Millisecond)
}
t.Fatalf("timed out waiting for protocol session UUID %s", registrationUUID)
return nil
}
func receiveRportStartRequest(t *testing.T, requests <-chan *sliverpb.RportFwdStartListenerReq) *sliverpb.RportFwdStartListenerReq {
t.Helper()
select {
case request := <-requests:
return request
case <-time.After(rportAuthorizationE2ETimeout):
t.Fatal("timed out waiting for reverse port forward start request")
return nil
}
}
func receiveRportStopRequest(t *testing.T, requests <-chan *sliverpb.RportFwdStopListenerReq) *sliverpb.RportFwdStopListenerReq {
t.Helper()
select {
case request := <-requests:
return request
case <-time.After(rportAuthorizationE2ETimeout):
t.Fatal("timed out waiting for reverse port forward stop request")
return nil
}
}
func receiveRportTunnelMessage(t *testing.T, messages <-chan *sliverpb.TunnelData, tunnelID uint64) *sliverpb.TunnelData {
t.Helper()
timer := time.NewTimer(rportAuthorizationE2ETimeout)
defer timer.Stop()
for {
select {
case message := <-messages:
if message.GetTunnelID() == tunnelID {
return message
}
case <-timer.C:
t.Fatalf("timed out waiting for reverse tunnel %d message", tunnelID)
return nil
}
}
}
func splitRportTargetAddress(t *testing.T, address string) (string, uint32) {
t.Helper()
host, portText, err := net.SplitHostPort(address)
if err != nil {
t.Fatalf("split target address %q: %v", address, err)
}
port, err := strconv.ParseUint(portText, 10, 16)
if err != nil {
t.Fatalf("parse target port %q: %v", portText, err)
}
return host, uint32(port)
}
type rportAuthorizationTarget struct {
listener net.Listener
wantPayload []byte
echo bool
payloads chan []byte
errors chan error
done chan struct{}
accepted atomic.Int64
connections sync.Map
}
func startRportAuthorizationTarget(t *testing.T, wantPayload []byte, echo bool) *rportAuthorizationTarget {
t.Helper()
listener, err := net.Listen("tcp4", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen for reverse port forward target: %v", err)
}
target := &rportAuthorizationTarget{
listener: listener,
wantPayload: append([]byte(nil), wantPayload...),
echo: echo,
payloads: make(chan []byte, 8),
errors: make(chan error, 8),
done: make(chan struct{}),
}
go target.serve()
t.Cleanup(target.close)
return target
}
func (target *rportAuthorizationTarget) address() string {
return target.listener.Addr().String()
}
func (target *rportAuthorizationTarget) serve() {
defer close(target.done)
for {
connection, err := target.listener.Accept()
if err != nil {
if !isClosedRportTargetError(err) {
select {
case target.errors <- err:
default:
}
}
return
}
target.accepted.Add(1)
target.connections.Store(connection, struct{}{})
go target.handleConnection(connection)
}
}
func (target *rportAuthorizationTarget) handleConnection(connection net.Conn) {
defer target.connections.Delete(connection)
defer connection.Close()
if len(target.wantPayload) == 0 {
return
}
_ = connection.SetDeadline(time.Now().Add(rportAuthorizationE2ETimeout))
payload := make([]byte, len(target.wantPayload))
if _, err := io.ReadFull(connection, payload); err != nil {
select {
case target.errors <- err:
default:
}
return
}
select {
case target.payloads <- payload:
default:
}
if target.echo {
if _, err := connection.Write(payload); err != nil {
select {
case target.errors <- err:
default:
}
}
}
}
func (target *rportAuthorizationTarget) close() {
_ = target.listener.Close()
target.connections.Range(func(connection, _ any) bool {
_ = connection.(net.Conn).Close()
return true
})
select {
case <-target.done:
case <-time.After(5 * time.Second):
}
}
func receiveRportTargetPayload(t *testing.T, target *rportAuthorizationTarget) []byte {
t.Helper()
select {
case payload := <-target.payloads:
return payload
case err := <-target.errors:
t.Fatalf("operator-authorized target relay error: %v", err)
return nil
case <-time.After(rportAuthorizationE2ETimeout):
t.Fatal("timed out waiting for data at operator-authorized target")
return nil
}
}
func isClosedRportTargetError(err error) bool {
return errors.Is(err, net.ErrClosed)
}
+47 -10
View File
@@ -261,7 +261,7 @@ func handleWGSliverConnection(conn net.Conn) {
implantConn := core.NewImplantConnection("wg", conn.RemoteAddr().String())
defer func() {
wgLog.Debugf("wireguard connection closing")
implantConn.Cleanup()
implantConn.Close()
conn.Close()
}()
@@ -291,6 +291,11 @@ func (c *wgBufferedConn) Read(p []byte) (int, error) {
}
func handleWGSliverConnectionYamux(conn net.Conn, implantConn *core.ImplantConnection) {
handleWGSliverConnectionYamuxWithDispatch(conn, implantConn, serverHandlers.GetHandlers(), nil, nil)
}
//nolint:gocyclo // The transport loop keeps stream admission, shutdown, dispatch, and response handling together.
func handleWGSliverConnectionYamuxWithDispatch(conn net.Conn, implantConn *core.ImplantConnection, handlers map[uint32]serverHandlers.ServerHandler, beforeDispatch func(), afterStream func()) {
defer recoverAndLogPanic(wgLog.Errorf, "wireguard handleWGSliverConnectionYamux")
session, err := yamux.Server(conn, nil)
@@ -308,11 +313,16 @@ func handleWGSliverConnectionYamux(conn net.Conn, implantConn *core.ImplantConne
session.Close()
})
}
go func() {
select {
case <-implantConn.Done():
closeDone()
case <-done:
}
}()
streamSem := make(chan struct{}, mtlsYamuxMaxConcurrentStreams)
sendSem := make(chan struct{}, mtlsYamuxMaxConcurrentSends)
handlers := serverHandlers.GetHandlers()
go func() {
defer closeDone()
defer recoverAndLogPanic(wgLog.Errorf, "wireguard yamux accept loop")
@@ -336,6 +346,9 @@ func handleWGSliverConnectionYamux(conn net.Conn, implantConn *core.ImplantConne
defer func() {
<-streamSem
}()
if afterStream != nil {
defer afterStream()
}
defer recoverAndLogPanic(wgLog.Errorf, "wireguard yamux stream")
defer stream.Close()
@@ -345,25 +358,37 @@ func handleWGSliverConnectionYamux(conn net.Conn, implantConn *core.ImplantConne
closeDone()
return
}
if beforeDispatch != nil {
beforeDispatch()
}
select {
case <-implantConn.Done():
return
default:
}
implantConn.UpdateLastMessage()
if envelope.ID != 0 {
implantConn.RespMutex.RLock()
resp, ok := implantConn.Resp[envelope.ID]
implantConn.RespMutex.RUnlock()
if ok {
resp <- envelope
}
implantConn.DeliverResponse(envelope)
return
}
if handler, ok := handlers[envelope.Type]; ok {
go func(envelope *sliverpb.Envelope) {
defer recoverAndLogPanic(wgLog.Errorf, "wireguard message handler")
select {
case <-implantConn.Done():
return
case <-done:
return
default:
}
respEnvelope := handler(implantConn, envelope.Data)
if respEnvelope != nil {
implantConn.Send <- respEnvelope
if err := implantConn.SendEnvelope(respEnvelope, core.DefaultImplantSendTimeout); err != nil {
implantConn.Close()
}
}
}(envelope)
}
@@ -376,7 +401,19 @@ func handleWGSliverConnectionYamux(conn net.Conn, implantConn *core.ImplantConne
defer recoverAndLogPanic(wgLog.Errorf, "wireguard yamux sender loop")
for {
select {
case <-implantConn.Done():
return
case envelope := <-implantConn.Send:
select {
case <-implantConn.Done():
return
case <-done:
return
default:
}
if envelope == nil {
continue
}
select {
case sendSem <- struct{}{}:
case <-done:
+240 -5
View File
@@ -19,6 +19,7 @@ package core
*/
import (
"errors"
"sync"
"time"
@@ -26,6 +27,51 @@ import (
"github.com/gofrs/uuid"
)
// maxClaimedReverseTunnelIDsPerConnection bounds the permanent replay-defense
// history for one C2 connection. Reverse tunnel IDs cannot be reused safely
// because the wire protocol has no generation number. A connection that uses
// the entire history is therefore closed and must establish a fresh ID domain.
// The limit is deliberately much larger than the concurrent reverse-tunnel
// quota while still bounding attacker-controlled memory growth.
const (
maxClaimedReverseTunnelIDsPerConnection = 4096
// DefaultImplantSendTimeout bounds server producers when a transport has
// stopped consuming outbound envelopes without closing its connection yet.
DefaultImplantSendTimeout = 10 * time.Second
implantSendRetryInterval = time.Millisecond
)
var (
// ErrImplantConnectionClosed indicates that an outbound envelope could not
// be queued because the connection is closing or already closed.
ErrImplantConnectionClosed = errors.New("implant connection closed")
// ErrImplantSendTimeout indicates that an outbound transport queue did not
// accept an envelope before its bounded deadline.
ErrImplantSendTimeout = errors.New("implant send timeout")
// ErrInvalidImplantSend indicates a nil connection, envelope, or send queue.
ErrInvalidImplantSend = errors.New("invalid implant send")
)
// ReverseTunnelIDClaimResult distinguishes a replayed ID from exhaustion of
// the per-connection ID domain. Callers should reject duplicates while leaving
// the connection live; capacity exhaustion has already failed it closed.
type ReverseTunnelIDClaimResult uint8
const (
// ReverseTunnelIDClaimed indicates that the connection accepted a new ID.
ReverseTunnelIDClaimed ReverseTunnelIDClaimResult = iota
// ReverseTunnelIDDuplicate indicates that the connection already owns the ID.
ReverseTunnelIDDuplicate
// ReverseTunnelIDCapacityExhausted indicates that no more IDs may be claimed.
ReverseTunnelIDCapacityExhausted
// ReverseTunnelIDConnectionClosed indicates that the connection is closing.
ReverseTunnelIDConnectionClosed
)
// ImplantConnection - Abstract connection to an implant
type ImplantConnection struct {
ID string
@@ -36,7 +82,194 @@ type ImplantConnection struct {
RemoteAddress string
LastMessage time.Time
LastMessageMutex *sync.RWMutex
Cleanup func()
lifecycleOnce sync.Once
lifecycleMutex sync.Mutex
done chan struct{}
closed bool
cleanupSet bool
cleanup func()
claimedTunnelIDs map[uint64]struct{}
}
// TryClaimReverseTunnelID permanently reserves a wire tunnel ID for this C2
// connection. The protocol has no generation number, so never reusing an ID is
// what prevents a delayed frame from targeting a newer relay generation. Once
// the bounded ID domain is exhausted, the connection is failed closed after
// releasing lifecycleMutex so cleanup can safely re-enter connection methods.
func (c *ImplantConnection) TryClaimReverseTunnelID(tunnelID uint64) ReverseTunnelIDClaimResult {
if c == nil {
return ReverseTunnelIDConnectionClosed
}
c.initializeLifecycle()
c.lifecycleMutex.Lock()
if c.closed {
c.lifecycleMutex.Unlock()
return ReverseTunnelIDConnectionClosed
}
if c.claimedTunnelIDs == nil {
c.claimedTunnelIDs = map[uint64]struct{}{}
}
if _, claimed := c.claimedTunnelIDs[tunnelID]; claimed {
c.lifecycleMutex.Unlock()
return ReverseTunnelIDDuplicate
}
if len(c.claimedTunnelIDs) >= maxClaimedReverseTunnelIDsPerConnection {
c.lifecycleMutex.Unlock()
c.Close()
return ReverseTunnelIDCapacityExhausted
}
c.claimedTunnelIDs[tunnelID] = struct{}{}
c.lifecycleMutex.Unlock()
return ReverseTunnelIDClaimed
}
// ClaimReverseTunnelID is the compatibility boolean wrapper for callers that
// do not need to distinguish duplicate IDs from a closed ID domain.
func (c *ImplantConnection) ClaimReverseTunnelID(tunnelID uint64) bool {
return c.TryClaimReverseTunnelID(tunnelID) == ReverseTunnelIDClaimed
}
func (c *ImplantConnection) initializeLifecycle() {
c.lifecycleOnce.Do(func() {
c.done = make(chan struct{})
})
}
// Close marks the implant connection closed and runs its cleanup callback. The
// Done signal is closed first so blocked work can fail closed even if cleanup
// takes time. It is safe for transport, protocol, and rejection paths to call
// Close concurrently; cleanup is performed exactly once.
func (c *ImplantConnection) Close() {
c.initializeLifecycle()
c.lifecycleMutex.Lock()
if c.closed {
c.lifecycleMutex.Unlock()
return
}
c.closed = true
close(c.done)
cleanup := c.cleanup
c.cleanup = nil
c.lifecycleMutex.Unlock()
if cleanup != nil {
cleanup()
}
}
// Done is closed when the implant connection begins closing.
func (c *ImplantConnection) Done() <-chan struct{} {
c.initializeLifecycle()
return c.done
}
// SetCleanup installs the connection's one-shot cleanup callback. It returns
// false for a duplicate registration or a connection that already closed.
// Callers must install cleanup before publishing connection-owned state.
func (c *ImplantConnection) SetCleanup(callback func()) bool {
if callback == nil {
return false
}
c.initializeLifecycle()
c.lifecycleMutex.Lock()
if c.cleanupSet || c.closed {
c.lifecycleMutex.Unlock()
return false
}
c.cleanupSet = true
c.cleanup = callback
c.lifecycleMutex.Unlock()
return true
}
// SendEnvelope queues an outbound envelope while the connection remains live.
// Every producer must use this method instead of writing to Send directly so a
// stalled transport cannot strand the producer forever.
func (c *ImplantConnection) SendEnvelope(envelope *sliverpb.Envelope, timeout time.Duration) error {
return c.SendEnvelopeUntil(envelope, nil, timeout)
}
// SendEnvelopeUntil is SendEnvelope with an additional owner lifecycle. This
// is used by tunnel and pivot producers, where the narrower owner may close
// while the underlying C2 connection remains healthy.
func (c *ImplantConnection) SendEnvelopeUntil(envelope *sliverpb.Envelope, ownerDone <-chan struct{}, timeout time.Duration) error {
if c == nil || envelope == nil || c.Send == nil {
return ErrInvalidImplantSend
}
c.initializeLifecycle()
if sent, err := c.trySendEnvelope(envelope, ownerDone); sent || err != nil {
return err
}
if timeout <= 0 {
timeout = DefaultImplantSendTimeout
}
timer := time.NewTimer(timeout)
defer timer.Stop()
retry := time.NewTicker(implantSendRetryInterval)
defer retry.Stop()
for {
select {
case <-c.done:
return ErrImplantConnectionClosed
case <-ownerDone:
return ErrImplantConnectionClosed
case <-timer.C:
return ErrImplantSendTimeout
case <-retry.C:
}
if sent, err := c.trySendEnvelope(envelope, ownerDone); sent || err != nil {
return err
}
}
}
func (c *ImplantConnection) trySendEnvelope(envelope *sliverpb.Envelope, ownerDone <-chan struct{}) (bool, error) {
// Close and successful queueing share lifecycleMutex as their linearization
// point. The non-blocking send keeps Close responsive even when an
// unbuffered transport queue is not being consumed.
c.lifecycleMutex.Lock()
defer c.lifecycleMutex.Unlock()
if c.closed {
return false, ErrImplantConnectionClosed
}
select {
case <-ownerDone:
return false, ErrImplantConnectionClosed
default:
}
select {
case c.Send <- envelope:
return true, nil
default:
return false, nil
}
}
// DeliverResponse completes a pending synchronous request without ever
// blocking a transport reader. Request response channels are single-message
// buffers, so a duplicate or late delivery is safely rejected.
func (c *ImplantConnection) DeliverResponse(envelope *sliverpb.Envelope) bool {
if c == nil || envelope == nil || envelope.ID == 0 || c.RespMutex == nil || c.Resp == nil {
return false
}
c.initializeLifecycle()
c.lifecycleMutex.Lock()
defer c.lifecycleMutex.Unlock()
if c.closed {
return false
}
c.RespMutex.RLock()
response, ok := c.Resp[envelope.ID]
c.RespMutex.RUnlock()
if !ok || response == nil {
return false
}
select {
case response <- envelope:
return true
default:
return false
}
}
// GetLastMessage - Retrieves the last message time
@@ -57,7 +290,7 @@ func (c *ImplantConnection) UpdateLastMessage() {
// NewImplantConnection - Creates a new implant connection
func NewImplantConnection(transport string, remoteAddress string) *ImplantConnection {
return &ImplantConnection{
connection := &ImplantConnection{
ID: generateImplantConnectionID(),
Send: make(chan *sliverpb.Envelope),
RespMutex: &sync.RWMutex{},
@@ -65,8 +298,10 @@ func NewImplantConnection(transport string, remoteAddress string) *ImplantConnec
Resp: map[int64]chan *sliverpb.Envelope{},
Transport: transport,
RemoteAddress: remoteAddress,
Cleanup: func() {},
claimedTunnelIDs: map[uint64]struct{}{},
}
connection.initializeLifecycle()
return connection
}
func generateImplantConnectionID() string {
@@ -75,8 +310,8 @@ func generateImplantConnectionID() string {
}
func (c *ImplantConnection) RequestResend(data []byte) {
c.Send <- &sliverpb.Envelope{
_ = c.SendEnvelope(&sliverpb.Envelope{
Type: sliverpb.MsgTunnelData,
Data: data,
}
}, DefaultImplantSendTimeout)
}
+273
View File
@@ -0,0 +1,273 @@
package core
import (
"errors"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/bishopfox/sliver/protobuf/sliverpb"
)
func TestImplantConnectionSendEnvelopeTimesOutWhenTransportStalls(t *testing.T) {
connection := NewImplantConnection("test", "test")
err := connection.SendEnvelope(&sliverpb.Envelope{Type: sliverpb.MsgPing}, 20*time.Millisecond)
if !errors.Is(err, ErrImplantSendTimeout) {
t.Fatalf("SendEnvelope error = %v, want %v", err, ErrImplantSendTimeout)
}
}
func TestImplantConnectionSendEnvelopeUnblocksOnDisconnect(t *testing.T) {
connection := NewImplantConnection("test", "test")
connection.Send = make(chan *sliverpb.Envelope, 1)
queued := &sliverpb.Envelope{Type: sliverpb.MsgPing, Data: []byte("queued")}
connection.Send <- queued
result := make(chan error, 1)
started := make(chan struct{})
go func() {
close(started)
result <- connection.SendEnvelope(&sliverpb.Envelope{Type: sliverpb.MsgPing, Data: []byte("blocked")}, time.Hour)
}()
<-started
connection.Close()
select {
case err := <-result:
if !errors.Is(err, ErrImplantConnectionClosed) {
t.Fatalf("SendEnvelope error = %v, want %v", err, ErrImplantConnectionClosed)
}
case <-time.After(time.Second):
t.Fatal("SendEnvelope remained blocked after connection close")
}
if got := <-connection.Send; got != queued {
t.Fatalf("full queue was overwritten after disconnect: got %+v, want %+v", got, queued)
}
select {
case unexpected := <-connection.Send:
t.Fatalf("blocked envelope was queued after disconnect: %+v", unexpected)
default:
}
}
func TestImplantConnectionSendEnvelopeUnblocksOnOwnerClose(t *testing.T) {
connection := NewImplantConnection("test", "test")
ownerDone := make(chan struct{})
result := make(chan error, 1)
go func() {
result <- connection.SendEnvelopeUntil(&sliverpb.Envelope{Type: sliverpb.MsgPing}, ownerDone, time.Hour)
}()
close(ownerDone)
select {
case err := <-result:
if !errors.Is(err, ErrImplantConnectionClosed) {
t.Fatalf("SendEnvelopeUntil error = %v, want %v", err, ErrImplantConnectionClosed)
}
case <-time.After(time.Second):
t.Fatal("SendEnvelopeUntil remained blocked after owner close")
}
}
func TestImplantConnectionSendEnvelopeRejectsClosedConnection(t *testing.T) {
connection := NewImplantConnection("test", "test")
connection.Send = make(chan *sliverpb.Envelope, 1)
connection.Close()
err := connection.SendEnvelope(&sliverpb.Envelope{Type: sliverpb.MsgPing}, time.Second)
if !errors.Is(err, ErrImplantConnectionClosed) {
t.Fatalf("SendEnvelope error = %v, want %v", err, ErrImplantConnectionClosed)
}
select {
case envelope := <-connection.Send:
t.Fatalf("closed connection accepted envelope: %+v", envelope)
default:
}
}
func TestImplantConnectionDeliverResponseNeverBlocks(t *testing.T) {
connection := NewImplantConnection("test", "test")
response := make(chan *sliverpb.Envelope, 1)
connection.Resp[7] = response
if !connection.DeliverResponse(&sliverpb.Envelope{ID: 7, Data: []byte("first")}) {
t.Fatal("first response was not delivered")
}
if connection.DeliverResponse(&sliverpb.Envelope{ID: 7, Data: []byte("duplicate")}) {
t.Fatal("duplicate response was delivered to a full waiter")
}
if connection.DeliverResponse(&sliverpb.Envelope{ID: 8}) {
t.Fatal("response was delivered to an unknown waiter")
}
}
func TestImplantConnectionCloseIsIdempotent(t *testing.T) {
connection := NewImplantConnection("test", "test")
var cleanupCalls atomic.Int32
if !connection.SetCleanup(func() {
cleanupCalls.Add(1)
}) {
t.Fatal("failed to install cleanup")
}
select {
case <-connection.Done():
t.Fatal("new connection is already closed")
default:
}
const closers = 32
var waitGroup sync.WaitGroup
waitGroup.Add(closers)
for range closers {
go func() {
defer waitGroup.Done()
connection.Close()
}()
}
waitGroup.Wait()
select {
case <-connection.Done():
case <-time.After(time.Second):
t.Fatal("connection Done was not closed")
}
if got := cleanupCalls.Load(); got != 1 {
t.Fatalf("cleanup called %d times, want 1", got)
}
connection.Close()
if got := cleanupCalls.Load(); got != 1 {
t.Fatalf("cleanup called %d times after repeated Close, want 1", got)
}
}
func TestImplantConnectionDonePrecedesCleanup(t *testing.T) {
connection := NewImplantConnection("test", "test")
cleanupStarted := make(chan struct{})
releaseCleanup := make(chan struct{})
if !connection.SetCleanup(func() {
close(cleanupStarted)
<-releaseCleanup
}) {
t.Fatal("failed to install cleanup")
}
go connection.Close()
select {
case <-cleanupStarted:
case <-time.After(time.Second):
t.Fatal("cleanup did not start")
}
select {
case <-connection.Done():
case <-time.After(time.Second):
t.Fatal("Done did not close while cleanup was blocked")
}
close(releaseCleanup)
}
func TestImplantConnectionZeroValueCloseWithNilCleanup(t *testing.T) {
connection := &ImplantConnection{}
connection.Close()
select {
case <-connection.Done():
case <-time.After(time.Second):
t.Fatal("zero-value connection Done was not closed")
}
connection.Close()
}
func TestImplantConnectionCleanupRegistrationIsOneShotAndClosedAware(t *testing.T) {
connection := NewImplantConnection("test", "test")
var firstCalls atomic.Int32
var duplicateCalls atomic.Int32
if !connection.SetCleanup(func() { firstCalls.Add(1) }) {
t.Fatal("first cleanup registration was rejected")
}
if connection.SetCleanup(func() { duplicateCalls.Add(1) }) {
t.Fatal("duplicate cleanup registration was accepted")
}
connection.Close()
if got := firstCalls.Load(); got != 1 {
t.Fatalf("first cleanup called %d times, want 1", got)
}
if got := duplicateCalls.Load(); got != 0 {
t.Fatalf("duplicate cleanup called %d times, want 0", got)
}
closed := NewImplantConnection("test", "test")
closed.Close()
if closed.SetCleanup(func() { t.Error("cleanup ran after closed registration") }) {
t.Fatal("cleanup registration succeeded after Close")
}
}
func TestImplantConnectionNeverReusesReverseTunnelID(t *testing.T) {
connection := NewImplantConnection("test", "test")
if result := connection.TryClaimReverseTunnelID(42); result != ReverseTunnelIDClaimed {
t.Fatalf("first tunnel ID claim result = %v, want %v", result, ReverseTunnelIDClaimed)
}
if result := connection.TryClaimReverseTunnelID(42); result != ReverseTunnelIDDuplicate {
t.Fatalf("reused tunnel ID claim result = %v, want %v", result, ReverseTunnelIDDuplicate)
}
connection.Close()
if result := connection.TryClaimReverseTunnelID(43); result != ReverseTunnelIDConnectionClosed {
t.Fatalf("closed connection claim result = %v, want %v", result, ReverseTunnelIDConnectionClosed)
}
}
func TestImplantConnectionClaimedReverseTunnelIDCapacityFailsClosed(t *testing.T) {
connection := NewImplantConnection("test", "test")
cleanupCalled := make(chan struct{})
if !connection.SetCleanup(func() {
// Re-entering a lifecycle method proves the exhaustion path does not
// invoke Close while holding lifecycleMutex.
if result := connection.TryClaimReverseTunnelID(0); result != ReverseTunnelIDConnectionClosed {
t.Errorf("cleanup reentrant claim result = %v, want %v", result, ReverseTunnelIDConnectionClosed)
}
close(cleanupCalled)
}) {
t.Fatal("failed to install cleanup")
}
for tunnelID := uint64(0); tunnelID < maxClaimedReverseTunnelIDsPerConnection; tunnelID++ {
if result := connection.TryClaimReverseTunnelID(tunnelID); result != ReverseTunnelIDClaimed {
t.Fatalf("claim %d result = %v, want %v", tunnelID, result, ReverseTunnelIDClaimed)
}
}
// Duplicate detection takes precedence over capacity, so an ordinary replay
// remains distinguishable and does not close the connection.
if result := connection.TryClaimReverseTunnelID(0); result != ReverseTunnelIDDuplicate {
t.Fatalf("duplicate-at-capacity result = %v, want %v", result, ReverseTunnelIDDuplicate)
}
select {
case <-connection.Done():
t.Fatal("duplicate-at-capacity closed the connection")
default:
}
result := make(chan ReverseTunnelIDClaimResult, 1)
go func() {
result <- connection.TryClaimReverseTunnelID(maxClaimedReverseTunnelIDsPerConnection)
}()
select {
case claimResult := <-result:
if claimResult != ReverseTunnelIDCapacityExhausted {
t.Fatalf("capacity exhaustion result = %v, want %v", claimResult, ReverseTunnelIDCapacityExhausted)
}
case <-time.After(time.Second):
t.Fatal("capacity exhaustion deadlocked")
}
select {
case <-cleanupCalled:
case <-time.After(time.Second):
t.Fatal("capacity exhaustion deadlocked before cleanup completed")
}
select {
case <-connection.Done():
default:
t.Fatal("capacity exhaustion did not fail the connection closed")
}
}
+58 -3
View File
@@ -36,6 +36,41 @@ var (
PivotSessions = &sync.Map{} // ID -> Pivot
)
// ClosePivotSession atomically removes and closes a synthetic pivot
// connection. It also handles key exchanges that created a pivot before the
// downstream implant registered a core Session.
func ClosePivotSession(pivotID string) bool {
value, ok := PivotSessions.Load(pivotID)
if !ok {
return false
}
pivot, ok := value.(*Pivot)
if !ok || pivot == nil || !PivotSessions.CompareAndDelete(pivotID, value) {
return false
}
if pivot.ImplantConn != nil {
pivot.ImplantConn.Close()
}
return true
}
func closePivotForConnection(connection *ImplantConnection) {
if connection == nil {
return
}
PivotSessions.Range(func(key, value any) bool {
pivot, ok := value.(*Pivot)
if !ok || pivot == nil || pivot.ImplantConn != connection {
return true
}
pivotID, ok := key.(string)
if ok {
ClosePivotSession(pivotID)
}
return false
})
}
// Pivot - Wraps an ImplantConnection
type Pivot struct {
ID string
@@ -51,9 +86,27 @@ type Pivot struct {
func (p *Pivot) Start() {
go func() {
defer func() {
PivotSessions.CompareAndDelete(p.ID, p)
if p.ImplantConn != nil {
p.ImplantConn.Close()
}
coreLog.Debugf("pivot session %s send loop closing (origin id: %d)", p.ID, p.OriginID)
}()
for envelope := range p.ImplantConn.Send {
if p.ImplantConn == nil || p.ImmediateImplantConn == nil || p.CipherCtx == nil {
return
}
for {
var envelope *sliverpb.Envelope
select {
case <-p.ImplantConn.Done():
return
case <-p.ImmediateImplantConn.Done():
return
case envelope = <-p.ImplantConn.Send:
}
if envelope == nil {
continue
}
envelopeData, err := proto.Marshal(envelope)
if err != nil {
coreLog.Errorf("failed to marshal envelope: %v", err)
@@ -64,7 +117,7 @@ func (p *Pivot) Start() {
coreLog.Errorf("failed to encrypt envelope: %v", err)
continue
}
peerEnvelopeData, _ := proto.Marshal(&sliverpb.PivotPeerEnvelope{
peerEnvelopeData, err := proto.Marshal(&sliverpb.PivotPeerEnvelope{
Type: envelope.Type,
Peers: p.Peers,
Data: ciphertext,
@@ -73,9 +126,11 @@ func (p *Pivot) Start() {
coreLog.Errorf("failed to wrap pivot peer envelope: %v", err)
continue
}
p.ImmediateImplantConn.Send <- &sliverpb.Envelope{
if err := p.ImmediateImplantConn.SendEnvelopeUntil(&sliverpb.Envelope{
Type: sliverpb.MsgPivotPeerEnvelope,
Data: peerEnvelopeData,
}, p.ImplantConn.Done(), DefaultImplantSendTimeout); err != nil {
return
}
}
}()
+80
View File
@@ -0,0 +1,80 @@
package core
import (
"testing"
"time"
"github.com/bishopfox/sliver/protobuf/sliverpb"
"github.com/bishopfox/sliver/server/cryptography"
)
func TestPivotStartStopsWhenEitherConnectionCloses(t *testing.T) {
tests := []struct {
name string
close func(*Pivot)
wantImmediateConnection bool
}{
{name: "pivot", close: func(pivot *Pivot) { pivot.ImplantConn.Close() }, wantImmediateConnection: true},
{name: "immediate", close: func(pivot *Pivot) { pivot.ImmediateImplantConn.Close() }},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
pivot := NewPivotSession([]*sliverpb.PivotPeer{{PeerID: 1}, {PeerID: 2}})
pivot.ImplantConn = NewImplantConnection(PivotTransportName, "pivot")
pivot.ImmediateImplantConn = NewImplantConnection("mtls", "immediate")
pivot.CipherCtx = cryptography.NewCipherContext(cryptography.RandomSymmetricKey())
PivotSessions.Store(pivot.ID, pivot)
t.Cleanup(func() {
pivot.ImplantConn.Close()
pivot.ImmediateImplantConn.Close()
PivotSessions.CompareAndDelete(pivot.ID, pivot)
})
pivot.Start()
// Exercise the guarded forwarding send without leaving an unbounded
// producer behind if the pivot shuts down first.
enqueued := make(chan bool, 1)
go func() {
select {
case pivot.ImplantConn.Send <- &sliverpb.Envelope{Type: sliverpb.MsgPing}:
enqueued <- true
case <-pivot.ImplantConn.Done():
enqueued <- false
}
}()
select {
case sent := <-enqueued:
if !sent {
t.Fatal("pivot closed before the send loop received the envelope")
}
case <-time.After(time.Second):
t.Fatal("pivot send loop did not receive envelope")
}
test.close(pivot)
select {
case <-pivot.ImplantConn.Done():
case <-time.After(time.Second):
t.Fatal("pivot connection survived lifecycle close")
}
select {
case <-pivot.ImmediateImplantConn.Done():
if test.wantImmediateConnection {
t.Fatal("closing a synthetic pivot closed its shared immediate transport connection")
}
default:
if !test.wantImmediateConnection {
t.Fatal("immediate transport connection did not close")
}
}
deadline := time.Now().Add(time.Second)
for time.Now().Before(deadline) {
if _, ok := PivotSessions.Load(pivot.ID); !ok {
return
}
time.Sleep(time.Millisecond)
}
t.Fatal("pivot map entry survived lifecycle close")
})
}
}
+11 -2
View File
@@ -2,8 +2,17 @@
## Overview
Reverse tunnel coordination within the server core. Handles tunnel registration, negotiation, and multiplexing.
Reverse tunnel coordination within the server core. Handles tunnel registration,
server-owned reverse-port-forward authorization, bounded outbound connection
brokering, and multiplexing.
Reverse port forward destinations are immutable operator inputs stored in a
`Registry`. Implant messages carry an opaque `AuthorizationID`; only `Broker`
may turn that ID into an outbound TCP connection. Legacy implant addresses are
accepted solely as canonical registry lookup keys and are never dialed directly.
## Go Files
- `rtunnels.go` – Manages reverse tunnel state and orchestrates tunnel handshakes.
- `authorization.go` – Owns authorization lifecycle and server-authoritative metadata.
- `broker.go` – Opens bounded, revocation-aware connections from stored dial plans.
- `rtunnels.go` – Manages active reverse tunnel state and lifecycle cleanup.
+641
View File
@@ -0,0 +1,641 @@
// Package rtunnels owns reverse-port-forward authorization and relay state.
package rtunnels
import (
"context"
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"io"
"net"
"net/netip"
"sort"
"strconv"
"strings"
"sync"
"unicode"
)
const (
authorizationIDBytes = 32
maxAuthorizationIDAttempts = 16
maxConnectionsPerAuthorization = 64
maxConnectionsPerSession = 256
maxConnectionsGlobal = 2048
)
// ErrInvalidSessionID and the related errors report rejected authorizations.
var (
ErrInvalidSessionID = errors.New("invalid reverse port forward session ID")
ErrInvalidForwardAddress = errors.New("invalid reverse port forward address")
ErrUnknownAuthorization = errors.New("unknown reverse port forward authorization")
ErrAuthorizationSession = errors.New("reverse port forward authorization belongs to another session")
ErrAuthorizationRevoked = errors.New("reverse port forward authorization is revoked")
ErrAuthorizationActive = errors.New("reverse port forward authorization is already active")
ErrAuthorizationIDRequired = errors.New("reverse port forward authorization ID is required")
ErrAuthorizationConnectionLimit = errors.New("reverse port forward authorization connection limit reached")
ErrSessionConnectionLimit = errors.New("reverse port forward session connection limit reached")
ErrGlobalConnectionLimit = errors.New("reverse port forward global connection limit reached")
ErrAuthorizationReservation = errors.New("invalid reverse port forward connection reservation")
ErrDuplicateListenerID = errors.New("reverse port forward listener ID is already registered")
ErrAuthorizationIDGeneration = errors.New("failed to generate a unique reverse port forward authorization ID")
)
// AuthorizationID is an opaque, teamserver-generated capability identifying a
// reverse port forward authorization. Implants must not be able to choose it.
type AuthorizationID string
func (id AuthorizationID) String() string {
return string(id)
}
// AuthorizationState describes the server-owned lifecycle of a reverse port
// forward listener authorization.
type AuthorizationState uint8
// AuthorizationStarting and the related states describe authorization lifecycle.
const (
AuthorizationStarting AuthorizationState = iota + 1
AuthorizationActive
AuthorizationRevoked
)
func (state AuthorizationState) String() string {
switch state {
case AuthorizationStarting:
return "starting"
case AuthorizationActive:
return "active"
case AuthorizationRevoked:
return "revoked"
default:
return "unknown"
}
}
// Authorization is an immutable snapshot of a server-owned reverse port
// forward authorization. Mutating a snapshot cannot affect the registry.
type Authorization struct {
AuthorizationID AuthorizationID
SessionID string
BindAddress string
Address string
KeepAlive int32
State AuthorizationState
ImplantListenerID uint32
HasListenerID bool
RequiresAuthorizationID bool
}
// dialPlan is destination data private to the registry and broker. Keeping it
// unexported prevents implant handlers from receiving an address they could
// accidentally redial outside the broker.
type dialPlan struct {
AuthorizationID AuthorizationID
Address string
KeepAlive int32
}
type authorizationRecord struct {
authorization Authorization
sequence uint64
revoked context.Context
cancel context.CancelFunc
connections map[uint64]net.Conn
pendingConnections uint64
nextConnID uint64
}
// connectionReservation is a once-consumable claim on all connection quota
// scopes. It keeps the owning record alive after revocation removes registry
// indexes, so late dial completion can release safely without looking up a
// newly-created authorization that happens to reuse the same opaque ID.
// Every field is protected by Registry.mu.
type connectionReservation struct {
record *authorizationRecord
active bool
}
// Registry owns all reverse port forward authorizations. Its indexes contain
// only server-generated IDs and destinations derived from operator requests.
type Registry struct {
mu sync.RWMutex
random io.Reader
byID map[AuthorizationID]*authorizationRecord
bySession map[string]map[AuthorizationID]*authorizationRecord
byDestination map[string]map[AuthorizationID]*authorizationRecord
byListener map[string]map[uint32]*authorizationRecord
sessionConnectionCounts map[string]uint64
totalConnectionCount uint64
nextSequence uint64
}
// NewRegistry creates an empty reverse port forward authorization registry.
func NewRegistry() *Registry {
return newRegistry(rand.Reader)
}
func newRegistry(random io.Reader) *Registry {
return &Registry{
random: random,
byID: map[AuthorizationID]*authorizationRecord{},
bySession: map[string]map[AuthorizationID]*authorizationRecord{},
byDestination: map[string]map[AuthorizationID]*authorizationRecord{},
byListener: map[string]map[uint32]*authorizationRecord{},
sessionConnectionCounts: map[string]uint64{},
}
}
// canonicalizeAddress validates and canonicalizes a TCP host:port address. It
// normalizes IP literals, hostname case, and IPv6 bracket formatting so legacy
// requests can only select an already-authorized operator destination.
func canonicalizeAddress(address string) (string, error) {
address = strings.TrimSpace(address)
host, portString, err := net.SplitHostPort(address)
if err != nil {
return "", fmt.Errorf("%w: %v", ErrInvalidForwardAddress, err)
}
if host == "" {
return "", fmt.Errorf("%w: host is empty", ErrInvalidForwardAddress)
}
port, err := strconv.ParseUint(portString, 10, 16)
if err != nil || port == 0 {
return "", fmt.Errorf("%w: port must be between 1 and 65535", ErrInvalidForwardAddress)
}
host, err = canonicalHost(host)
if err != nil {
return "", err
}
return net.JoinHostPort(host, strconv.FormatUint(port, 10)), nil
}
func canonicalHost(host string) (string, error) {
if ip, err := netip.ParseAddr(host); err == nil {
return ip.Unmap().String(), nil
}
host = strings.TrimSuffix(strings.ToLower(host), ".")
if host == "" || len(host) > 253 {
return "", fmt.Errorf("%w: invalid hostname", ErrInvalidForwardAddress)
}
for _, label := range strings.Split(host, ".") {
if label == "" || len(label) > 63 || label[0] == '-' || label[len(label)-1] == '-' {
return "", fmt.Errorf("%w: invalid hostname", ErrInvalidForwardAddress)
}
for _, char := range label {
if unicode.IsLetter(char) || unicode.IsDigit(char) || char == '-' || char == '_' {
continue
}
return "", fmt.Errorf("%w: invalid hostname", ErrInvalidForwardAddress)
}
}
return host, nil
}
// Begin creates a Starting authorization for an operator-selected destination.
// Prefer BeginSpec when the operator's bind address is available.
func (registry *Registry) Begin(sessionID string, address string, keepAlive int32) (AuthorizationID, error) {
return registry.BeginSpec(sessionID, "", address, keepAlive)
}
// BeginSpec creates a Starting authorization before the start request is sent
// to the implant. The destination and bind address are immutable thereafter.
func (registry *Registry) BeginSpec(sessionID string, bindAddress string, address string, keepAlive int32) (AuthorizationID, error) {
if sessionID == "" {
return "", ErrInvalidSessionID
}
canonicalAddress, err := canonicalizeAddress(address)
if err != nil {
return "", err
}
registry.mu.Lock()
defer registry.mu.Unlock()
registry.initializeLocked()
id, err := registry.newAuthorizationIDLocked()
if err != nil {
return "", err
}
revoked, cancel := context.WithCancel(context.Background())
registry.nextSequence++
record := &authorizationRecord{
authorization: Authorization{
AuthorizationID: id,
SessionID: sessionID,
BindAddress: bindAddress,
Address: canonicalAddress,
KeepAlive: keepAlive,
State: AuthorizationStarting,
},
sequence: registry.nextSequence,
revoked: revoked,
cancel: cancel,
connections: map[uint64]net.Conn{},
}
registry.byID[id] = record
if registry.bySession[sessionID] == nil {
registry.bySession[sessionID] = map[AuthorizationID]*authorizationRecord{}
}
registry.bySession[sessionID][id] = record
if registry.byDestination[sessionID] == nil {
registry.byDestination[sessionID] = map[AuthorizationID]*authorizationRecord{}
}
registry.byDestination[sessionID][id] = record
return id, nil
}
// Activate binds an implant listener ID to a Starting authorization. A
// duplicate implant-selected listener ID never replaces or orphans the existing
// record: the rejected candidate is revoked atomically.
func (registry *Registry) Activate(sessionID string, id AuthorizationID, listenerID uint32) error {
return registry.ActivateProtocol(sessionID, id, listenerID, false)
}
// ActivateProtocol binds an implant listener ID and records whether the
// implant proved support for authorization IDs by echoing the issued ID in its
// start response. Legacy address lookup is disabled for capable listeners, so
// a new-implant protocol regression fails closed while older implants retain
// exact-address compatibility.
func (registry *Registry) ActivateProtocol(sessionID string, id AuthorizationID, listenerID uint32, requiresAuthorizationID bool) error {
registry.mu.Lock()
registry.initializeLocked()
record, err := registry.lookupLocked(sessionID, id)
if err != nil {
registry.mu.Unlock()
return err
}
if record.authorization.State == AuthorizationRevoked {
registry.mu.Unlock()
return ErrAuthorizationRevoked
}
if record.authorization.State == AuthorizationActive {
if record.authorization.HasListenerID && record.authorization.ImplantListenerID == listenerID {
registry.mu.Unlock()
return nil
}
registry.mu.Unlock()
return ErrAuthorizationActive
}
if registry.byListener[sessionID] == nil {
registry.byListener[sessionID] = map[uint32]*authorizationRecord{}
}
if existing := registry.byListener[sessionID][listenerID]; existing != nil && existing != record && existing.authorization.State != AuthorizationRevoked {
connections := registry.revokeLocked(record)
registry.mu.Unlock()
closeConnections(connections)
return ErrDuplicateListenerID
}
record.authorization.State = AuthorizationActive
record.authorization.ImplantListenerID = listenerID
record.authorization.HasListenerID = true
record.authorization.RequiresAuthorizationID = requiresAuthorizationID
registry.byListener[sessionID][listenerID] = record
registry.mu.Unlock()
return nil
}
func dialPlanForRecord(record *authorizationRecord) (dialPlan, error) {
if record.authorization.State == AuthorizationRevoked {
return dialPlan{}, ErrAuthorizationRevoked
}
return dialPlan{
AuthorizationID: record.authorization.AuthorizationID,
Address: record.authorization.Address,
KeepAlive: record.authorization.KeepAlive,
}, nil
}
// Lookup returns an immutable snapshot for an authorization.
func (registry *Registry) Lookup(sessionID string, id AuthorizationID) (Authorization, bool) {
registry.mu.RLock()
defer registry.mu.RUnlock()
record, err := registry.lookupLocked(sessionID, id)
if err != nil {
return Authorization{}, false
}
return record.authorization, true
}
// LookupListener returns an immutable snapshot for an implant listener ID.
func (registry *Registry) LookupListener(sessionID string, listenerID uint32) (Authorization, bool) {
registry.mu.RLock()
defer registry.mu.RUnlock()
record := registry.byListener[sessionID][listenerID]
if record == nil || record.authorization.State == AuthorizationRevoked {
return Authorization{}, false
}
return record.authorization, true
}
// List returns server-authoritative Starting and Active listeners for a session.
func (registry *Registry) List(sessionID string) []Authorization {
registry.mu.RLock()
defer registry.mu.RUnlock()
records := registry.bySession[sessionID]
authorizations := make([]Authorization, 0, len(records))
for _, record := range records {
if record.authorization.State != AuthorizationRevoked {
authorizations = append(authorizations, record.authorization)
}
}
sort.Slice(authorizations, func(i, j int) bool {
return authorizations[i].AuthorizationID < authorizations[j].AuthorizationID
})
return authorizations
}
// Revoke prevents new acquisitions and closes pending or active broker-owned
// connections for an authorization. It is idempotent.
func (registry *Registry) Revoke(sessionID string, id AuthorizationID) bool {
registry.mu.Lock()
record, err := registry.lookupLocked(sessionID, id)
if err != nil || record.authorization.State == AuthorizationRevoked {
registry.mu.Unlock()
return false
}
connections := registry.revokeLocked(record)
registry.mu.Unlock()
closeConnections(connections)
return true
}
// RevokeListener revokes by the implant's listener ID. It does not depend on a
// successful implant stop response and is idempotent.
func (registry *Registry) RevokeListener(sessionID string, listenerID uint32) bool {
registry.mu.Lock()
record := registry.byListener[sessionID][listenerID]
if record == nil || record.authorization.State == AuthorizationRevoked {
registry.mu.Unlock()
return false
}
connections := registry.revokeLocked(record)
registry.mu.Unlock()
closeConnections(connections)
return true
}
// RevokeSession revokes every authorization for a disconnected session, closes
// its broker-owned connections, and removes authorization tombstones. It is
// idempotent and never affects another session.
func (registry *Registry) RevokeSession(sessionID string) int {
registry.mu.Lock()
records := registry.bySession[sessionID]
connections := make([]net.Conn, 0)
revoked := 0
for id, record := range records {
if record.authorization.State != AuthorizationRevoked {
revoked++
connections = append(connections, registry.revokeLocked(record)...)
}
delete(registry.byID, id)
}
delete(registry.bySession, sessionID)
delete(registry.byDestination, sessionID)
delete(registry.byListener, sessionID)
registry.mu.Unlock()
closeConnections(connections)
return revoked
}
func (registry *Registry) lookupLocked(sessionID string, id AuthorizationID) (*authorizationRecord, error) {
if id == "" {
return nil, ErrUnknownAuthorization
}
record := registry.byID[id]
if record == nil {
return nil, ErrUnknownAuthorization
}
if record.authorization.SessionID != sessionID {
return nil, ErrAuthorizationSession
}
return record, nil
}
func (registry *Registry) latestDestinationLocked(sessionID string, address string) *authorizationRecord {
var latest *authorizationRecord
for _, record := range registry.byDestination[sessionID] {
if record.authorization.Address != address || record.authorization.State == AuthorizationRevoked {
continue
}
if latest == nil || latest.sequence < record.sequence {
latest = record
}
}
return latest
}
func (registry *Registry) revokeLocked(record *authorizationRecord) []net.Conn {
if record.authorization.State == AuthorizationRevoked {
return nil
}
record.authorization.State = AuthorizationRevoked
record.cancel()
sessionID := record.authorization.SessionID
authorizationID := record.authorization.AuthorizationID
connectionCount := record.pendingConnections + uint64(len(record.connections))
record.pendingConnections = 0
registry.releaseConnectionCountLocked(sessionID, connectionCount)
if record.authorization.HasListenerID {
if listeners := registry.byListener[sessionID]; listeners != nil {
if listeners[record.authorization.ImplantListenerID] == record {
delete(listeners, record.authorization.ImplantListenerID)
}
if len(listeners) == 0 {
delete(registry.byListener, sessionID)
}
}
}
if destinations := registry.byDestination[sessionID]; destinations != nil {
delete(destinations, authorizationID)
if len(destinations) == 0 {
delete(registry.byDestination, sessionID)
}
}
delete(registry.byID, authorizationID)
if sessionAuthorizations := registry.bySession[sessionID]; sessionAuthorizations != nil {
delete(sessionAuthorizations, authorizationID)
if len(sessionAuthorizations) == 0 {
delete(registry.bySession, sessionID)
}
}
connections := make([]net.Conn, 0, len(record.connections))
for connectionID, connection := range record.connections {
connections = append(connections, connection)
delete(record.connections, connectionID)
}
return connections
}
func closeConnections(connections []net.Conn) {
for _, connection := range connections {
_ = connection.Close()
}
}
func (registry *Registry) newAuthorizationIDLocked() (AuthorizationID, error) {
buffer := make([]byte, authorizationIDBytes)
for range maxAuthorizationIDAttempts {
if _, err := io.ReadFull(registry.random, buffer); err != nil {
return "", fmt.Errorf("%w: %v", ErrAuthorizationIDGeneration, err)
}
id := AuthorizationID(base64.RawURLEncoding.EncodeToString(buffer))
if registry.byID[id] == nil {
return id, nil
}
}
return "", ErrAuthorizationIDGeneration
}
func (registry *Registry) initializeLocked() {
if registry.random == nil {
registry.random = rand.Reader
}
if registry.byID == nil {
registry.byID = map[AuthorizationID]*authorizationRecord{}
}
if registry.bySession == nil {
registry.bySession = map[string]map[AuthorizationID]*authorizationRecord{}
}
if registry.byDestination == nil {
registry.byDestination = map[string]map[AuthorizationID]*authorizationRecord{}
}
if registry.byListener == nil {
registry.byListener = map[string]map[uint32]*authorizationRecord{}
}
if registry.sessionConnectionCounts == nil {
registry.sessionConnectionCounts = map[string]uint64{}
}
}
func (registry *Registry) reserve(sessionID string, id AuthorizationID, legacyAddress string) (dialPlan, context.Context, *connectionReservation, error) {
registry.mu.Lock()
defer registry.mu.Unlock()
var record *authorizationRecord
var err error
if id != "" {
record, err = registry.lookupLocked(sessionID, id)
} else {
canonicalAddress, canonicalErr := canonicalizeAddress(legacyAddress)
if canonicalErr != nil {
return dialPlan{}, nil, nil, canonicalErr
}
record = registry.latestDestinationLocked(sessionID, canonicalAddress)
if record == nil {
err = ErrUnknownAuthorization
} else if record.authorization.RequiresAuthorizationID {
err = ErrAuthorizationIDRequired
}
}
if err != nil {
return dialPlan{}, nil, nil, err
}
if record.pendingConnections+uint64(len(record.connections)) >= maxConnectionsPerAuthorization {
return dialPlan{}, nil, nil, ErrAuthorizationConnectionLimit
}
if registry.sessionConnectionCounts[sessionID] >= maxConnectionsPerSession {
return dialPlan{}, nil, nil, ErrSessionConnectionLimit
}
if registry.totalConnectionCount >= maxConnectionsGlobal {
return dialPlan{}, nil, nil, ErrGlobalConnectionLimit
}
plan, err := dialPlanForRecord(record)
if err != nil {
return dialPlan{}, nil, nil, err
}
record.pendingConnections++
registry.sessionConnectionCounts[sessionID]++
registry.totalConnectionCount++
reservation := &connectionReservation{record: record, active: true}
return plan, record.revoked, reservation, nil
}
func (registry *Registry) registerConnection(sessionID string, id AuthorizationID, reservation *connectionReservation, connection net.Conn) (uint64, error) {
registry.mu.Lock()
defer registry.mu.Unlock()
if reservation == nil || !reservation.active || reservation.record == nil {
return 0, ErrAuthorizationReservation
}
record, err := registry.lookupLocked(sessionID, id)
if err != nil {
return 0, err
}
if record != reservation.record || record.pendingConnections == 0 {
return 0, ErrAuthorizationReservation
}
if record.authorization.State == AuthorizationRevoked {
return 0, ErrAuthorizationRevoked
}
reservation.active = false
record.pendingConnections--
record.nextConnID++
record.connections[record.nextConnID] = connection
return record.nextConnID, nil
}
func (registry *Registry) releaseReservation(reservation *connectionReservation) {
registry.mu.Lock()
defer registry.mu.Unlock()
if reservation == nil || !reservation.active || reservation.record == nil {
return
}
reservation.active = false
record := reservation.record
if record.authorization.State == AuthorizationRevoked {
return
}
if record.pendingConnections == 0 {
return
}
record.pendingConnections--
registry.releaseConnectionCountLocked(record.authorization.SessionID, 1)
}
func (registry *Registry) unregisterConnection(record *authorizationRecord, connectionID uint64) {
registry.mu.Lock()
defer registry.mu.Unlock()
if record == nil {
return
}
if _, ok := record.connections[connectionID]; !ok {
return
}
delete(record.connections, connectionID)
registry.releaseConnectionCountLocked(record.authorization.SessionID, 1)
}
func (registry *Registry) releaseConnectionCountLocked(sessionID string, count uint64) {
if count == 0 {
return
}
sessionCount := registry.sessionConnectionCounts[sessionID]
if count > sessionCount {
count = sessionCount
}
if count == 0 {
return
}
if count >= sessionCount {
delete(registry.sessionConnectionCounts, sessionID)
} else {
registry.sessionConnectionCounts[sessionID] = sessionCount - count
}
if count > registry.totalConnectionCount {
count = registry.totalConnectionCount
}
if count >= registry.totalConnectionCount {
registry.totalConnectionCount = 0
} else {
registry.totalConnectionCount -= count
}
}
+520
View File
@@ -0,0 +1,520 @@
package rtunnels
import (
"bytes"
"errors"
"io"
"net"
"strings"
"sync"
"testing"
"github.com/stretchr/testify/assert"
)
func TestCanonicalizeAddress(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input string
expected string
}{
{name: "IPv4", input: "127.0.0.1:8080", expected: "127.0.0.1:8080"},
{name: "mapped IPv4", input: "[::ffff:127.0.0.1]:080", expected: "127.0.0.1:80"},
{name: "IPv6", input: "[2001:0db8::1]:443", expected: "[2001:db8::1]:443"},
{name: "IPv6 zone", input: "[fe80::1%en0]:22", expected: "[fe80::1%en0]:22"},
{name: "hostname", input: "Example.COM.:00443", expected: "example.com:443"},
{name: "trim", input: " localhost:80 ", expected: "localhost:80"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
actual, err := canonicalizeAddress(test.input)
must.NoError(t, err)
assert.Equal(t, test.expected, actual)
})
}
}
func TestCanonicalizeAddressRejectsInvalidTargets(t *testing.T) {
t.Parallel()
for _, address := range []string{
"",
"localhost",
":80",
"localhost:0",
"localhost:65536",
"localhost:http",
"bad host:80",
"-invalid.example:80",
"invalid..example:80",
"127.0.0.1:80/path",
} {
t.Run(address, func(t *testing.T) {
_, err := canonicalizeAddress(address)
must.ErrorIs(t, err, ErrInvalidForwardAddress)
})
}
}
func TestRegistryBeginSpecCreatesImmutableStartingAuthorization(t *testing.T) {
t.Parallel()
random := bytes.NewReader(bytes.Repeat([]byte{0x42}, authorizationIDBytes))
registry := newRegistry(random)
id, err := registry.BeginSpec("session-a", "0.0.0.0:8443", "Example.COM.:080", 47)
must.NoError(t, err)
must.NotEmpty(t, id)
assert.NotContains(t, id.String(), "+")
assert.NotContains(t, id.String(), "/")
authorization, ok := registry.Lookup("session-a", id)
must.True(t, ok)
assert.Equal(t, AuthorizationStarting, authorization.State)
assert.Equal(t, "0.0.0.0:8443", authorization.BindAddress)
assert.Equal(t, "example.com:80", authorization.Address)
assert.Equal(t, int32(47), authorization.KeepAlive)
assert.False(t, authorization.HasListenerID)
// Snapshots and dial plans are values. Mutating either must not alter the
// registry's immutable operator-derived destination.
authorization.Address = "127.0.0.1:1"
plan, err := acquirePlanForTest(registry, "session-a", id)
must.NoError(t, err)
plan.Address = "127.0.0.1:2"
plan, err = acquirePlanForTest(registry, "session-a", id)
must.NoError(t, err)
assert.Equal(t, "example.com:80", plan.Address)
}
func TestRegistryBeginValidationAndRandomFailure(t *testing.T) {
t.Parallel()
registry := NewRegistry()
_, err := registry.Begin("", "127.0.0.1:80", 0)
must.ErrorIs(t, err, ErrInvalidSessionID)
_, err = registry.Begin("session", "127.0.0.1", 0)
must.ErrorIs(t, err, ErrInvalidForwardAddress)
registry = newRegistry(errorReader{})
_, err = registry.Begin("session", "127.0.0.1:80", 0)
must.ErrorIs(t, err, ErrAuthorizationIDGeneration)
}
func TestRegistryActivationDuplicateListenerFailsClosed(t *testing.T) {
t.Parallel()
registry := NewRegistry()
firstID, err := registry.Begin("session", "127.0.0.1:8001", 10)
must.NoError(t, err)
must.NoError(t, registry.Activate("session", firstID, 7))
must.NoError(t, registry.Activate("session", firstID, 7), "same transition is idempotent")
secondID, err := registry.Begin("session", "127.0.0.1:8002", 20)
must.NoError(t, err)
err = registry.Activate("session", secondID, 7)
must.ErrorIs(t, err, ErrDuplicateListenerID)
first, ok := registry.LookupListener("session", 7)
must.True(t, ok)
assert.Equal(t, firstID, first.AuthorizationID)
assert.Equal(t, "127.0.0.1:8001", first.Address)
_, ok = registry.Lookup("session", secondID)
assert.False(t, ok, "rejected candidate must be purged rather than retained as a tombstone")
_, err = acquirePlanForTest(registry, "session", secondID)
must.ErrorIs(t, err, ErrUnknownAuthorization)
listed := registry.List("session")
must.Len(t, listed, 1)
assert.Equal(t, firstID, listed[0].AuthorizationID)
}
func TestRegistryAuthorizationIsSessionScoped(t *testing.T) {
t.Parallel()
registry := NewRegistry()
id, err := registry.Begin("owner", "127.0.0.1:9000", 0)
must.NoError(t, err)
_, err = acquirePlanForTest(registry, "attacker", id)
must.ErrorIs(t, err, ErrAuthorizationSession)
err = registry.Activate("attacker", id, 1)
must.ErrorIs(t, err, ErrAuthorizationSession)
assert.False(t, registry.Revoke("attacker", id))
plan, err := acquirePlanForTest(registry, "owner", id)
must.NoError(t, err)
assert.Equal(t, "127.0.0.1:9000", plan.Address)
}
func TestRegistryLegacyLookupReturnsStoredCanonicalDestination(t *testing.T) {
t.Parallel()
registry := NewRegistry()
firstID, err := registry.Begin("session", "Example.COM.:080", 10)
must.NoError(t, err)
secondID, err := registry.Begin("session", "example.com:80", 20)
must.NoError(t, err)
plan, err := acquireLegacyPlanForTest(registry, "session", "EXAMPLE.COM.:00080")
must.NoError(t, err)
assert.Equal(t, secondID, plan.AuthorizationID)
assert.Equal(t, "example.com:80", plan.Address, "wire spelling must never become the dial target")
assert.Equal(t, int32(20), plan.KeepAlive)
must.True(t, registry.Revoke("session", secondID))
plan, err = acquireLegacyPlanForTest(registry, "session", "example.com:80")
must.NoError(t, err)
assert.Equal(t, firstID, plan.AuthorizationID)
assert.Equal(t, int32(10), plan.KeepAlive)
_, err = acquireLegacyPlanForTest(registry, "session", "127.0.0.1:80")
must.ErrorIs(t, err, ErrUnknownAuthorization)
}
func TestRegistryNegotiatedAuthorizationIDDisablesLegacyLookup(t *testing.T) {
t.Parallel()
registry := NewRegistry()
id, err := registry.Begin("session", "Example.COM.:443", 0)
must.NoError(t, err)
must.NoError(t, registry.ActivateProtocol("session", id, 7, true))
authorization, ok := registry.LookupListener("session", 7)
must.True(t, ok)
assert.True(t, authorization.RequiresAuthorizationID)
_, err = acquireLegacyPlanForTest(registry, "session", "example.com:443")
must.ErrorIs(t, err, ErrAuthorizationIDRequired)
plan, err := acquirePlanForTest(registry, "session", id)
must.NoError(t, err)
assert.Equal(t, "example.com:443", plan.Address)
}
func TestRegistryAuthorizationConnectionLimitIncludesPendingAndActive(t *testing.T) {
assert.Equal(t, 64, maxConnectionsPerAuthorization)
assert.Equal(t, 256, maxConnectionsPerSession)
assert.Equal(t, 2048, maxConnectionsGlobal)
registry := NewRegistry()
id, err := registry.Begin("session", "127.0.0.1:8080", 0)
must.NoError(t, err)
reservations := make([]*connectionReservation, 0, maxConnectionsPerAuthorization)
for range maxConnectionsPerAuthorization {
_, _, reservation, reserveErr := registry.reserve("session", id, "")
must.NoError(t, reserveErr)
reservations = append(reservations, reservation)
}
clients := make([]net.Conn, 0, maxConnectionsPerAuthorization/2)
peers := make([]net.Conn, 0, maxConnectionsPerAuthorization/2)
connectionIDs := make([]uint64, 0, maxConnectionsPerAuthorization/2)
for index := 0; index < maxConnectionsPerAuthorization/2; index++ {
client, peer := net.Pipe()
connectionID, registerErr := registry.registerConnection("session", id, reservations[index], client)
must.NoError(t, registerErr)
clients = append(clients, client)
peers = append(peers, peer)
connectionIDs = append(connectionIDs, connectionID)
}
t.Cleanup(func() {
for _, connection := range clients {
_ = connection.Close()
}
for _, connection := range peers {
_ = connection.Close()
}
})
assertRegistryConnectionCounts(t, registry, 64, map[string]uint64{"session": 64})
registry.mu.RLock()
record := registry.byID[id]
if record == nil {
registry.mu.RUnlock()
t.Fatal("authorization record disappeared before revocation")
}
assert.Equal(t, uint64(32), record.pendingConnections)
assert.Len(t, record.connections, 32)
registry.mu.RUnlock()
_, _, _, err = registry.reserve("session", id, "")
must.ErrorIs(t, err, ErrAuthorizationConnectionLimit)
registry.releaseReservation(reservations[32])
_, _, replacementPending, err := registry.reserve("session", id, "")
must.NoError(t, err)
registry.releaseReservation(replacementPending)
assertRegistryConnectionCounts(t, registry, 63, map[string]uint64{"session": 63})
registry.unregisterConnection(record, connectionIDs[0])
_ = clients[0].Close()
_, _, replacementActive, err := registry.reserve("session", id, "")
must.NoError(t, err)
registry.releaseReservation(replacementActive)
assertRegistryConnectionCounts(t, registry, 62, map[string]uint64{"session": 62})
must.True(t, registry.Revoke("session", id))
for _, reservation := range reservations {
registry.releaseReservation(reservation)
}
assertRegistryConnectionCounts(t, registry, 0, nil)
}
func TestRegistrySessionConnectionLimitIsScopedAndReclaimable(t *testing.T) {
registry := NewRegistry()
reservations := make([]*connectionReservation, 0, maxConnectionsPerSession)
for authIndex := 0; authIndex < 8; authIndex++ {
id, err := registry.Begin("full-session", "127.0.0.1:8080", 0)
must.NoError(t, err)
for range 32 {
_, _, reservation, reserveErr := registry.reserve("full-session", id, "")
must.NoError(t, reserveErr)
reservations = append(reservations, reservation)
}
}
assertRegistryConnectionCounts(t, registry, 256, map[string]uint64{"full-session": 256})
overflowID, err := registry.Begin("full-session", "127.0.0.1:8081", 0)
must.NoError(t, err)
_, _, _, err = registry.reserve("full-session", overflowID, "")
must.ErrorIs(t, err, ErrSessionConnectionLimit)
otherID, err := registry.Begin("other-session", "127.0.0.1:8082", 0)
must.NoError(t, err)
_, _, otherReservation, err := registry.reserve("other-session", otherID, "")
must.NoError(t, err)
assertRegistryConnectionCounts(t, registry, 257, map[string]uint64{"full-session": 256, "other-session": 1})
registry.releaseReservation(otherReservation)
registry.releaseReservation(reservations[0])
_, _, reclaimed, err := registry.reserve("full-session", overflowID, "")
must.NoError(t, err)
registry.releaseReservation(reclaimed)
assertRegistryConnectionCounts(t, registry, 255, map[string]uint64{"full-session": 255})
assert.Equal(t, 9, registry.RevokeSession("full-session"))
registry.RevokeSession("other-session")
assertRegistryConnectionCounts(t, registry, 0, nil)
}
func TestRegistryGlobalConnectionLimitIsReclaimable(t *testing.T) {
registry := NewRegistry()
filledSessions := make([]string, 0, 8)
var firstReservation *connectionReservation
for sessionIndex := 0; sessionIndex < 8; sessionIndex++ {
sessionID := "global-session-" + string(rune('a'+sessionIndex))
filledSessions = append(filledSessions, sessionID)
for authIndex := 0; authIndex < 4; authIndex++ {
id, err := registry.Begin(sessionID, "127.0.0.1:8080", 0)
must.NoError(t, err)
for range maxConnectionsPerAuthorization {
_, _, reservation, reserveErr := registry.reserve(sessionID, id, "")
must.NoError(t, reserveErr)
if firstReservation == nil {
firstReservation = reservation
}
}
}
}
wantSessions := map[string]uint64{}
for _, sessionID := range filledSessions {
wantSessions[sessionID] = maxConnectionsPerSession
}
assertRegistryConnectionCounts(t, registry, 2048, wantSessions)
overflowID, err := registry.Begin("overflow-session", "127.0.0.1:8081", 0)
must.NoError(t, err)
_, _, _, err = registry.reserve("overflow-session", overflowID, "")
must.ErrorIs(t, err, ErrGlobalConnectionLimit)
registry.releaseReservation(firstReservation)
_, _, reclaimed, err := registry.reserve("overflow-session", overflowID, "")
must.NoError(t, err)
registry.releaseReservation(reclaimed)
assertRegistryConnectionCounts(t, registry, maxConnectionsGlobal-1, map[string]uint64{
"global-session-a": maxConnectionsPerSession - 1,
"global-session-b": maxConnectionsPerSession,
"global-session-c": maxConnectionsPerSession,
"global-session-d": maxConnectionsPerSession,
"global-session-e": maxConnectionsPerSession,
"global-session-f": maxConnectionsPerSession,
"global-session-g": maxConnectionsPerSession,
"global-session-h": maxConnectionsPerSession,
})
for _, sessionID := range filledSessions {
registry.RevokeSession(sessionID)
}
registry.RevokeSession("overflow-session")
assertRegistryConnectionCounts(t, registry, 0, nil)
}
func TestRegistryRegisterAndRevokeRaceReclaimsConnectionQuota(t *testing.T) {
for iteration := 0; iteration < 100; iteration++ {
registry := NewRegistry()
id, err := registry.Begin("session", "127.0.0.1:8080", 0)
must.NoError(t, err)
_, _, reservation, err := registry.reserve("session", id, "")
must.NoError(t, err)
client, peer := net.Pipe()
start := make(chan struct{})
var wait sync.WaitGroup
wait.Add(2)
go func() {
defer wait.Done()
<-start
_, _ = registry.registerConnection("session", id, reservation, client)
}()
go func() {
defer wait.Done()
<-start
registry.Revoke("session", id)
}()
close(start)
wait.Wait()
registry.releaseReservation(reservation)
_ = client.Close()
_ = peer.Close()
assertRegistryConnectionCounts(t, registry, 0, nil)
}
}
func TestRegistryLateReservationReleaseCannotAffectReusedAuthorizationID(t *testing.T) {
random := bytes.NewReader(bytes.Repeat([]byte{0x42}, authorizationIDBytes*2))
registry := newRegistry(random)
oldID, err := registry.Begin("session", "127.0.0.1:8080", 0)
must.NoError(t, err)
_, _, oldReservation, err := registry.reserve("session", oldID, "")
must.NoError(t, err)
must.True(t, registry.Revoke("session", oldID))
newID, err := registry.Begin("session", "127.0.0.1:8081", 0)
must.NoError(t, err)
assert.Equal(t, oldID, newID)
_, _, newReservation, err := registry.reserve("session", newID, "")
must.NoError(t, err)
registry.releaseReservation(oldReservation)
assertRegistryConnectionCounts(t, registry, 1, map[string]uint64{"session": 1})
registry.releaseReservation(newReservation)
assertRegistryConnectionCounts(t, registry, 0, nil)
}
func TestRegistryRevokeAndActivateRaceFailsClosed(t *testing.T) {
t.Parallel()
for iteration := 0; iteration < 100; iteration++ {
registry := NewRegistry()
id, err := registry.Begin("session", "127.0.0.1:9000", 0)
must.NoError(t, err)
start := make(chan struct{})
var activateErr error
var wait sync.WaitGroup
wait.Add(2)
go func() {
defer wait.Done()
<-start
activateErr = registry.Activate("session", id, 1)
}()
go func() {
defer wait.Done()
<-start
registry.Revoke("session", id)
}()
close(start)
wait.Wait()
if activateErr != nil {
if !errors.Is(activateErr, ErrAuthorizationRevoked) && !errors.Is(activateErr, ErrUnknownAuthorization) {
t.Fatalf("Activate() race error = %v, want revoked or unknown", activateErr)
}
}
_, ok := registry.Lookup("session", id)
assert.False(t, ok)
_, err = acquirePlanForTest(registry, "session", id)
must.ErrorIs(t, err, ErrUnknownAuthorization)
}
}
func TestRegistryRevokeSessionIsScopedAndIdempotent(t *testing.T) {
t.Parallel()
registry := NewRegistry()
firstID, err := registry.Begin("first", "127.0.0.1:8001", 0)
must.NoError(t, err)
secondID, err := registry.Begin("first", "127.0.0.1:8002", 0)
must.NoError(t, err)
otherID, err := registry.Begin("other", "127.0.0.1:8003", 0)
must.NoError(t, err)
must.NoError(t, registry.Activate("first", firstID, 1))
must.NoError(t, registry.Activate("first", secondID, 2))
must.NoError(t, registry.Activate("other", otherID, 1))
assert.Equal(t, 2, registry.RevokeSession("first"))
assert.Equal(t, 0, registry.RevokeSession("first"))
_, ok := registry.Lookup("first", firstID)
assert.False(t, ok)
assert.Empty(t, registry.List("first"))
plan, err := acquirePlanForTest(registry, "other", otherID)
must.NoError(t, err)
assert.Equal(t, "127.0.0.1:8003", plan.Address)
}
func TestRegistryGeneratesUniqueOpaqueIDs(t *testing.T) {
t.Parallel()
registry := NewRegistry()
seen := map[AuthorizationID]struct{}{}
for index := 0; index < 256; index++ {
id, err := registry.Begin("session", "127.0.0.1:8080", 0)
must.NoError(t, err)
assert.Len(t, id, 43)
assert.False(t, strings.ContainsAny(id.String(), "+/="))
_, duplicate := seen[id]
assert.False(t, duplicate)
seen[id] = struct{}{}
}
}
type errorReader struct{}
func (errorReader) Read(_ []byte) (int, error) {
return 0, io.ErrUnexpectedEOF
}
func TestAuthorizationErrorsAreComparable(t *testing.T) {
t.Parallel()
_, err := canonicalizeAddress("invalid")
assert.True(t, errors.Is(err, ErrInvalidForwardAddress))
}
func acquirePlanForTest(registry *Registry, sessionID string, id AuthorizationID) (dialPlan, error) {
plan, _, reservation, err := registry.reserve(sessionID, id, "")
if err == nil {
registry.releaseReservation(reservation)
}
return plan, err
}
func acquireLegacyPlanForTest(registry *Registry, sessionID string, address string) (dialPlan, error) {
plan, _, reservation, err := registry.reserve(sessionID, "", address)
if err == nil {
registry.releaseReservation(reservation)
}
return plan, err
}
func assertRegistryConnectionCounts(t *testing.T, registry *Registry, total uint64, sessions map[string]uint64) {
t.Helper()
registry.mu.RLock()
defer registry.mu.RUnlock()
assert.Equal(t, total, registry.totalConnectionCount)
if sessions == nil {
assert.Empty(t, registry.sessionConnectionCounts)
return
}
assert.Equal(t, sessions, registry.sessionConnectionCounts)
}
+168
View File
@@ -0,0 +1,168 @@
package rtunnels
import (
"context"
"errors"
"net"
"sync"
"time"
)
// DefaultDialTimeout bounds an authorized reverse-port-forward connection attempt.
const DefaultDialTimeout = 10 * time.Second
// ErrNilDialConnection rejects dialers that return success without a connection.
var ErrNilDialConnection = errors.New("reverse port forward dialer returned a nil connection")
// ContextDialer is the only dependency allowed to create outbound reverse port
// forward connections. It is injectable so authorization-to-dial behavior can
// be tested without opening a real socket.
type ContextDialer interface {
DialContext(ctx context.Context, network string, address string) (net.Conn, error)
}
// DialContextFunc adapts a function into a ContextDialer for focused tests.
type DialContextFunc func(ctx context.Context, network string, address string) (net.Conn, error)
// DialContext invokes the adapted dial function.
func (dial DialContextFunc) DialContext(ctx context.Context, network string, address string) (net.Conn, error) {
return dial(ctx, network, address)
}
// Broker resolves server-owned authorizations and opens their immutable dial
// plans. A caller can provide an implant address only for legacy lookup; that
// value is never passed to the dialer.
type Broker struct {
registry *Registry
dialer ContextDialer
dialTimeout time.Duration
registerConnection func(string, AuthorizationID, *connectionReservation, net.Conn) (uint64, error)
}
// NewBroker creates an outbound reverse port forward broker. Nil dependencies
// use production-safe defaults, and non-positive timeouts use DefaultDialTimeout.
func NewBroker(registry *Registry, dialer ContextDialer, dialTimeout time.Duration) *Broker {
if registry == nil {
registry = NewRegistry()
}
if dialer == nil {
dialer = &net.Dialer{Timeout: DefaultDialTimeout}
}
if dialTimeout <= 0 {
dialTimeout = DefaultDialTimeout
}
return &Broker{
registry: registry,
dialer: dialer,
dialTimeout: dialTimeout,
}
}
// Open resolves an authorization, dials only its stored operator destination,
// applies keepalive policy, and tracks the connection until it is closed or its
// authorization is revoked. Authorization revocation cancels pending dials and
// closes committed connections. The destination-bearing plan remains private;
// callers receive only the resolved capability for tunnel association.
func (broker *Broker) Open(ctx context.Context, sessionID string, id AuthorizationID, legacyAddress string) (net.Conn, AuthorizationID, error) {
if ctx == nil {
ctx = context.Background()
}
plan, revoked, reservation, err := broker.registry.reserve(sessionID, id, legacyAddress)
if err != nil {
return nil, "", err
}
reservationActive := true
defer func() {
if reservationActive {
broker.registry.releaseReservation(reservation)
}
}()
dialContext, cancel := context.WithTimeout(ctx, broker.dialTimeout)
stopRevocationCancel := context.AfterFunc(revoked, cancel)
connection, err := broker.dialer.DialContext(dialContext, "tcp", plan.Address)
stopRevocationCancel()
dialContextErr := dialContext.Err()
cancel()
if err != nil {
if connection != nil {
_ = connection.Close()
}
if revoked.Err() != nil {
return nil, "", ErrAuthorizationRevoked
}
return nil, "", err
}
if connection == nil {
return nil, "", ErrNilDialConnection
}
if dialContextErr != nil {
_ = connection.Close()
if revoked.Err() != nil {
return nil, "", ErrAuthorizationRevoked
}
return nil, "", dialContextErr
}
configureKeepAlive(connection, plan.KeepAlive)
registerConnection := broker.registry.registerConnection
if broker.registerConnection != nil {
registerConnection = broker.registerConnection
}
connectionID, err := registerConnection(sessionID, plan.AuthorizationID, reservation, connection)
if err != nil {
_ = connection.Close()
if revoked.Err() != nil {
return nil, "", ErrAuthorizationRevoked
}
return nil, "", err
}
reservationActive = false
return &authorizedConn{
Conn: connection,
registry: broker.registry,
record: reservation.record,
connectionID: connectionID,
}, plan.AuthorizationID, nil
}
func configureKeepAlive(connection net.Conn, keepAlive int32) {
tcpConnection, ok := connection.(*net.TCPConn)
if !ok {
return
}
if keepAlive < 0 {
_ = tcpConnection.SetKeepAlive(false)
return
}
_ = tcpConnection.SetKeepAlive(true)
if keepAlive == 0 {
_ = tcpConnection.SetKeepAlivePeriod(30 * time.Second)
return
}
_ = tcpConnection.SetKeepAlivePeriod(time.Duration(keepAlive) * time.Second)
}
type authorizedConn struct {
net.Conn
registry *Registry
record *authorizationRecord
connectionID uint64
closeOnce sync.Once
closeErr error
}
func (connection *authorizedConn) Close() error {
connection.closeOnce.Do(func() {
connection.registry.unregisterConnection(connection.record, connection.connectionID)
connection.closeErr = connection.Conn.Close()
})
return connection.closeErr
}
// DefaultRegistry and DefaultBroker are the production reverse port forward
// authorization components. Tests should prefer instance-owned registries.
var (
DefaultRegistry = NewRegistry()
DefaultBroker = NewBroker(DefaultRegistry, nil, DefaultDialTimeout)
)
+413
View File
@@ -0,0 +1,413 @@
package rtunnels
import (
"context"
"errors"
"io"
"net"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestBrokerOpenDialsOnlyStoredOperatorAddress(t *testing.T) {
t.Parallel()
registry := NewRegistry()
id, err := registry.Begin("session", "Operator.Example.:080", 19)
must.NoError(t, err)
var network string
var address string
client, peer := net.Pipe()
t.Cleanup(func() {
_ = peer.Close()
})
dialer := DialContextFunc(func(_ context.Context, dialNetwork string, dialAddress string) (net.Conn, error) {
network = dialNetwork
address = dialAddress
return client, nil
})
broker := NewBroker(registry, dialer, time.Second)
connection, resolvedID, err := broker.Open(context.Background(), "session", id, "127.0.0.1:1")
must.NoError(t, err)
t.Cleanup(func() {
_ = connection.Close()
})
assert.Equal(t, "tcp", network)
assert.Equal(t, "operator.example:80", address)
assert.Equal(t, id, resolvedID)
}
func TestBrokerLegacyOpenUsesWireAddressOnlyAsLookupKey(t *testing.T) {
t.Parallel()
registry := NewRegistry()
id, err := registry.Begin("session", "Example.COM.:080", 0)
must.NoError(t, err)
client, peer := net.Pipe()
t.Cleanup(func() {
_ = peer.Close()
})
var dialed string
broker := NewBroker(registry, DialContextFunc(func(_ context.Context, _ string, address string) (net.Conn, error) {
dialed = address
return client, nil
}), time.Second)
connection, resolvedID, err := broker.Open(context.Background(), "session", "", "EXAMPLE.COM.:00080")
must.NoError(t, err)
t.Cleanup(func() {
_ = connection.Close()
})
assert.Equal(t, id, resolvedID)
assert.Equal(t, "example.com:80", dialed)
assert.NotEqual(t, "EXAMPLE.COM.:00080", dialed)
}
func TestBrokerRejectsUnknownAndCrossSessionIDsWithoutDialing(t *testing.T) {
t.Parallel()
registry := NewRegistry()
id, err := registry.Begin("owner", "127.0.0.1:8080", 0)
must.NoError(t, err)
dials := 0
broker := NewBroker(registry, DialContextFunc(func(_ context.Context, _ string, _ string) (net.Conn, error) {
dials++
return nil, errors.New("unexpected dial")
}), time.Second)
_, _, err = broker.Open(context.Background(), "owner", AuthorizationID("unknown"), "127.0.0.1:1")
must.ErrorIs(t, err, ErrUnknownAuthorization)
_, _, err = broker.Open(context.Background(), "attacker", id, "127.0.0.1:1")
must.ErrorIs(t, err, ErrAuthorizationSession)
_, _, err = broker.Open(context.Background(), "owner", "", "127.0.0.1:1")
must.ErrorIs(t, err, ErrUnknownAuthorization)
assert.Zero(t, dials)
}
func TestBrokerAppliesBoundedDialContext(t *testing.T) {
t.Parallel()
registry := NewRegistry()
id, err := registry.Begin("session", "127.0.0.1:8080", 0)
must.NoError(t, err)
timeout := 250 * time.Millisecond
var remaining time.Duration
broker := NewBroker(registry, DialContextFunc(func(ctx context.Context, _ string, _ string) (net.Conn, error) {
deadline, ok := ctx.Deadline()
must.True(t, ok)
remaining = time.Until(deadline)
return nil, context.DeadlineExceeded
}), timeout)
_, _, err = broker.Open(context.Background(), "session", id, "")
must.ErrorIs(t, err, context.DeadlineExceeded)
assert.Greater(t, remaining, time.Duration(0))
assert.LessOrEqual(t, remaining, timeout)
}
func TestBrokerRevokeCancelsPendingDial(t *testing.T) {
t.Parallel()
registry := NewRegistry()
id, err := registry.Begin("session", "127.0.0.1:8080", 0)
must.NoError(t, err)
started := make(chan struct{})
broker := NewBroker(registry, DialContextFunc(func(ctx context.Context, _ string, _ string) (net.Conn, error) {
close(started)
<-ctx.Done()
return nil, ctx.Err()
}), time.Minute)
result := make(chan error, 1)
go func() {
_, _, openErr := broker.Open(context.Background(), "session", id, "")
result <- openErr
}()
<-started
must.True(t, registry.Revoke("session", id))
select {
case openErr := <-result:
must.ErrorIs(t, openErr, ErrAuthorizationRevoked)
case <-time.After(time.Second):
t.Fatal("pending dial was not canceled by revocation")
}
}
func TestBrokerRevokeClosesActiveConnections(t *testing.T) {
t.Parallel()
registry := NewRegistry()
id, err := registry.Begin("session", "127.0.0.1:8080", 0)
must.NoError(t, err)
client, peer := net.Pipe()
t.Cleanup(func() {
_ = peer.Close()
})
broker := NewBroker(registry, DialContextFunc(func(_ context.Context, _ string, _ string) (net.Conn, error) {
return client, nil
}), time.Second)
connection, _, err := broker.Open(context.Background(), "session", id, "")
must.NoError(t, err)
assert.NotNil(t, connection)
must.True(t, registry.Revoke("session", id))
_ = peer.SetReadDeadline(time.Now().Add(time.Second))
_, err = peer.Read(make([]byte, 1))
must.ErrorIs(t, err, io.EOF)
_, _, err = broker.Open(context.Background(), "session", id, "")
must.ErrorIs(t, err, ErrUnknownAuthorization)
}
func TestBrokerListenerAndSessionRevocationCloseOnlyOwnedConnections(t *testing.T) {
t.Parallel()
registry := NewRegistry()
listenerID, err := registry.Begin("session", "127.0.0.1:8001", 0)
must.NoError(t, err)
must.NoError(t, registry.Activate("session", listenerID, 17))
sessionID, err := registry.Begin("session", "127.0.0.1:8002", 0)
must.NoError(t, err)
otherID, err := registry.Begin("other", "127.0.0.1:8003", 0)
must.NoError(t, err)
peers := make(chan net.Conn, 3)
broker := NewBroker(registry, DialContextFunc(func(_ context.Context, _ string, _ string) (net.Conn, error) {
client, peer := net.Pipe()
peers <- peer
return client, nil
}), time.Second)
_, _, err = broker.Open(context.Background(), "session", listenerID, "")
must.NoError(t, err)
listenerPeer := <-peers
sessionConnection, _, err := broker.Open(context.Background(), "session", sessionID, "")
must.NoError(t, err)
sessionPeer := <-peers
otherConnection, _, err := broker.Open(context.Background(), "other", otherID, "")
must.NoError(t, err)
otherPeer := <-peers
t.Cleanup(func() {
_ = listenerPeer.Close()
_ = sessionPeer.Close()
_ = otherPeer.Close()
_ = sessionConnection.Close()
_ = otherConnection.Close()
})
must.True(t, registry.RevokeListener("session", 17))
assertConnectionClosed(t, listenerPeer)
assertConnectionOpen(t, sessionPeer, sessionConnection)
assert.Equal(t, 1, registry.RevokeSession("session"))
assertConnectionClosed(t, sessionPeer)
assertConnectionOpen(t, otherPeer, otherConnection)
}
func TestBrokerDialCompletingAfterRevokeIsClosedAndRejected(t *testing.T) {
t.Parallel()
registry := NewRegistry()
id, err := registry.Begin("session", "127.0.0.1:8080", 0)
must.NoError(t, err)
started := make(chan struct{})
release := make(chan struct{})
peerReady := make(chan net.Conn, 1)
broker := NewBroker(registry, DialContextFunc(func(_ context.Context, _ string, _ string) (net.Conn, error) {
close(started)
<-release // Deliberately ignore context cancellation to exercise commit.
client, peer := net.Pipe()
peerReady <- peer
return client, nil
}), time.Minute)
result := make(chan error, 1)
go func() {
_, _, openErr := broker.Open(context.Background(), "session", id, "")
result <- openErr
}()
<-started
must.True(t, registry.Revoke("session", id))
close(release)
peer := <-peerReady
t.Cleanup(func() {
_ = peer.Close()
})
select {
case openErr := <-result:
must.ErrorIs(t, openErr, ErrAuthorizationRevoked)
case <-time.After(time.Second):
t.Fatal("dial completion did not fail closed after revocation")
}
_ = peer.SetReadDeadline(time.Now().Add(time.Second))
_, err = peer.Read(make([]byte, 1))
must.ErrorIs(t, err, io.EOF)
}
func TestBrokerAllowsMultipleConnectionsUntilRevocation(t *testing.T) {
t.Parallel()
registry := NewRegistry()
id, err := registry.Begin("session", "127.0.0.1:8080", 0)
must.NoError(t, err)
var mutex sync.Mutex
peers := make([]net.Conn, 0, 2)
broker := NewBroker(registry, DialContextFunc(func(_ context.Context, _ string, _ string) (net.Conn, error) {
client, peer := net.Pipe()
mutex.Lock()
peers = append(peers, peer)
mutex.Unlock()
return client, nil
}), time.Second)
first, _, err := broker.Open(context.Background(), "session", id, "")
must.NoError(t, err)
second, _, err := broker.Open(context.Background(), "session", id, "")
must.NoError(t, err)
assert.NotNil(t, first)
assert.NotNil(t, second)
must.True(t, registry.Revoke("session", id))
mutex.Lock()
defer mutex.Unlock()
must.Len(t, peers, 2)
for _, peer := range peers {
_ = peer.SetReadDeadline(time.Now().Add(time.Second))
_, readErr := peer.Read(make([]byte, 1))
must.ErrorIs(t, readErr, io.EOF)
_ = peer.Close()
}
}
func TestBrokerRejectsNilConnection(t *testing.T) {
t.Parallel()
registry := NewRegistry()
id, err := registry.Begin("session", "127.0.0.1:8080", 0)
must.NoError(t, err)
broker := NewBroker(registry, DialContextFunc(func(_ context.Context, _ string, _ string) (net.Conn, error) {
return nil, nil
}), time.Second)
_, _, err = broker.Open(context.Background(), "session", id, "")
must.ErrorIs(t, err, ErrNilDialConnection)
}
func TestBrokerOpenFailuresReleaseConnectionReservation(t *testing.T) {
tests := []struct {
name string
dialErr error
wantErr error
}{
{name: "dial error", dialErr: errors.New("dial failed"), wantErr: errors.New("dial failed")},
{name: "nil connection", wantErr: ErrNilDialConnection},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
registry := NewRegistry()
id, err := registry.Begin("session", "127.0.0.1:8080", 0)
must.NoError(t, err)
broker := NewBroker(registry, DialContextFunc(func(_ context.Context, _ string, _ string) (net.Conn, error) {
return nil, test.dialErr
}), time.Second)
for range maxConnectionsPerAuthorization + 1 {
_, _, openErr := broker.Open(context.Background(), "session", id, "")
if test.dialErr != nil {
assert.EqualError(t, openErr, test.dialErr.Error())
} else {
must.ErrorIs(t, openErr, test.wantErr)
}
assertRegistryConnectionCounts(t, registry, 0, nil)
}
})
}
}
func TestBrokerRegisterErrorClosesConnectionAndReleasesReservation(t *testing.T) {
registry := NewRegistry()
id, err := registry.Begin("session", "127.0.0.1:8080", 0)
must.NoError(t, err)
registerErr := errors.New("register failed")
peers := make(chan net.Conn, maxConnectionsPerAuthorization+1)
broker := NewBroker(registry, DialContextFunc(func(_ context.Context, _ string, _ string) (net.Conn, error) {
client, peer := net.Pipe()
peers <- peer
return client, nil
}), time.Second)
broker.registerConnection = func(string, AuthorizationID, *connectionReservation, net.Conn) (uint64, error) {
return 0, registerErr
}
for range maxConnectionsPerAuthorization + 1 {
_, _, openErr := broker.Open(context.Background(), "session", id, "")
must.ErrorIs(t, openErr, registerErr)
peer := <-peers
assertConnectionClosed(t, peer)
_ = peer.Close()
assertRegistryConnectionCounts(t, registry, 0, nil)
}
}
func TestBrokerRejectsConnectionReturnedAfterDialContextCancellation(t *testing.T) {
registry := NewRegistry()
id, err := registry.Begin("session", "127.0.0.1:8080", 0)
must.NoError(t, err)
started := make(chan struct{})
peerReady := make(chan net.Conn, 1)
broker := NewBroker(registry, DialContextFunc(func(ctx context.Context, _ string, _ string) (net.Conn, error) {
close(started)
<-ctx.Done()
client, peer := net.Pipe()
peerReady <- peer
return client, nil
}), time.Minute)
ctx, cancel := context.WithCancel(context.Background())
result := make(chan error, 1)
go func() {
_, _, openErr := broker.Open(ctx, "session", id, "")
result <- openErr
}()
<-started
cancel()
peer := <-peerReady
defer func() { _ = peer.Close() }()
select {
case openErr := <-result:
must.ErrorIs(t, openErr, context.Canceled)
case <-time.After(time.Second):
t.Fatal("Open did not reject a connection returned after cancellation")
}
assertConnectionClosed(t, peer)
assertRegistryConnectionCounts(t, registry, 0, nil)
}
func assertConnectionClosed(t *testing.T, connection net.Conn) {
t.Helper()
_ = connection.SetReadDeadline(time.Now().Add(time.Second))
_, err := connection.Read(make([]byte, 1))
must.ErrorIs(t, err, io.EOF)
}
func assertConnectionOpen(t *testing.T, peer net.Conn, connection net.Conn) {
t.Helper()
_ = peer.SetReadDeadline(time.Now().Add(time.Second))
writeDone := make(chan error, 1)
go func() {
_, err := connection.Write([]byte{0x42})
writeDone <- err
}()
buffer := make([]byte, 1)
_, err := peer.Read(buffer)
must.NoError(t, err)
assert.Equal(t, byte(0x42), buffer[0])
must.NoError(t, <-writeDone)
}
+547 -161
View File
@@ -1,23 +1,131 @@
package rtunnels
import (
"errors"
"fmt"
"io"
"sync"
"sync/atomic"
"time"
"github.com/bishopfox/sliver/protobuf/sliverpb"
)
var (
Rtunnels map[uint64]*RTunnel = make(map[uint64]*RTunnel)
mutex sync.RWMutex
pending map[string]map[string]*pendingInfo
listeners map[string]map[uint32]string
pendingMu sync.Mutex
rtunnels = make(map[uint64]*RTunnel)
mutex sync.RWMutex
)
type pendingInfo struct {
count int
keepAlive int32
// ErrDuplicateTunnelID rejects reuse of an active reverse tunnel ID.
var ErrDuplicateTunnelID = errors.New("reverse tunnel ID is already registered")
const (
maxReverseTunnelFrameBytes = sliverpb.MaxTunnelFrameBytes
maxReverseTunnelPendingFrames = 128
maxReverseTunnelPendingBytes = maxReverseTunnelFrameBytes * maxReverseTunnelPendingFrames
maxReverseTunnelIngress = 128
maxPendingBytesPerAuthorization int64 = 16 * 1024 * 1024
maxPendingBytesPerSession int64 = 32 * 1024 * 1024
maxPendingBytesGlobal int64 = 128 * 1024 * 1024
closeMatchingTunnelsTimeout = 20 * time.Second
)
// ErrReverseTunnelFrameTooLarge and the related errors report rejected relay operations.
var (
ErrReverseTunnelFrameTooLarge = errors.New("reverse tunnel frame exceeds the size limit")
ErrReverseTunnelWindow = errors.New("reverse tunnel sequence exceeds the pending window")
ErrReverseTunnelPendingBytes = errors.New("reverse tunnel pending data exceeds the byte limit")
ErrReverseTunnelClosed = errors.New("reverse tunnel is closed")
ErrReverseTunnelIngressLimit = errors.New("reverse tunnel inbound concurrency limit reached")
ErrReverseTunnelAuthBudget = errors.New("reverse tunnel authorization pending-data budget reached")
ErrReverseTunnelSessionBudget = errors.New("reverse tunnel session pending-data budget reached")
ErrReverseTunnelGlobalBudget = errors.New("reverse tunnel global pending-data budget reached")
ErrReverseTunnelTerminal = errors.New("reverse tunnel terminal sequence is invalid")
)
type pendingRelayAuthorization struct {
sessionID string
authorizationID AuthorizationID
}
// pendingRelayBudget bounds payloads admitted to relay handlers, including
// payloads waiting for the per-tunnel serialization lock. This prevents a
// blocked destination from multiplying the per-tunnel bound across every
// connection owned by one authorization or implant.
type pendingRelayBudget struct {
mutex sync.Mutex
total int64
bySession map[string]int64
byAuthorization map[pendingRelayAuthorization]int64
}
func newPendingRelayBudget() *pendingRelayBudget {
return &pendingRelayBudget{
bySession: map[string]int64{},
byAuthorization: map[pendingRelayAuthorization]int64{},
}
}
func (budget *pendingRelayBudget) reserve(sessionID string, authorizationID AuthorizationID, size int) error {
if size <= 0 {
return nil
}
amount := int64(size)
key := pendingRelayAuthorization{sessionID: sessionID, authorizationID: authorizationID}
budget.mutex.Lock()
defer budget.mutex.Unlock()
if budget.byAuthorization[key]+amount > maxPendingBytesPerAuthorization {
return ErrReverseTunnelAuthBudget
}
if budget.bySession[sessionID]+amount > maxPendingBytesPerSession {
return ErrReverseTunnelSessionBudget
}
if budget.total+amount > maxPendingBytesGlobal {
return ErrReverseTunnelGlobalBudget
}
budget.byAuthorization[key] += amount
budget.bySession[sessionID] += amount
budget.total += amount
return nil
}
func (budget *pendingRelayBudget) release(sessionID string, authorizationID AuthorizationID, size int) {
if budget == nil || size <= 0 {
return
}
amount := int64(size)
key := pendingRelayAuthorization{sessionID: sessionID, authorizationID: authorizationID}
budget.mutex.Lock()
defer budget.mutex.Unlock()
currentAuthorization := budget.byAuthorization[key]
if currentAuthorization == 0 {
return
}
if currentAuthorization < amount {
amount = currentAuthorization
}
if currentAuthorization == amount {
delete(budget.byAuthorization, key)
} else {
budget.byAuthorization[key] = currentAuthorization - amount
}
if current := budget.bySession[sessionID]; current <= amount {
delete(budget.bySession, sessionID)
} else {
budget.bySession[sessionID] = current - amount
}
if budget.total <= amount {
budget.total = 0
} else {
budget.total -= amount
}
}
var defaultPendingRelayBudget = newPendingRelayBudget()
// RTunnel - Duplex byte read/write
type RTunnel struct {
ID uint64
@@ -26,22 +134,301 @@ type RTunnel struct {
Readers []io.ReadCloser
readSequence uint64
Writer io.WriteCloser
writeSequence uint64
Writer io.WriteCloser
writeSequence uint64
outboundMutex sync.Mutex
outboundClosing bool
mutex *sync.RWMutex
mutex *sync.RWMutex
inboundMutex sync.Mutex
inboundAdmission chan struct{}
pendingInbound map[uint64][]byte
pendingBytes int
pendingBudget *pendingRelayBudget
closeOnce sync.Once
done chan struct{}
authorizationID AuthorizationID
peerCloseSet atomic.Bool
peerTeardown atomic.Bool
peerCloseSequence uint64
peerCloseTimerOnce sync.Once
peerCloseNotifier func(uint64) error
}
func NewRTunnel(id uint64, sID string, writer io.WriteCloser, readers ...io.ReadCloser) *RTunnel {
return &RTunnel{
ID: id,
SessionID: sID,
Readers: readers,
Writer: writer,
mutex: &sync.RWMutex{},
ID: id,
SessionID: sID,
Readers: readers,
Writer: writer,
mutex: &sync.RWMutex{},
inboundAdmission: make(chan struct{}, maxReverseTunnelIngress),
pendingInbound: map[uint64][]byte{},
pendingBudget: defaultPendingRelayBudget,
done: make(chan struct{}),
}
}
// NewAuthorizedRTunnel associates a reverse tunnel with the authorization that
// created its outbound connection. NewRTunnel remains available for callers
// that do not use reverse port forward authorization.
func NewAuthorizedRTunnel(id uint64, sessionID string, authorizationID AuthorizationID, writer io.WriteCloser, readers ...io.ReadCloser) *RTunnel {
tunnel := NewRTunnel(id, sessionID, writer, readers...)
tunnel.authorizationID = authorizationID
return tunnel
}
// AuthorizationID returns the server-owned authorization associated with the
// tunnel, or an empty ID for non-reverse tunnels.
func (c *RTunnel) AuthorizationID() AuthorizationID {
c.mutex.RLock()
defer c.mutex.RUnlock()
return c.authorizationID
}
// Done is closed when the relay is revoked or otherwise closed. Writers use it
// to abandon a blocked implant send without leaking a goroutine.
func (c *RTunnel) Done() <-chan struct{} {
return c.done
}
// SetPeerCloseNotifier installs the bounded transport callback used when the
// server is the side that first closes this relay. It must be set before the
// tunnel is published.
func (c *RTunnel) SetPeerCloseNotifier(notifier func(uint64) error) {
c.peerCloseNotifier = notifier
}
// QueueOutbound serializes server-to-implant frames with terminal close. The
// sequence advances only after the transport accepted the frame.
func (c *RTunnel) QueueOutbound(send func(uint64) error) error {
if send == nil {
return ErrReverseTunnelClosed
}
c.outboundMutex.Lock()
defer c.outboundMutex.Unlock()
if c.outboundClosing {
return ErrReverseTunnelClosed
}
select {
case <-c.done:
return ErrReverseTunnelClosed
default:
}
sequence := c.WriteSequence()
if err := send(sequence); err != nil {
return err
}
c.IncWriteSequence()
return nil
}
// CloseLocal sends one terminal sequence before closing local resources. The
// exclusive sequence guarantees that a concurrently dispatched close cannot
// overtake already accepted data at the implant.
func (c *RTunnel) closeLocal() (bool, error) {
c.outboundMutex.Lock()
if c.outboundClosing {
c.outboundMutex.Unlock()
return false, nil
}
select {
case <-c.done:
c.outboundMutex.Unlock()
return false, nil
default:
}
c.outboundClosing = true
sequence := c.WriteSequence()
notifier := c.peerCloseNotifier
var err error
if notifier != nil && !c.PeerTeardownPending() {
err = notifier(sequence)
}
c.outboundMutex.Unlock()
c.Close()
return true, err
}
// CloseRemote closes a relay without echoing another close to the peer.
func (c *RTunnel) closeRemote() bool {
// Publish peer ownership before Close unblocks a local terminal notifier.
// A simultaneous local close must not mistake the resulting send failure for
// a broken C2 transport.
c.peerTeardown.Store(true)
c.Close()
c.outboundMutex.Lock()
initiated := !c.outboundClosing
c.outboundClosing = true
c.outboundMutex.Unlock()
return initiated
}
// MarkPeerClose records the exclusive last sequence expected from the implant.
// Legacy zero-sequence closes after data retain their immediate-close behavior.
func (c *RTunnel) MarkPeerClose(sequence uint64) (bool, error) {
c.inboundMutex.Lock()
defer c.inboundMutex.Unlock()
select {
case <-c.done:
return true, ErrReverseTunnelClosed
default:
}
expected := c.ReadSequence()
if sequence == 0 {
if c.peerCloseSet.Load() && c.peerCloseSequence != expected {
return false, ErrReverseTunnelTerminal
}
c.peerCloseSequence = expected
c.peerTeardown.Store(true)
c.peerCloseSet.Store(true)
return true, nil
}
if sequence < expected || sequence-expected > maxReverseTunnelPendingFrames {
return false, ErrReverseTunnelTerminal
}
if c.peerCloseSet.Load() && c.peerCloseSequence != sequence {
return false, ErrReverseTunnelTerminal
}
for pendingSequence := range c.pendingInbound {
if pendingSequence >= sequence {
return false, ErrReverseTunnelTerminal
}
}
c.peerCloseSequence = sequence
c.peerTeardown.Store(true)
c.peerCloseSet.Store(true)
return expected >= sequence, nil
}
// PeerClosePending reports whether the peer supplied a terminal sequence.
func (c *RTunnel) PeerClosePending() bool {
return c.peerCloseSet.Load()
}
// PeerTeardownPending reports peer-owned teardown without pretending that a
// sequenced terminal has been installed. Inbound ordering consults only
// PeerClosePending; notifier failure suppression uses this combined state.
func (c *RTunnel) PeerTeardownPending() bool {
return c.peerTeardown.Load() || c.peerCloseSet.Load()
}
// PeerCloseReady reports whether all frames preceding the peer terminal arrived.
func (c *RTunnel) PeerCloseReady() bool {
c.inboundMutex.Lock()
defer c.inboundMutex.Unlock()
return c.peerCloseSet.Load() && c.ReadSequence() >= c.peerCloseSequence
}
// StartPeerCloseDeadline runs expired if the terminal sequence remains incomplete.
func (c *RTunnel) StartPeerCloseDeadline(timeout time.Duration, expired func()) {
if expired == nil {
return
}
c.peerCloseTimerOnce.Do(func() {
go func() {
timer := time.NewTimer(timeout)
defer timer.Stop()
select {
case <-c.Done():
case <-timer.C:
expired()
}
}()
})
}
// ProcessInbound serializes one relay's inbound stream, bounds out-of-order
// buffering, and drains contiguous frames through write. Pending data is owned
// by this tunnel instance, so stale cleanup for a reused numeric ID cannot
// delete another generation's cache.
//
//nolint:gocyclo // Admission, ordering, budget, and terminal checks must remain one locked transaction.
func (c *RTunnel) ProcessInbound(sequence uint64, data []byte, write func([]byte) error) (int, error) {
if write == nil {
return 0, errors.New("reverse tunnel write function is nil")
}
if len(data) > maxReverseTunnelFrameBytes {
return 0, fmt.Errorf("%w: got %d bytes, limit %d", ErrReverseTunnelFrameTooLarge, len(data), maxReverseTunnelFrameBytes)
}
select {
case <-c.done:
return 0, ErrReverseTunnelClosed
default:
}
budget := c.pendingBudget
if budget == nil {
budget = defaultPendingRelayBudget
}
if err := budget.reserve(c.SessionID, c.authorizationID, len(data)); err != nil {
return 0, err
}
reservationTransferred := false
defer func() {
if !reservationTransferred {
budget.release(c.SessionID, c.authorizationID, len(data))
}
}()
select {
case c.inboundAdmission <- struct{}{}:
defer func() { <-c.inboundAdmission }()
default:
return 0, ErrReverseTunnelIngressLimit
}
c.inboundMutex.Lock()
defer c.inboundMutex.Unlock()
select {
case <-c.done:
return len(c.pendingInbound), ErrReverseTunnelClosed
default:
}
// Constructors initialize this map, but retaining the invariant here keeps a
// zero-value or previously cleared tunnel from ever turning late data into a
// process-wide nil-map panic.
if c.pendingInbound == nil {
c.pendingInbound = map[uint64][]byte{}
}
expected := c.ReadSequence()
if c.peerCloseSet.Load() && sequence >= c.peerCloseSequence {
return len(c.pendingInbound), ErrReverseTunnelTerminal
}
if sequence < expected {
return len(c.pendingInbound), nil
}
if sequence-expected >= maxReverseTunnelPendingFrames {
return len(c.pendingInbound), fmt.Errorf("%w: got %d, expected %d", ErrReverseTunnelWindow, sequence, expected)
}
if _, duplicate := c.pendingInbound[sequence]; !duplicate {
if c.pendingBytes+len(data) > maxReverseTunnelPendingBytes {
return len(c.pendingInbound), ErrReverseTunnelPendingBytes
}
payload := append([]byte(nil), data...)
c.pendingInbound[sequence] = payload
c.pendingBytes += len(payload)
reservationTransferred = true
}
for {
payload, ok := c.pendingInbound[expected]
if !ok {
break
}
if err := write(payload); err != nil {
return len(c.pendingInbound), err
}
delete(c.pendingInbound, expected)
c.pendingBytes -= len(payload)
budget.release(c.SessionID, c.authorizationID, len(payload))
c.IncReadSequence()
expected++
}
return len(c.pendingInbound), nil
}
func (c *RTunnel) ReadSequence() uint64 {
c.mutex.RLock()
defer c.mutex.RUnlock()
@@ -72,165 +459,164 @@ func (c *RTunnel) IncWriteSequence() {
// Close - close RTunnel reader and writer
func (c *RTunnel) Close() {
for _, rc := range c.Readers {
if rc != nil {
rc.Close()
c.closeOnce.Do(func() {
close(c.done)
for _, rc := range c.Readers {
if rc != nil {
_ = rc.Close()
}
}
if c.Writer != nil {
_ = c.Writer.Close()
}
if c.inboundMutex.TryLock() {
c.releasePendingLocked()
c.inboundMutex.Unlock()
} else {
// A destination write can be slow or non-cooperative. Registry and
// resource teardown must not wait for it; release its bounded pending
// budget once the generation's inbound actor yields.
go func() {
c.inboundMutex.Lock()
c.releasePendingLocked()
c.inboundMutex.Unlock()
}()
}
})
}
func (c *RTunnel) releasePendingLocked() {
// Keep the map allocated so a retained closed pointer cannot turn late data
// into a nil-map write even if a future caller bypasses the Done guard.
clear(c.pendingInbound)
budget := c.pendingBudget
if budget == nil {
budget = defaultPendingRelayBudget
}
c.Writer.Close()
budget.release(c.SessionID, c.authorizationID, c.pendingBytes)
c.pendingBytes = 0
}
// Tunnel - Add tunnel to mapping
func GetRTunnel(ID uint64) *RTunnel {
mutex.RLock()
defer mutex.RUnlock()
return Rtunnels[ID]
return rtunnels[ID]
}
// AddTunnel - Add tunnel to mapping
func AddRTunnel(tun *RTunnel) {
// TryAddRTunnel atomically rejects duplicate tunnel IDs instead of replacing
// and orphaning an existing tunnel.
func TryAddRTunnel(tun *RTunnel) bool {
if tun == nil {
return false
}
mutex.Lock()
defer mutex.Unlock()
Rtunnels[tun.ID] = tun
}
// RemoveTunnel - Add tunnel to mapping
func RemoveRTunnel(ID uint64) {
mutex.Lock()
defer mutex.Unlock()
delete(Rtunnels, ID)
}
func AddPending(sessionID string, connStr string) {
pendingMu.Lock()
defer pendingMu.Unlock()
if pending == nil {
pending = make(map[string]map[string]*pendingInfo)
}
addrMap := pending[sessionID]
if addrMap == nil {
addrMap = make(map[string]*pendingInfo)
pending[sessionID] = addrMap
}
if info, ok := addrMap[connStr]; ok {
info.count++
} else {
addrMap[connStr] = &pendingInfo{count: 1}
}
}
func DeletePending(sessionID string, connStr string) {
pendingMu.Lock()
defer pendingMu.Unlock()
if pending == nil {
return
}
if addrMap, ok := pending[sessionID]; ok {
if info, ok := addrMap[connStr]; ok {
if info.count <= 1 {
delete(addrMap, connStr)
} else {
info.count--
}
}
if len(addrMap) == 0 {
delete(pending, sessionID)
}
}
}
func Check(sessionID string, connStr string) bool {
pendingMu.Lock()
defer pendingMu.Unlock()
if pending == nil {
if rtunnels[tun.ID] != nil {
return false
}
if addrMap, ok := pending[sessionID]; ok {
if info, ok := addrMap[connStr]; ok {
return info.count > 0
}
}
return false
}
func GetKeepAlive(sessionID string, connStr string) int32 {
pendingMu.Lock()
defer pendingMu.Unlock()
if pending == nil {
return 0
}
if addrMap, ok := pending[sessionID]; ok {
if info, ok := addrMap[connStr]; ok {
return info.keepAlive
}
}
return 0
}
func TrackListener(sessionID string, listenerID uint32, connStr string, keepAlive int32) {
pendingMu.Lock()
defer pendingMu.Unlock()
if listeners == nil {
listeners = make(map[string]map[uint32]string)
}
if pending == nil {
pending = make(map[string]map[string]*pendingInfo)
}
lm := listeners[sessionID]
if lm == nil {
lm = make(map[uint32]string)
listeners[sessionID] = lm
}
lm[listenerID] = connStr
addrMap := pending[sessionID]
if addrMap == nil {
addrMap = make(map[string]*pendingInfo)
pending[sessionID] = addrMap
}
if info, ok := addrMap[connStr]; ok {
info.count++
// If multiple listeners point to the same connStr, the last one's KeepAlive wins
// but they should be the same anyway.
info.keepAlive = keepAlive
} else {
addrMap[connStr] = &pendingInfo{
count: 1,
keepAlive: keepAlive,
}
}
}
func UntrackListener(sessionID string, listenerID uint32) bool {
pendingMu.Lock()
defer pendingMu.Unlock()
if listeners == nil || pending == nil {
return false
}
lm, ok := listeners[sessionID]
if !ok {
return false
}
connStr, ok := lm[listenerID]
if !ok {
return false
}
delete(lm, listenerID)
if len(lm) == 0 {
delete(listeners, sessionID)
}
if addrMap, ok := pending[sessionID]; ok {
if info, ok := addrMap[connStr]; ok {
if info.count <= 1 {
delete(addrMap, connStr)
} else {
info.count--
}
}
if len(addrMap) == 0 {
delete(pending, sessionID)
}
}
rtunnels[tun.ID] = tun
return true
}
// RemoveRTunnelIf removes ID only when it still points to expected. This keeps
// stale cleanup from deleting a different tunnel that reused the same ID.
func RemoveRTunnelIf(id uint64, expected *RTunnel) bool {
if expected == nil {
return false
}
mutex.Lock()
defer mutex.Unlock()
if rtunnels[id] != expected {
return false
}
delete(rtunnels, id)
return true
}
// CloseLocalIfActive selects local-close semantics only while this exact
// generation is still published. The terminal state is established before the
// registry entry disappears, so retained writers cannot enqueue after detach.
func CloseLocalIfActive(tunnel *RTunnel) (bool, error) {
if tunnel == nil || GetRTunnel(tunnel.ID) != tunnel {
return false, nil
}
initiated, err := tunnel.closeLocal()
if !initiated || !RemoveRTunnelIf(tunnel.ID, tunnel) {
return false, nil
}
return true, err
}
// CloseRemoteIfActive closes an exact peer-terminated generation without an
// echo and removes it only after its closed state is visible to retained work.
func CloseRemoteIfActive(tunnel *RTunnel) bool {
if tunnel == nil || GetRTunnel(tunnel.ID) != tunnel {
return false
}
initiated := tunnel.closeRemote()
if !initiated {
return false
}
return RemoveRTunnelIf(tunnel.ID, tunnel)
}
// CloseSession atomically detaches and closes only tunnels owned by sessionID.
// It is safe to call repeatedly during disconnect cleanup.
func CloseSession(sessionID string) int {
return closeMatchingTunnels(false, func(tunnel *RTunnel) bool {
return tunnel.SessionID == sessionID
})
}
// CloseAuthorization atomically detaches and closes active relays created by a
// specific authorization. It never closes another session's tunnel.
func CloseAuthorization(sessionID string, authorizationID AuthorizationID) int {
if authorizationID == "" {
return 0
}
return closeMatchingTunnels(true, func(tunnel *RTunnel) bool {
return tunnel.SessionID == sessionID && tunnel.AuthorizationID() == authorizationID
})
}
func closeMatchingTunnels(notifyPeer bool, matches func(*RTunnel) bool) int {
mutex.RLock()
tunnels := make([]*RTunnel, 0)
for _, tunnel := range rtunnels {
if tunnel != nil && matches(tunnel) {
tunnels = append(tunnels, tunnel)
}
}
mutex.RUnlock()
var closeWait sync.WaitGroup
var closedCount atomic.Int64
closeWait.Add(len(tunnels))
for _, tunnel := range tunnels {
go func(tunnel *RTunnel) {
defer closeWait.Done()
if notifyPeer {
if closed, _ := CloseLocalIfActive(tunnel); closed {
closedCount.Add(1)
}
} else if CloseRemoteIfActive(tunnel) {
closedCount.Add(1)
}
}(tunnel)
}
closed := make(chan struct{})
go func() {
closeWait.Wait()
close(closed)
}()
select {
case <-closed:
case <-time.After(closeMatchingTunnelsTimeout):
// Each goroutine retains ownership of its exact generation. Return within
// one batch deadline; bounded notifier callbacks or connection teardown
// finish state-before-detach cleanup.
}
return int(closedCount.Load())
}
@@ -0,0 +1,522 @@
package rtunnels
import (
"bytes"
"errors"
"fmt"
"io"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestRTunnelCloseIsIdempotentAndNilSafe(t *testing.T) {
reader := &countReadCloser{}
writer := &countWriteCloser{}
tunnel := NewAuthorizedRTunnel(91001, "session", AuthorizationID("authorization"), writer, reader, nil)
tunnel.Close()
tunnel.Close()
assert.Equal(t, int32(1), reader.closes.Load())
assert.Equal(t, int32(1), writer.closes.Load())
assert.Equal(t, AuthorizationID("authorization"), tunnel.AuthorizationID())
select {
case <-tunnel.Done():
default:
t.Fatal("Close() did not signal tunnel completion")
}
}
func TestRTunnelProcessInboundIsOrderedBoundedAndGenerationOwned(t *testing.T) {
first := NewRTunnel(92001, "session", &countWriteCloser{})
second := NewRTunnel(92001, "session", &countWriteCloser{})
var output bytes.Buffer
write := func(data []byte) error {
_, err := output.Write(data)
return err
}
pending, err := first.ProcessInbound(1, []byte("second"), write)
must.NoError(t, err)
assert.Equal(t, 1, pending)
pending, err = first.ProcessInbound(0, []byte("first-"), write)
must.NoError(t, err)
assert.Zero(t, pending)
assert.Equal(t, "first-second", output.String())
first.Close()
var secondOutput bytes.Buffer
pending, err = second.ProcessInbound(0, []byte("new-generation"), func(data []byte) error {
_, writeErr := secondOutput.Write(data)
return writeErr
})
must.NoError(t, err)
assert.Zero(t, pending)
assert.Equal(t, "new-generation", secondOutput.String())
}
func TestRTunnelPeerCloseWaitsForOutOfOrderFinalFrame(t *testing.T) {
tunnel := NewRTunnel(77, "session", &countWriteCloser{})
ready, err := tunnel.MarkPeerClose(2)
assert.NoError(t, err)
assert.False(t, ready)
var output bytes.Buffer
_, err = tunnel.ProcessInbound(1, []byte("second"), func(data []byte) error {
_, writeErr := output.Write(data)
return writeErr
})
assert.NoError(t, err)
assert.False(t, tunnel.PeerCloseReady())
_, err = tunnel.ProcessInbound(0, []byte("first-"), func(data []byte) error {
_, writeErr := output.Write(data)
return writeErr
})
assert.NoError(t, err)
assert.Equal(t, "first-second", output.String())
assert.True(t, tunnel.PeerCloseReady())
_, err = tunnel.ProcessInbound(2, []byte("after-terminal"), func([]byte) error { return nil })
assert.ErrorIs(t, err, ErrReverseTunnelTerminal)
}
func TestRTunnelAcceptsFullTransportReorderWindowBeforeTerminal(t *testing.T) {
tunnel := NewRTunnel(78, "session", &countWriteCloser{})
ready, err := tunnel.MarkPeerClose(maxReverseTunnelPendingFrames)
assert.NoError(t, err)
assert.False(t, ready)
var output bytes.Buffer
for sequence := maxReverseTunnelPendingFrames; sequence > 0; sequence-- {
value := byte(sequence - 1)
_, err := tunnel.ProcessInbound(uint64(sequence-1), []byte{value}, func(data []byte) error {
_, writeErr := output.Write(data)
return writeErr
})
if err != nil {
t.Fatalf("ProcessInbound(%d): %v", sequence-1, err)
}
}
assert.True(t, tunnel.PeerCloseReady())
assert.Len(t, output.Bytes(), maxReverseTunnelPendingFrames)
for index, value := range output.Bytes() {
assert.Equal(t, byte(index), value)
}
}
func TestRTunnelLegacyZeroCloseIsImmediateWithPendingFrames(t *testing.T) {
tunnel := NewRTunnel(79, "session", &countWriteCloser{})
pending, err := tunnel.ProcessInbound(1, []byte("pending"), func([]byte) error { return nil })
assert.NoError(t, err)
assert.Equal(t, 1, pending)
ready, err := tunnel.MarkPeerClose(0)
assert.NoError(t, err)
assert.True(t, ready)
}
func TestRTunnelLocalCloseNotifiesOnceAfterAcceptedFrames(t *testing.T) {
tunnel := NewRTunnel(88, "session", &countWriteCloser{})
var notified []uint64
tunnel.SetPeerCloseNotifier(func(sequence uint64) error {
notified = append(notified, sequence)
return nil
})
assert.NoError(t, tunnel.QueueOutbound(func(sequence uint64) error {
assert.Equal(t, uint64(0), sequence)
return nil
}))
initiated, err := tunnel.closeLocal()
assert.True(t, initiated)
assert.NoError(t, err)
initiated, err = tunnel.closeLocal()
assert.False(t, initiated)
assert.NoError(t, err)
assert.Equal(t, []uint64{1}, notified)
select {
case <-tunnel.Done():
default:
t.Fatal("local close did not close tunnel resources")
}
}
func TestRTunnelConcurrentLocalCloseNotifiesOnceOnFailure(t *testing.T) {
tunnel := NewRTunnel(89, "session", &countWriteCloser{})
notifyErr := errors.New("notification failed")
var notifications atomic.Int32
tunnel.SetPeerCloseNotifier(func(uint64) error {
notifications.Add(1)
return notifyErr
})
const closers = 32
type closeResult struct {
initiated bool
err error
}
results := make(chan closeResult, closers)
var closeWait sync.WaitGroup
closeWait.Add(closers)
for range closers {
go func() {
defer closeWait.Done()
initiated, err := tunnel.closeLocal()
results <- closeResult{initiated: initiated, err: err}
}()
}
closeWait.Wait()
close(results)
failures := 0
initiators := 0
for result := range results {
if result.initiated {
initiators++
}
if errors.Is(result.err, notifyErr) {
failures++
}
}
assert.Equal(t, int32(1), notifications.Load())
assert.Equal(t, 1, initiators)
assert.Equal(t, 1, failures)
}
func TestRTunnelSimultaneousPeerCloseUnblocksLocalNotifier(t *testing.T) {
tunnelID := uint64(90)
tunnel := NewRTunnel(tunnelID, "session", &countWriteCloser{})
must.True(t, TryAddRTunnel(tunnel))
t.Cleanup(func() {
if RemoveRTunnelIf(tunnelID, tunnel) {
tunnel.Close()
}
})
notifierStarted := make(chan struct{})
tunnel.SetPeerCloseNotifier(func(uint64) error {
close(notifierStarted)
<-tunnel.Done()
return ErrReverseTunnelClosed
})
type localCloseResult struct {
closed bool
err error
}
localResult := make(chan localCloseResult, 1)
go func() {
closed, err := CloseLocalIfActive(tunnel)
localResult <- localCloseResult{closed: closed, err: err}
}()
select {
case <-notifierStarted:
case <-time.After(time.Second):
t.Fatal("local terminal notifier did not start")
}
remoteResult := make(chan bool, 1)
go func() {
remoteResult <- CloseRemoteIfActive(tunnel)
}()
select {
case result := <-localResult:
if result.err != nil {
assert.ErrorIs(t, result.err, ErrReverseTunnelClosed)
}
case <-time.After(time.Second):
t.Fatal("local close did not finish after peer teardown")
}
select {
case <-remoteResult:
case <-time.After(time.Second):
t.Fatal("remote close did not finish")
}
assert.Nil(t, GetRTunnel(tunnelID))
assert.True(t, tunnel.PeerTeardownPending())
assert.False(t, tunnel.PeerClosePending())
}
func TestRTunnelRemoteTeardownMarkerDoesNotInstallTerminal(t *testing.T) {
tunnel := NewRTunnel(91, "session", &countWriteCloser{})
// Reproduce the closeRemote linearization window after peer ownership is
// published but before Done is closed.
tunnel.peerTeardown.Store(true)
written := 0
pending, err := tunnel.ProcessInbound(0, []byte("in-flight"), func(payload []byte) error {
written += len(payload)
return nil
})
assert.NoError(t, err)
assert.Zero(t, pending)
assert.Equal(t, len("in-flight"), written)
assert.True(t, tunnel.PeerTeardownPending())
assert.False(t, tunnel.PeerClosePending())
tunnel.Close()
}
func TestRTunnelProcessInboundRejectsRetainedPointerAfterClose(t *testing.T) {
tunnel := NewRTunnel(92006, "session", &countWriteCloser{})
tunnel.Close()
writeCalled := false
pending, err := tunnel.ProcessInbound(0, []byte("late-data"), func([]byte) error {
writeCalled = true
return nil
})
assert.ErrorIs(t, err, ErrReverseTunnelClosed)
assert.Zero(t, pending)
assert.False(t, writeCalled)
assert.NotNil(t, tunnel.pendingInbound)
assert.Empty(t, tunnel.pendingInbound)
}
func TestRTunnelProcessInboundRejectsResourceExhaustion(t *testing.T) {
tests := []struct {
name string
sequence uint64
data []byte
want error
}{
{name: "oversized frame", data: make([]byte, maxReverseTunnelFrameBytes+1), want: ErrReverseTunnelFrameTooLarge},
{name: "sequence window", sequence: maxReverseTunnelPendingFrames, data: []byte("x"), want: ErrReverseTunnelWindow},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
tunnel := NewRTunnel(92002, "session", &countWriteCloser{})
_, err := tunnel.ProcessInbound(test.sequence, test.data, func([]byte) error { return nil })
if !errors.Is(err, test.want) {
t.Fatalf("ProcessInbound() error = %v, want %v", err, test.want)
}
})
}
}
func TestRTunnelProcessInboundBoundsIngressBeforeSerialization(t *testing.T) {
budget := newPendingRelayBudget()
tunnel := NewAuthorizedRTunnel(92007, "session", AuthorizationID("authorization"), &countWriteCloser{})
tunnel.pendingBudget = budget
writeStarted := make(chan struct{})
releaseWrite := make(chan struct{})
var blockOnce sync.Once
write := func([]byte) error {
blockOnce.Do(func() {
close(writeStarted)
<-releaseWrite
})
return nil
}
results := make(chan error, maxReverseTunnelIngress)
go func() {
_, err := tunnel.ProcessInbound(0, []byte("first"), write)
results <- err
}()
select {
case <-writeStarted:
case <-time.After(time.Second):
t.Fatal("first inbound frame did not reach the writer")
}
for range maxReverseTunnelIngress - 1 {
go func() {
_, err := tunnel.ProcessInbound(1, []byte("queued"), write)
results <- err
}()
}
deadline := time.Now().Add(time.Second)
for len(tunnel.inboundAdmission) != maxReverseTunnelIngress && time.Now().Before(deadline) {
time.Sleep(time.Millisecond)
}
if got := len(tunnel.inboundAdmission); got != maxReverseTunnelIngress {
t.Fatalf("admitted inbound handlers = %d, want %d", got, maxReverseTunnelIngress)
}
_, err := tunnel.ProcessInbound(1, []byte("rejected"), write)
assert.ErrorIs(t, err, ErrReverseTunnelIngressLimit)
close(releaseWrite)
for range maxReverseTunnelIngress {
select {
case err := <-results:
must.NoError(t, err)
case <-time.After(time.Second):
t.Fatal("admitted inbound handler did not finish")
}
}
tunnel.Close()
assertPendingRelayBudgetEmpty(t, budget)
}
func TestRTunnelProcessInboundReleasesBudgetOnEveryTerminalPath(t *testing.T) {
budget := newPendingRelayBudget()
tunnel := NewAuthorizedRTunnel(92008, "session", AuthorizationID("authorization"), &countWriteCloser{})
tunnel.pendingBudget = budget
_, err := tunnel.ProcessInbound(maxReverseTunnelPendingFrames, []byte("window"), func([]byte) error { return nil })
assert.ErrorIs(t, err, ErrReverseTunnelWindow)
assertPendingRelayBudgetEmpty(t, budget)
_, err = tunnel.ProcessInbound(0, []byte("written"), func([]byte) error { return nil })
must.NoError(t, err)
_, err = tunnel.ProcessInbound(0, []byte("old"), func([]byte) error { return nil })
must.NoError(t, err)
assertPendingRelayBudgetEmpty(t, budget)
writeErr := errors.New("write failed")
_, err = tunnel.ProcessInbound(1, []byte("retained"), func([]byte) error { return writeErr })
assert.ErrorIs(t, err, writeErr)
budget.mutex.Lock()
assert.Equal(t, int64(len("retained")), budget.total)
budget.mutex.Unlock()
tunnel.Close()
assertPendingRelayBudgetEmpty(t, budget)
}
func TestRTunnelConcurrentCloseAndInboundFailsClosed(t *testing.T) {
for iteration := 0; iteration < 100; iteration++ {
budget := newPendingRelayBudget()
tunnel := NewAuthorizedRTunnel(uint64(93000+iteration), "session", AuthorizationID("authorization"), &countWriteCloser{})
tunnel.pendingBudget = budget
start := make(chan struct{})
result := make(chan error, 1)
go func() {
<-start
_, err := tunnel.ProcessInbound(0, []byte("data"), func([]byte) error { return nil })
result <- err
}()
close(start)
tunnel.Close()
err := <-result
if err != nil && !errors.Is(err, ErrReverseTunnelClosed) {
t.Fatalf("ProcessInbound() race error = %v, want nil or closed", err)
}
assertPendingRelayBudgetEmpty(t, budget)
}
}
func TestPendingRelayBudgetEnforcesAggregateLimitsAndSafeRelease(t *testing.T) {
budget := newPendingRelayBudget()
must.NoError(t, budget.reserve("auth-session", "auth-a", int(maxPendingBytesPerAuthorization)))
assert.ErrorIs(t, budget.reserve("auth-session", "auth-a", 1), ErrReverseTunnelAuthBudget)
budget.release("auth-session", "auth-a", int(maxPendingBytesPerAuthorization))
assertPendingRelayBudgetEmpty(t, budget)
for _, authorizationID := range []AuthorizationID{"session-a", "session-b"} {
must.NoError(t, budget.reserve("session", authorizationID, int(maxPendingBytesPerAuthorization)))
}
assert.ErrorIs(t, budget.reserve("session", "session-c", 1), ErrReverseTunnelSessionBudget)
for _, authorizationID := range []AuthorizationID{"session-a", "session-b"} {
budget.release("session", authorizationID, int(maxPendingBytesPerAuthorization))
}
assertPendingRelayBudgetEmpty(t, budget)
for sessionIndex := 0; sessionIndex < int(maxPendingBytesGlobal/maxPendingBytesPerSession); sessionIndex++ {
sessionID := fmt.Sprintf("global-session-%d", sessionIndex)
for authorizationIndex := 0; authorizationIndex < int(maxPendingBytesPerSession/maxPendingBytesPerAuthorization); authorizationIndex++ {
authorizationID := AuthorizationID(fmt.Sprintf("auth-%d", authorizationIndex))
must.NoError(t, budget.reserve(sessionID, authorizationID, int(maxPendingBytesPerAuthorization)))
}
}
assert.ErrorIs(t, budget.reserve("overflow-session", "overflow-auth", 1), ErrReverseTunnelGlobalBudget)
budget.release("global-session-0", "auth-0", int(maxPendingBytesPerAuthorization*2))
budget.mutex.Lock()
wantAfterRelease := maxPendingBytesGlobal - maxPendingBytesPerAuthorization
assert.Equal(t, wantAfterRelease, budget.total)
budget.mutex.Unlock()
budget.release("global-session-0", "auth-0", int(maxPendingBytesPerAuthorization))
budget.mutex.Lock()
assert.Equal(t, wantAfterRelease, budget.total, "double release must not consume another authorization's budget")
budget.mutex.Unlock()
}
func assertPendingRelayBudgetEmpty(t *testing.T, budget *pendingRelayBudget) {
t.Helper()
budget.mutex.Lock()
defer budget.mutex.Unlock()
assert.Zero(t, budget.total)
assert.Empty(t, budget.bySession)
assert.Empty(t, budget.byAuthorization)
}
func TestTryAddRTunnelAndConditionalRemovalRejectStaleCleanup(t *testing.T) {
const tunnelID = uint64(91002)
first := NewRTunnel(tunnelID, "first", &countWriteCloser{})
duplicate := NewRTunnel(tunnelID, "second", &countWriteCloser{})
t.Cleanup(func() {
RemoveRTunnelIf(tunnelID, first)
})
must.True(t, TryAddRTunnel(first))
assert.False(t, TryAddRTunnel(duplicate))
assert.Same(t, first, GetRTunnel(tunnelID))
assert.False(t, RemoveRTunnelIf(tunnelID, duplicate))
assert.Same(t, first, GetRTunnel(tunnelID))
assert.True(t, RemoveRTunnelIf(tunnelID, first))
assert.Nil(t, GetRTunnel(tunnelID))
assert.False(t, RemoveRTunnelIf(tunnelID, first))
assert.False(t, TryAddRTunnel(nil))
}
func TestCloseAuthorizationAndCloseSessionAreScopedAndIdempotent(t *testing.T) {
const (
matchingID = uint64(91003)
otherAuthID = uint64(91004)
otherSID = uint64(91005)
)
t.Cleanup(func() {
CloseSession("session")
CloseSession("other-session")
})
matchingWriter := &countWriteCloser{}
otherAuthWriter := &countWriteCloser{}
otherSessionWriter := &countWriteCloser{}
matching := NewAuthorizedRTunnel(matchingID, "session", AuthorizationID("auth-a"), matchingWriter)
otherAuth := NewAuthorizedRTunnel(otherAuthID, "session", AuthorizationID("auth-b"), otherAuthWriter)
otherSession := NewAuthorizedRTunnel(otherSID, "other-session", AuthorizationID("auth-a"), otherSessionWriter)
must.True(t, TryAddRTunnel(matching))
must.True(t, TryAddRTunnel(otherAuth))
must.True(t, TryAddRTunnel(otherSession))
assert.Equal(t, 1, CloseAuthorization("session", AuthorizationID("auth-a")))
assert.Equal(t, int32(1), matchingWriter.closes.Load())
assert.Zero(t, otherAuthWriter.closes.Load())
assert.Zero(t, otherSessionWriter.closes.Load())
assert.Equal(t, 0, CloseAuthorization("session", AuthorizationID("auth-a")))
assert.Equal(t, 0, CloseAuthorization("session", ""))
assert.Equal(t, 1, CloseSession("session"))
assert.Equal(t, int32(1), otherAuthWriter.closes.Load())
assert.Zero(t, otherSessionWriter.closes.Load())
assert.Equal(t, 0, CloseSession("session"))
assert.Equal(t, 1, CloseSession("other-session"))
assert.Equal(t, int32(1), otherSessionWriter.closes.Load())
}
type countReadCloser struct {
closes atomic.Int32
}
func (*countReadCloser) Read(_ []byte) (int, error) {
return 0, io.EOF
}
func (closer *countReadCloser) Close() error {
closer.closes.Add(1)
return nil
}
type countWriteCloser struct {
closes atomic.Int32
}
func (*countWriteCloser) Write(buffer []byte) (int, error) {
return len(buffer), nil
}
func (closer *countWriteCloser) Close() error {
closer.closes.Add(1)
return nil
}
-45
View File
@@ -1,45 +0,0 @@
package rtunnels
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestKeepAliveState(t *testing.T) {
sessionID := "test-session"
connStr := "127.0.0.1:8080"
listenerID := uint32(1)
keepAlive := int32(42)
// Test TrackListener
TrackListener(sessionID, listenerID, connStr, keepAlive)
assert.True(t, Check(sessionID, connStr))
assert.Equal(t, keepAlive, GetKeepAlive(sessionID, connStr))
// Test multiple listeners for same address
TrackListener(sessionID, 2, connStr, 60)
assert.Equal(t, int32(60), GetKeepAlive(sessionID, connStr))
assert.True(t, Check(sessionID, connStr))
// Test UntrackListener
UntrackListener(sessionID, 1)
assert.True(t, Check(sessionID, connStr)) // Still has listener 2
assert.Equal(t, int32(60), GetKeepAlive(sessionID, connStr))
UntrackListener(sessionID, 2)
assert.False(t, Check(sessionID, connStr))
assert.Equal(t, int32(0), GetKeepAlive(sessionID, connStr))
}
func TestAddDeletePending(t *testing.T) {
sessionID := "pending-session"
connStr := "127.0.0.1:9090"
AddPending(sessionID, connStr)
assert.True(t, Check(sessionID, connStr))
assert.Equal(t, int32(0), GetKeepAlive(sessionID, connStr))
DeletePending(sessionID, connStr)
assert.False(t, Check(sessionID, connStr))
}
@@ -0,0 +1,48 @@
package rtunnels
import (
"testing"
"github.com/stretchr/testify/assert"
)
// The vendored testify subset includes assert but not require. Keep fatal test
// assertions local so repository tests remain fully vendored.
var must testRequirements
type testRequirements struct{}
func (testRequirements) NoError(t testing.TB, err error, message ...any) {
t.Helper()
if !assert.NoError(t, err, message...) {
t.FailNow()
}
}
func (testRequirements) ErrorIs(t testing.TB, err error, target error, message ...any) {
t.Helper()
if !assert.ErrorIs(t, err, target, message...) {
t.FailNow()
}
}
func (testRequirements) True(t testing.TB, value bool, message ...any) {
t.Helper()
if !assert.True(t, value, message...) {
t.FailNow()
}
}
func (testRequirements) NotEmpty(t testing.TB, value any, message ...any) {
t.Helper()
if !assert.NotEmpty(t, value, message...) {
t.FailNow()
}
}
func (testRequirements) Len(t testing.TB, value any, length int, message ...any) {
t.Helper()
if !assert.Len(t, value, length, message...) {
t.FailNow()
}
}
+99 -28
View File
@@ -19,6 +19,7 @@ package core
*/
import (
"context"
"errors"
"sync"
"time"
@@ -27,6 +28,7 @@ import (
"github.com/bishopfox/sliver/implant/sliver/transports/wireguard"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/protobuf/sliverpb"
"github.com/bishopfox/sliver/server/core/rtunnels"
"github.com/bishopfox/sliver/server/log"
"github.com/gofrs/uuid"
@@ -158,40 +160,74 @@ func (s *Session) ToProtobuf() *clientpb.Session {
// Request - Sends a protobuf request to the active sliver and returns the response
func (s *Session) Request(msgType uint32, timeout time.Duration, data []byte) ([]byte, error) {
resp := make(chan *sliverpb.Envelope)
reqID := EnvelopeID()
s.Connection.RespMutex.Lock()
s.Connection.Resp[reqID] = resp
s.Connection.RespMutex.Unlock()
defer func() {
s.Connection.RespMutex.Lock()
defer s.Connection.RespMutex.Unlock()
// close(resp)
delete(s.Connection.Resp, reqID)
}()
if timeout == 0 {
timeout = 60 * time.Second
}
select {
case s.Connection.Send <- &sliverpb.Envelope{
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
response, err := s.RequestContext(ctx, msgType, data)
if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, ErrImplantSendTimeout) {
return nil, ErrImplantTimeout
}
return response, err
}
// RequestContext sends a request and uses one context budget across both
// outbound queueing and the response wait. A canceled context is checked before
// a response waiter is installed, and every return path removes that waiter.
func (s *Session) RequestContext(ctx context.Context, msgType uint32, data []byte) ([]byte, error) {
if s == nil || s.Connection == nil {
return nil, ErrImplantConnectionClosed
}
if ctx == nil {
ctx = context.Background()
}
if err := ctx.Err(); err != nil {
return nil, err
}
connection := s.Connection
resp := make(chan *sliverpb.Envelope, 1)
reqID := EnvelopeID()
connection.RespMutex.Lock()
connection.Resp[reqID] = resp
connection.RespMutex.Unlock()
defer func() {
connection.RespMutex.Lock()
defer connection.RespMutex.Unlock()
// close(resp)
delete(connection.Resp, reqID)
}()
sendTimeout := 60 * time.Second
if deadline, ok := ctx.Deadline(); ok {
sendTimeout = time.Until(deadline)
if sendTimeout <= 0 {
return nil, context.DeadlineExceeded
}
}
err := connection.SendEnvelopeUntil(&sliverpb.Envelope{
ID: reqID,
Type: msgType,
Data: data,
}:
case <-time.After(timeout):
return nil, ErrImplantTimeout
}, ctx.Done(), sendTimeout)
if err != nil {
if ctxErr := ctx.Err(); ctxErr != nil {
return nil, ctxErr
}
return nil, err
}
var respEnvelope *sliverpb.Envelope
select {
case respEnvelope = <-resp:
case <-time.After(timeout):
return nil, ErrImplantTimeout
case respEnvelope := <-resp:
if respEnvelope.UnknownMessageType {
return nil, ErrUnknownMessageType
}
return respEnvelope.Data, nil
case <-connection.Done():
return nil, ErrImplantConnectionClosed
case <-ctx.Done():
return nil, ctx.Err()
}
if respEnvelope.UnknownMessageType {
return nil, ErrUnknownMessageType
}
return respEnvelope.Data, nil
}
// sessions - Manages the slivers, provides atomic access
@@ -233,26 +269,61 @@ func (s *sessions) Remove(sessionID string) {
if !ok {
return
}
parentSession := val.(*Session)
s.RemoveIf(val.(*Session))
}
// RemoveIf removes a session only when the registry still contains the exact
// expected generation. Connection cleanup can run after a same-ID session has
// been replaced and must never evict that replacement.
func (s *sessions) RemoveIf(expected *Session) bool {
if expected == nil || expected.ID == "" {
return false
}
parentSession := expected
children := findAllChildrenByPeerID(parentSession.PeerID)
s.sessions.Delete(parentSession.ID)
if !s.sessions.CompareAndDelete(parentSession.ID, parentSession) {
return false
}
cleanupSessionTunnels(parentSession.ID)
coreLog.Debugf("Removing %d children of session %s (%v)", len(children), parentSession.ID, children)
for _, child := range children {
childSession, ok := s.sessions.LoadAndDelete(child.SessionID)
if ok {
PivotSessions.Delete(childSession.(*Session).Connection.ID)
removedChild := childSession.(*Session)
cleanupSessionTunnels(removedChild.ID)
if removedChild.Connection != nil {
closePivotForConnection(removedChild.Connection)
removedChild.Connection.Close()
}
EventBroker.Publish(Event{
EventType: consts.SessionClosedEvent,
Session: childSession.(*Session),
Session: removedChild,
})
}
}
if parentSession.Connection != nil && parentSession.Connection.Transport == PivotTransportName {
closePivotForConnection(parentSession.Connection)
parentSession.Connection.Close()
}
// Remove the parent session
EventBroker.Publish(Event{
EventType: consts.SessionClosedEvent,
Session: parentSession,
})
return true
}
func cleanupSessionTunnels(sessionID string) {
// Close generic forward tunnels before publishing SessionClosed so clients
// receive terminal tunnel frames while handling the disconnect event.
Tunnels.CloseForSession(sessionID)
SocksTunnels.CloseForSession(sessionID)
// Revoke reverse-forward authorization first to cancel pending broker dials,
// then detach registered relays. These operations are idempotent because
// disconnect paths can race.
rtunnels.DefaultRegistry.RevokeSession(sessionID)
rtunnels.CloseSession(sessionID)
}
// NewSession - Create a new session
+238 -1
View File
@@ -1,6 +1,14 @@
package core
import "testing"
import (
"context"
"errors"
"sync"
"testing"
"time"
"github.com/bishopfox/sliver/protobuf/sliverpb"
)
func TestSessionToProtobufNilConnection(t *testing.T) {
s := &Session{Name: "test-canary", Capabilities: 42}
@@ -22,3 +30,232 @@ func TestSessionToProtobufNilConnection(t *testing.T) {
t.Fatalf("expected Capabilities=%d, got %d", s.Capabilities, pb.Capabilities)
}
}
func TestSessionRequestDisconnectCleansResponseWaiter(t *testing.T) {
connection := NewImplantConnection("test", "test")
session := &Session{Connection: connection}
result := make(chan error, 1)
go func() {
_, err := session.Request(sliverpb.MsgPing, time.Hour, nil)
result <- err
}()
var request *sliverpb.Envelope
select {
case request = <-connection.Send:
case <-time.After(time.Second):
t.Fatal("request was not queued")
}
connection.RespMutex.RLock()
_, waiting := connection.Resp[request.ID]
connection.RespMutex.RUnlock()
if !waiting {
t.Fatal("request response waiter was not registered")
}
connection.Close()
select {
case err := <-result:
if !errors.Is(err, ErrImplantConnectionClosed) {
t.Fatalf("Request error = %v, want %v", err, ErrImplantConnectionClosed)
}
case <-time.After(time.Second):
t.Fatal("Request remained blocked after connection close")
}
connection.RespMutex.RLock()
waiters := len(connection.Resp)
connection.RespMutex.RUnlock()
if waiters != 0 {
t.Fatalf("response waiter count = %d, want 0", waiters)
}
}
func TestSessionRequestLateResponseCannotStrandTransportReader(t *testing.T) {
connection := NewImplantConnection("test", "test")
session := &Session{Connection: connection}
result := make(chan error, 1)
go func() {
_, err := session.Request(sliverpb.MsgPing, 25*time.Millisecond, nil)
result <- err
}()
var request *sliverpb.Envelope
select {
case request = <-connection.Send:
case <-time.After(time.Second):
t.Fatal("request was not queued")
}
connection.RespMutex.RLock()
response := connection.Resp[request.ID]
connection.RespMutex.RUnlock()
if response == nil {
t.Fatal("request response waiter was not registered")
}
select {
case err := <-result:
if !errors.Is(err, ErrImplantTimeout) {
t.Fatalf("Request error = %v, want %v", err, ErrImplantTimeout)
}
case <-time.After(time.Second):
t.Fatal("Request did not time out")
}
// A transport may have looked up the waiter immediately before the request
// deadline removed it from the map. The single-message buffer guarantees
// that this stale delivery still cannot block that transport reader.
select {
case response <- &sliverpb.Envelope{ID: request.ID}:
default:
t.Fatal("late response blocked on a stale waiter")
}
if connection.DeliverResponse(&sliverpb.Envelope{ID: request.ID}) {
t.Fatal("late response was delivered after waiter cleanup")
}
}
func TestSessionRequestContextPreCanceledDoesNotInstallWaiter(t *testing.T) {
connection := NewImplantConnection("test", "test")
session := &Session{Connection: connection}
ctx, cancel := context.WithCancel(context.Background())
cancel()
_, err := session.RequestContext(ctx, sliverpb.MsgPing, nil)
if !errors.Is(err, context.Canceled) {
t.Fatalf("RequestContext error = %v, want %v", err, context.Canceled)
}
connection.RespMutex.RLock()
waiters := len(connection.Resp)
connection.RespMutex.RUnlock()
if waiters != 0 {
t.Fatalf("response waiter count = %d, want 0", waiters)
}
select {
case envelope := <-connection.Send:
t.Fatalf("pre-canceled request queued envelope: %#v", envelope)
default:
}
}
func TestSessionRequestContextSharesSendAndResponseDeadline(t *testing.T) {
connection := NewImplantConnection("test", "test")
session := &Session{Connection: connection}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
started := time.Now()
result := make(chan error, 1)
go func() {
_, err := session.RequestContext(ctx, sliverpb.MsgPing, nil)
result <- err
}()
// Consume most of the request budget before allowing the outbound envelope
// to queue. The response wait must use only the remainder of that same
// budget, rather than starting a second timeout window.
time.Sleep(600 * time.Millisecond)
select {
case <-connection.Send:
case <-time.After(200 * time.Millisecond):
t.Fatal("request was not queued within its remaining context budget")
}
select {
case err := <-result:
if !errors.Is(err, context.DeadlineExceeded) {
t.Fatalf("RequestContext error = %v, want %v", err, context.DeadlineExceeded)
}
case <-time.After(750 * time.Millisecond):
t.Fatal("response wait started a second timeout window")
}
if elapsed := time.Since(started); elapsed > 1350*time.Millisecond {
t.Fatalf("shared deadline elapsed = %v, want at most 1.35s", elapsed)
}
connection.RespMutex.RLock()
waiters := len(connection.Resp)
connection.RespMutex.RUnlock()
if waiters != 0 {
t.Fatalf("response waiter count = %d, want 0", waiters)
}
}
func TestSessionRemovalClosesOwnedGenericTunnels(t *testing.T) {
session := newTestSession()
Sessions.Add(session)
tunnel, err := Tunnels.Create(session.ID)
if err != nil {
t.Fatal(err)
}
Sessions.Remove(session.ID)
if Sessions.Get(session.ID) != nil {
t.Fatal("removed session remains registered")
}
if Tunnels.Get(tunnel.ID) != nil {
t.Fatal("generic tunnel survived owning session removal")
}
select {
case <-tunnel.Done():
default:
t.Fatal("generic tunnel did not publish closed state on session removal")
}
}
func TestSessionsRemoveIfPreservesReplacementGeneration(t *testing.T) {
registry := &sessions{sessions: &sync.Map{}}
original := newTestSession()
replacement := newTestSession()
replacement.ID = original.ID
registry.Add(original)
registry.Add(replacement)
if registry.RemoveIf(original) {
t.Fatal("generation-scoped removal deleted a replacement session")
}
if got := registry.Get(original.ID); got != replacement {
t.Fatalf("session after stale removal = %p, want replacement %p", got, replacement)
}
if !registry.RemoveIf(replacement) {
t.Fatal("generation-scoped removal rejected the registered session")
}
if got := registry.Get(original.ID); got != nil {
t.Fatalf("registered session survived exact removal: %p", got)
}
}
func TestSessionRemovalClosesOwnedSocksTunnels(t *testing.T) {
session := newTestSession()
otherSession := newTestSession()
Sessions.Add(session)
Sessions.Add(otherSession)
defer Sessions.Remove(otherSession.ID)
owned, err := SocksTunnels.Create(session.ID)
if err != nil {
t.Fatal(err)
}
other, err := SocksTunnels.Create(otherSession.ID)
if err != nil {
t.Fatal(err)
}
Sessions.Remove(session.ID)
if Sessions.Get(session.ID) != nil {
t.Fatal("removed session remains registered")
}
if SocksTunnels.Get(owned.ID) != nil {
t.Fatal("SOCKS tunnel survived owning session removal")
}
select {
case <-owned.Done():
default:
t.Fatal("SOCKS tunnel did not publish closed state on session removal")
}
if got := SocksTunnels.Get(other.ID); got != other {
t.Fatalf("unrelated SOCKS tunnel = %p, want %p", got, other)
}
select {
case <-other.Done():
t.Fatal("unrelated session SOCKS tunnel was closed")
default:
}
}
+965 -25
View File
File diff suppressed because it is too large. Load diff
+830
View File
@@ -0,0 +1,830 @@
package core
import (
"context"
"errors"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/bishopfox/sliver/protobuf/commonpb"
"github.com/bishopfox/sliver/protobuf/sliverpb"
)
func newUnitSocksTunnel(id uint64, sessionID string) *TcpTunnel {
return newTcpTunnel(id, sessionID, nil)
}
func TestTcpTunnelDeliverFromImplantCloseRace(t *testing.T) {
const iterations = 1000
for iteration := 0; iteration < iterations; iteration++ {
tunnel := newUnitSocksTunnel(uint64(iteration+1), "session")
start := make(chan struct{})
delivered := make(chan bool, 1)
closed := make(chan bool, 1)
go func() {
<-start
delivered <- tunnel.DeliverFromImplant(&sliverpb.SocksData{TunnelID: tunnel.ID})
}()
go func() {
<-start
closed <- tunnel.close()
}()
close(start)
select {
case <-delivered:
case <-time.After(time.Second):
t.Fatalf("iteration %d: implant delivery blocked during close", iteration)
}
select {
case firstClose := <-closed:
if !firstClose {
t.Fatalf("iteration %d: first tunnel close returned false", iteration)
}
case <-time.After(time.Second):
t.Fatalf("iteration %d: tunnel close blocked during delivery", iteration)
}
if tunnel.DeliverFromImplant(&sliverpb.SocksData{TunnelID: tunnel.ID}) {
t.Fatalf("iteration %d: closed tunnel accepted implant data", iteration)
}
}
}
func TestSocksTunnelRegistryCloseIfIsGenerationSafeAndIdempotent(t *testing.T) {
registry := tcpTunnel{tunnels: map[uint64]*TcpTunnel{}, mutex: &sync.RWMutex{}}
old := newUnitSocksTunnel(7, "session")
current := newUnitSocksTunnel(7, "session")
registry.tunnels[7] = current
if registry.CloseIf(old) {
t.Fatal("stale SOCKS tunnel generation closed the current tunnel")
}
if got := registry.Get(7); got != current {
t.Fatalf("stale close changed registry: got=%p want=%p", got, current)
}
if !registry.CloseIf(current) {
t.Fatal("current SOCKS tunnel generation did not close")
}
if registry.CloseIf(current) {
t.Fatal("second SOCKS tunnel close returned true")
}
if got := registry.Get(7); got != nil {
t.Fatalf("closed SOCKS tunnel remained registered: %p", got)
}
select {
case <-current.Done():
default:
t.Fatal("closed SOCKS tunnel did not signal done")
}
}
func TestTcpTunnelClientBindingIsExclusive(t *testing.T) {
tunnel := newUnitSocksTunnel(11, "session")
first := NewSocksClient(&trackingSocksSender{})
second := NewSocksClient(&trackingSocksSender{})
if owned, newlyBound := tunnel.BindClient(first); !owned || !newlyBound {
t.Fatalf("first bind = owned:%v new:%v, want true,true", owned, newlyBound)
}
if owned, newlyBound := tunnel.BindClient(first); !owned || newlyBound {
t.Fatalf("repeat owner bind = owned:%v new:%v, want true,false", owned, newlyBound)
}
if owned, newlyBound := tunnel.BindClient(second); owned || newlyBound {
t.Fatalf("second client bind = owned:%v new:%v, want false,false", owned, newlyBound)
}
tunnel.close()
if owned, newlyBound := tunnel.BindClient(first); owned || newlyBound {
t.Fatalf("closed tunnel bind = owned:%v new:%v, want false,false", owned, newlyBound)
}
}
//nolint:gocyclo // The test intentionally checks every lifecycle snapshot transition.
func TestTcpTunnelTracksLifecycleCapabilityAndFirstPayload(t *testing.T) {
tunnel := newUnitSocksTunnel(111, "session")
client := NewSocksClient(&trackingSocksSender{})
if owned, newlyBound, err := tunnel.BindClientWithCapabilities(client, "user", "password", true); err != nil || !owned || !newlyBound {
t.Fatalf("capability bind = owned:%v new:%v err:%v", owned, newlyBound, err)
}
bound := tunnel.ClientLifecycle()
if bound.BoundAt.IsZero() || bound.LastActivity.IsZero() || bound.ReceivedPayload || !bound.SendsTerminal {
t.Fatalf("initial lifecycle = %+v", bound)
}
if err := tunnel.AdmitToImplant(&sliverpb.SocksData{TunnelID: tunnel.ID, Sequence: 0, Data: []byte("greeting")}); err != nil {
t.Fatalf("admit first payload: %v", err)
}
afterPayload := tunnel.ClientLifecycle()
if !afterPayload.ReceivedPayload || !afterPayload.SendsTerminal || afterPayload.LastActivity.Before(bound.LastActivity) {
t.Fatalf("lifecycle after first payload = %+v, initial %+v", afterPayload, bound)
}
legacy := newUnitSocksTunnel(112, "session")
if owned, newlyBound, err := legacy.BindClientWithCredentials(client, "", ""); err != nil || !owned || !newlyBound {
t.Fatalf("legacy bind = owned:%v new:%v err:%v", owned, newlyBound, err)
}
if lifecycle := legacy.ClientLifecycle(); lifecycle.SendsTerminal {
t.Fatalf("legacy lifecycle unexpectedly advertises terminals: %+v", lifecycle)
}
}
//nolint:gocyclo // Capability ownership and both acknowledgement directions form one scenario.
func TestTcpTunnelNegotiatesCapabilitiesAndCoalescesOwnedAcknowledgements(t *testing.T) {
connection := NewImplantConnection("flow-control", "test")
t.Cleanup(connection.Close)
tunnel := newTCPTunnelWithCapabilities(113, "session", connection, sliverpb.CapabilitySocksFlowControlV1)
t.Cleanup(func() { tunnel.close() })
client := NewSocksClient(&trackingSocksSender{})
otherClient := NewSocksClient(&trackingSocksSender{})
if owned, newlyBound, err := tunnel.BindClientWithCapabilities(client, "", "", true); !errors.Is(err, ErrSocksCapabilityMismatch) || owned || newlyBound {
t.Fatalf("bind without negotiated capability echo = owned:%v new:%v err:%v", owned, newlyBound, err)
}
if owned, newlyBound, err := tunnel.BindClientWithNegotiatedCapabilities(client, "user", "password", true, sliverpb.CapabilitySocksFlowControlV1); err != nil || !owned || !newlyBound {
t.Fatalf("negotiated capability bind = owned:%v new:%v err:%v", owned, newlyBound, err)
}
if got := tunnel.Capabilities(); got != sliverpb.CapabilitySocksFlowControlV1 || !tunnel.FlowControlEnabled() {
t.Fatalf("negotiated capabilities = %d enabled:%v", got, tunnel.FlowControlEnabled())
}
atomic.StoreUint64(&tunnel.FromImplantSequence, 3)
for _, ack := range []uint64{1, 3, 2} {
if err := tunnel.RelayClientAcknowledgement(client, ack); err != nil {
t.Fatalf("relay client acknowledgement %d: %v", ack, err)
}
}
select {
case ack := <-tunnel.AcknowledgementsToImplant():
if ack != 3 {
t.Fatalf("coalesced client acknowledgement = %d, want 3", ack)
}
default:
t.Fatal("client acknowledgement mailbox was empty")
}
select {
case ack := <-tunnel.AcknowledgementsToImplant():
t.Fatalf("client acknowledgement mailbox retained duplicate %d", ack)
default:
}
if err := tunnel.RelayClientAcknowledgement(otherClient, 1); !errors.Is(err, ErrSocksOwner) {
t.Fatalf("wrong-client acknowledgement = %v, want %v", err, ErrSocksOwner)
}
if err := tunnel.RelayClientAcknowledgement(client, 0); !errors.Is(err, ErrSocksAcknowledgement) {
t.Fatalf("zero client acknowledgement = %v, want %v", err, ErrSocksAcknowledgement)
}
if err := tunnel.RelayClientAcknowledgement(client, 4); !errors.Is(err, ErrSocksAcknowledgement) {
t.Fatalf("future client acknowledgement = %v, want %v", err, ErrSocksAcknowledgement)
}
atomic.StoreUint64(&tunnel.ToImplantSequence, 4)
for _, ack := range []uint64{2, 4, 3} {
if err := tunnel.RelayImplantAcknowledgement(connection, ack); err != nil {
t.Fatalf("relay implant acknowledgement %d: %v", ack, err)
}
}
select {
case ack := <-tunnel.AcknowledgementsToClient():
if ack != 4 {
t.Fatalf("coalesced implant acknowledgement = %d, want 4", ack)
}
default:
t.Fatal("implant acknowledgement mailbox was empty")
}
wrongConnection := NewImplantConnection("wrong-flow-owner", "test")
t.Cleanup(wrongConnection.Close)
if err := tunnel.RelayImplantAcknowledgement(wrongConnection, 1); !errors.Is(err, ErrSocksOwner) {
t.Fatalf("wrong-implant acknowledgement = %v, want %v", err, ErrSocksOwner)
}
if err := tunnel.RelayImplantAcknowledgement(connection, 5); !errors.Is(err, ErrSocksAcknowledgement) {
t.Fatalf("future implant acknowledgement = %v, want %v", err, ErrSocksAcknowledgement)
}
select {
case frame := <-tunnel.ToImplant():
t.Fatalf("acknowledgement entered to-implant data queue: %+v", frame)
default:
}
select {
case frame := <-tunnel.FromImplant():
t.Fatalf("acknowledgement entered from-implant data queue: %+v", frame)
default:
}
}
func TestTcpTunnelRejectsAcknowledgementsWithoutFlowNegotiation(t *testing.T) {
connection := NewImplantConnection("legacy-flow", "test")
t.Cleanup(connection.Close)
tunnel := newTcpTunnel(114, "session", connection)
t.Cleanup(func() { tunnel.close() })
client := NewSocksClient(&trackingSocksSender{})
if owned, newlyBound := tunnel.BindClient(client); !owned || !newlyBound {
t.Fatalf("legacy bind = owned:%v new:%v", owned, newlyBound)
}
atomic.StoreUint64(&tunnel.FromImplantSequence, 1)
atomic.StoreUint64(&tunnel.ToImplantSequence, 1)
if err := tunnel.RelayClientAcknowledgement(client, 1); !errors.Is(err, ErrSocksFlowControl) {
t.Fatalf("legacy client acknowledgement = %v, want %v", err, ErrSocksFlowControl)
}
if err := tunnel.RelayImplantAcknowledgement(connection, 1); !errors.Is(err, ErrSocksFlowControl) {
t.Fatalf("legacy implant acknowledgement = %v, want %v", err, ErrSocksFlowControl)
}
}
func TestSocksTunnelRegistryBoundsPerSessionPreGreetingState(t *testing.T) {
connection := NewImplantConnection("test", "socks-capacity")
session := NewSession(connection)
Sessions.Add(session)
registry := tcpTunnel{tunnels: map[uint64]*TcpTunnel{}, mutex: &sync.RWMutex{}}
for index := 0; index < maxSocksTunnelsPerSession; index++ {
tunnel := newTcpTunnel(uint64(index+1), session.ID, connection)
registry.tunnels[tunnel.ID] = tunnel
}
t.Cleanup(func() {
for _, tunnel := range registry.List() {
registry.CloseIf(tunnel)
}
Sessions.Remove(session.ID)
connection.Close()
})
if tunnel, err := registry.Create(session.ID); !errors.Is(err, ErrSocksTunnelLimit) || tunnel != nil {
t.Fatalf("create beyond per-session limit = tunnel:%p err:%v, want nil/%v", tunnel, err, ErrSocksTunnelLimit)
}
registry.CloseIf(registry.tunnels[1])
if tunnel, err := registry.Create(session.ID); err != nil || tunnel == nil {
t.Fatalf("create after releasing capacity = tunnel:%p err:%v", tunnel, err)
}
}
func TestSocksTunnelCreateForSessionRejectsReplacedGeneration(t *testing.T) {
originalConnection := NewImplantConnection("original-generation", "test")
original := NewSession(originalConnection)
Sessions.Add(original)
replacementConnection := NewImplantConnection("replacement-generation", "test")
replacement := NewSession(replacementConnection)
replacement.ID = original.ID
Sessions.Add(replacement)
t.Cleanup(func() {
Sessions.RemoveIf(replacement)
originalConnection.Close()
replacementConnection.Close()
})
registry := tcpTunnel{tunnels: map[uint64]*TcpTunnel{}, mutex: &sync.RWMutex{}}
tunnel, err := registry.CreateForSession(original, sliverpb.CapabilitySocksFlowControlV1)
if !errors.Is(err, ErrInvalidSessionID) || tunnel != nil {
t.Fatalf("create with replaced session = tunnel:%p err:%v, want nil/%v", tunnel, err, ErrInvalidSessionID)
}
if got := len(registry.List()); got != 0 {
t.Fatalf("replaced generation published %d SOCKS tunnels", got)
}
tunnel, err = registry.CreateForSession(replacement, sliverpb.CapabilitySocksFlowControlV1)
if err != nil || tunnel == nil {
t.Fatalf("create with exact replacement = tunnel:%p err:%v", tunnel, err)
}
if tunnel.ImplantConnection() != replacementConnection || tunnel.Capabilities() != sliverpb.CapabilitySocksFlowControlV1 {
t.Fatalf("replacement tunnel owner/capabilities = %p/%d", tunnel.ImplantConnection(), tunnel.Capabilities())
}
registry.CloseIf(tunnel)
}
//nolint:gocyclo // The test keeps bind, credential, and budget invariants in one scenario.
func TestTcpTunnelCredentialsAreBoundOnceAndExcludedFromFrameBudget(t *testing.T) {
tunnel := newUnitSocksTunnel(12, "session")
client := NewSocksClient(&trackingSocksSender{})
if owned, newlyBound, err := tunnel.BindClientWithCredentials(client, "user", "password"); err != nil || !owned || !newlyBound {
t.Fatalf("credential bind = owned:%v new:%v err:%v", owned, newlyBound, err)
}
if owned, newlyBound, err := tunnel.BindClientWithCredentials(client, string(make([]byte, maxSocksCredentialBytes+1)), "replacement"); err != nil || !owned || newlyBound {
t.Fatalf("repeat bind metadata = owned:%v new:%v err:%v", owned, newlyBound, err)
}
if username, password := tunnel.Credentials(); username != "user" || password != "password" {
t.Fatalf("later metadata replaced credentials: %q/%q", username, password)
}
requestMetadata := string(make([]byte, MaxSocksFrameBytes))
if err := tunnel.AdmitToImplant(&sliverpb.SocksData{
TunnelID: tunnel.ID,
Sequence: 1,
Data: []byte("counted"),
Username: requestMetadata,
Password: requestMetadata,
Request: &commonpb.Request{SessionID: requestMetadata},
}); err != nil {
t.Fatalf("admit metadata-bearing frame: %v", err)
}
tunnel.toImplant.mu.Lock()
pending := tunnel.toImplant.pending[1]
tunnel.toImplant.mu.Unlock()
if pending == nil || pending.Username != "" || pending.Password != "" || pending.Request != nil {
t.Fatal("pending queue retained arbitrary metadata")
}
_, frames, size := tunnel.toImplant.snapshot()
if frames != 1 || size != len("counted") {
t.Fatalf("metadata inflated queue budget: frames=%d bytes=%d", frames, size)
}
if !tunnel.close() {
t.Fatal("close credential-bearing tunnel")
}
if username, password := tunnel.Credentials(); username != "" || password != "" {
t.Fatalf("closed tunnel retained credentials: %q/%q", username, password)
}
unbound := newUnitSocksTunnel(13, "session")
tooLong := string(make([]byte, maxSocksCredentialBytes+1))
if owned, newlyBound, err := unbound.BindClientWithCredentials(client, tooLong, ""); !errors.Is(err, ErrSocksCredentialSize) || owned || newlyBound {
t.Fatalf("oversized credential bind = owned:%v new:%v err:%v", owned, newlyBound, err)
}
if unbound.Client() != nil {
t.Fatal("oversized credentials claimed tunnel")
}
}
type trackingSocksSender struct {
inFlight atomic.Int32
overlap atomic.Bool
calls atomic.Int32
}
func (sender *trackingSocksSender) Send(*sliverpb.SocksData) error {
if sender.inFlight.Add(1) != 1 {
sender.overlap.Store(true)
}
time.Sleep(time.Millisecond)
sender.calls.Add(1)
sender.inFlight.Add(-1)
return nil
}
func TestSocksClientSerializesConcurrentSends(t *testing.T) {
const sendCount = 32
sender := &trackingSocksSender{}
client := NewSocksClient(sender)
start := make(chan struct{})
var workers sync.WaitGroup
for index := 0; index < sendCount; index++ {
workers.Add(1)
go func(index int) {
defer workers.Done()
<-start
if err := client.Send(&sliverpb.SocksData{TunnelID: uint64(index + 1)}); err != nil {
t.Errorf("send %d: %v", index, err)
}
}(index)
}
close(start)
workers.Wait()
if sender.overlap.Load() {
t.Fatal("SOCKS client stream observed concurrent Send calls")
}
if got := sender.calls.Load(); got != sendCount {
t.Fatalf("serialized send calls = %d, want %d", got, sendCount)
}
}
func TestSocksFrameQueueRejectsFarFutureAndConflictingDuplicate(t *testing.T) {
tunnel := newUnitSocksTunnel(21, "session")
ready := &sliverpb.SocksData{TunnelID: tunnel.ID, Sequence: 0, Data: []byte("ready")}
if err := tunnel.AdmitToImplant(ready); err != nil {
t.Fatalf("admit ready frame: %v", err)
}
if err := tunnel.AdmitToImplant(ready); err != nil {
t.Fatalf("byte-identical ready duplicate = %v, want nil", err)
}
if err := tunnel.AdmitToImplant(&sliverpb.SocksData{TunnelID: tunnel.ID, Sequence: 0, Data: []byte("ready conflict")}); !errors.Is(err, ErrSocksSequenceConflict) {
t.Fatalf("conflicting ready duplicate = %v, want ErrSocksSequenceConflict", err)
}
if err := tunnel.ProcessDataFromImplant(&sliverpb.SocksData{
TunnelID: tunnel.ID,
Sequence: 1,
Data: []byte("future"),
}); err != nil {
t.Fatalf("admit future frame: %v", err)
}
if err := tunnel.ProcessDataFromImplant(&sliverpb.SocksData{
TunnelID: tunnel.ID,
Sequence: 1,
Data: []byte("future"),
}); err != nil {
t.Fatalf("byte-identical duplicate = %v, want nil", err)
}
if err := tunnel.ProcessDataFromImplant(&sliverpb.SocksData{
TunnelID: tunnel.ID,
Sequence: 1,
Data: []byte("conflict"),
}); !errors.Is(err, ErrSocksSequenceConflict) {
t.Fatalf("conflicting duplicate = %v, want ErrSocksSequenceConflict", err)
}
if err := tunnel.ProcessDataFromImplant(&sliverpb.SocksData{
TunnelID: tunnel.ID,
Sequence: maxSocksPendingFrames,
}); !errors.Is(err, ErrTunnelSequenceWindow) {
t.Fatalf("far-future frame = %v, want ErrTunnelSequenceWindow", err)
}
_, frames, size := tunnel.fromImplant.snapshot()
if frames != 1 || size != len("future") {
t.Fatalf("duplicate/invalid frames changed reservation: frames=%d bytes=%d", frames, size)
}
}
func TestSocksFrameQueueIgnoresStaleReplayAndDropsRequestMetadata(t *testing.T) {
tunnel := newUnitSocksTunnel(22, "session")
frame := &sliverpb.SocksData{
TunnelID: tunnel.ID,
Sequence: 0,
Data: []byte("payload"),
Request: &commonpb.Request{SessionID: "credential-bearing-metadata"},
}
if err := tunnel.AdmitToImplant(frame); err != nil {
t.Fatalf("admit operator frame: %v", err)
}
ordered := <-tunnel.ToImplant()
if ordered.Request != nil {
t.Fatalf("ordered frame retained request metadata: %+v", ordered.Request)
}
if ordered.TunnelID != tunnel.ID || ordered.Sequence != 0 || string(ordered.Data) != "payload" {
t.Fatalf("ordered frame = %+v", ordered)
}
tunnel.CompleteToImplant(ordered)
if err := tunnel.AdmitToImplant(&sliverpb.SocksData{
TunnelID: tunnel.ID,
Sequence: 0,
Data: []byte("different stale payload"),
}); err != nil {
t.Fatalf("stale replay = %v, want nil", err)
}
select {
case replay := <-tunnel.ToImplant():
t.Fatalf("stale replay was queued: %+v", replay)
default:
}
_, frames, size := tunnel.toImplant.snapshot()
if frames != 0 || size != 0 {
t.Fatalf("stale replay retained reservation: frames=%d bytes=%d", frames, size)
}
}
func TestSocksFrameQueueCompletionUsesAdmissionSize(t *testing.T) {
queue := newSocksFrameQueue(1, 4)
if err := queue.admit(22, &sliverpb.SocksData{Data: []byte("four")}); err != nil {
t.Fatalf("admit frame: %v", err)
}
frame := <-queue.ready
frame.Data = frame.Data[:1]
queue.complete(frame)
if _, frames, size := queue.snapshot(); frames != 0 || size != 0 {
t.Fatalf("mutated completed frame retained reservation: frames=%d bytes=%d", frames, size)
}
}
func TestSocksFrameQueueBoundsFrameCountAndBytes(t *testing.T) {
if MaxSocksFrameBytes != 64*1024 || maxSocksPendingFrames != 128 || maxSocksPendingBytes != 8*1024*1024 {
t.Fatalf("SOCKS framing limits = payload:%d frames:%d bytes:%d", MaxSocksFrameBytes, maxSocksPendingFrames, maxSocksPendingBytes)
}
tunnel := newUnitSocksTunnel(23, "session")
// Retain half the window in the ready channel and half behind a sequence
// gap, proving the admission limit is combined rather than per container.
for sequence := uint64(0); sequence < uint64(maxSocksPendingFrames/2); sequence++ {
if err := tunnel.AdmitToImplant(&sliverpb.SocksData{
TunnelID: tunnel.ID,
Sequence: sequence,
}); err != nil {
t.Fatalf("admit frame %d: %v", sequence, err)
}
}
for sequence := uint64(maxSocksPendingFrames/2 + 1); sequence <= uint64(maxSocksPendingFrames); sequence++ {
if err := tunnel.AdmitToImplant(&sliverpb.SocksData{
TunnelID: tunnel.ID,
Sequence: sequence,
}); err != nil {
t.Fatalf("admit future frame %d: %v", sequence, err)
}
}
if err := tunnel.AdmitToImplant(&sliverpb.SocksData{
TunnelID: tunnel.ID,
Sequence: uint64(maxSocksPendingFrames / 2),
}); !errors.Is(err, ErrTunnelIngressLimit) {
t.Fatalf("129th admitted frame = %v, want ErrTunnelIngressLimit", err)
}
byteLimited := newSocksFrameQueue(maxSocksPendingFrames, 3)
if err := byteLimited.admit(tunnel.ID, &sliverpb.SocksData{Sequence: 1, Data: []byte("four")}); !errors.Is(err, ErrTunnelPendingBytes) {
t.Fatalf("frame beyond byte budget = %v, want ErrTunnelPendingBytes", err)
}
_, frames, size := byteLimited.snapshot()
if frames != 0 || size != 0 {
t.Fatalf("rejected byte-budget frame retained reservation: frames=%d bytes=%d", frames, size)
}
}
//nolint:gocyclo // Admission, wakeup, and accounting assertions are one lifecycle scenario.
func TestTcpTunnelAdmitToImplantContextWaitsForCapacityAndPreservesAccounting(t *testing.T) {
tunnel := newUnitSocksTunnel(2301, "session")
tunnel.toImplant = newSocksFrameQueue(2, 5)
t.Cleanup(func() { tunnel.close() })
if err := tunnel.AdmitToImplant(&sliverpb.SocksData{TunnelID: tunnel.ID, Sequence: 0, Data: []byte("aa")}); err != nil {
t.Fatalf("admit first frame: %v", err)
}
if err := tunnel.AdmitToImplant(&sliverpb.SocksData{TunnelID: tunnel.ID, Sequence: 1, Data: []byte("bbb")}); err != nil {
t.Fatalf("admit second frame: %v", err)
}
first := <-tunnel.ToImplant()
second := <-tunnel.ToImplant()
result := make(chan error, 1)
go func() {
result <- tunnel.AdmitToImplantContext(context.Background(), &sliverpb.SocksData{
TunnelID: tunnel.ID,
Sequence: 2,
Data: []byte("c"),
})
}()
select {
case err := <-result:
t.Fatalf("capacity waiter returned before a reservation was released: %v", err)
case <-time.After(25 * time.Millisecond):
}
if next, frames, size := tunnel.toImplant.snapshot(); next != 2 || frames != 2 || size != 5 {
t.Fatalf("full queue snapshot = next:%d frames:%d bytes:%d, want 2/2/5", next, frames, size)
}
tunnel.CompleteToImplant(first)
select {
case err := <-result:
if err != nil {
t.Fatalf("capacity waiter after release: %v", err)
}
case <-time.After(time.Second):
t.Fatal("released reservation did not wake capacity waiter")
}
third := <-tunnel.ToImplant()
if third.Sequence != 2 || string(third.Data) != "c" {
t.Fatalf("admitted frame after release = %+v", third)
}
if next, frames, size := tunnel.toImplant.snapshot(); next != 3 || frames != 2 || size != 4 {
t.Fatalf("refilled queue snapshot = next:%d frames:%d bytes:%d, want 3/2/4", next, frames, size)
}
tunnel.CompleteToImplant(second)
tunnel.CompleteToImplant(third)
if next, frames, size := tunnel.toImplant.snapshot(); next != 3 || frames != 0 || size != 0 {
t.Fatalf("drained queue snapshot = next:%d frames:%d bytes:%d, want 3/0/0", next, frames, size)
}
}
//nolint:gocyclo // Context and tunnel cancellation share the same bounded-wait contract.
func TestTcpTunnelAdmitToImplantContextCancellationKeepsBounds(t *testing.T) {
t.Run("context cancellation", func(t *testing.T) {
tunnel := newUnitSocksTunnel(2302, "session")
tunnel.toImplant = newSocksFrameQueue(2, 2)
t.Cleanup(func() { tunnel.close() })
for sequence := uint64(0); sequence < 2; sequence++ {
if err := tunnel.AdmitToImplant(&sliverpb.SocksData{TunnelID: tunnel.ID, Sequence: sequence, Data: []byte("x")}); err != nil {
t.Fatalf("admit frame %d: %v", sequence, err)
}
}
ctx, cancel := context.WithCancel(context.Background())
result := make(chan error, 1)
go func() {
result <- tunnel.AdmitToImplantContext(ctx, &sliverpb.SocksData{TunnelID: tunnel.ID, Sequence: 2, Data: []byte("x")})
}()
select {
case err := <-result:
t.Fatalf("capacity waiter returned before cancellation: %v", err)
case <-time.After(25 * time.Millisecond):
}
cancel()
select {
case err := <-result:
if !errors.Is(err, context.Canceled) {
t.Fatalf("canceled capacity waiter = %v, want %v", err, context.Canceled)
}
case <-time.After(time.Second):
t.Fatal("context cancellation did not wake capacity waiter")
}
if next, frames, size := tunnel.toImplant.snapshot(); next != 2 || frames != 2 || size != 2 {
t.Fatalf("canceled queue snapshot = next:%d frames:%d bytes:%d, want 2/2/2", next, frames, size)
}
})
t.Run("tunnel cancellation", func(t *testing.T) {
tunnel := newUnitSocksTunnel(2303, "session")
tunnel.toImplant = newSocksFrameQueue(2, 2)
for sequence := uint64(0); sequence < 2; sequence++ {
if err := tunnel.AdmitToImplant(&sliverpb.SocksData{TunnelID: tunnel.ID, Sequence: sequence, Data: []byte("x")}); err != nil {
t.Fatalf("admit frame %d: %v", sequence, err)
}
}
result := make(chan error, 1)
go func() {
result <- tunnel.AdmitToImplantContext(context.Background(), &sliverpb.SocksData{TunnelID: tunnel.ID, Sequence: 2, Data: []byte("x")})
}()
select {
case err := <-result:
t.Fatalf("capacity waiter returned before tunnel cancellation: %v", err)
case <-time.After(25 * time.Millisecond):
}
if !tunnel.close() {
t.Fatal("close saturated tunnel")
}
select {
case err := <-result:
if !errors.Is(err, ErrTunnelClosed) {
t.Fatalf("closed-tunnel capacity waiter = %v, want %v", err, ErrTunnelClosed)
}
case <-time.After(time.Second):
t.Fatal("tunnel cancellation did not wake capacity waiter")
}
if _, frames, size := tunnel.toImplant.snapshot(); frames != 0 || size != 0 {
t.Fatalf("closed queue retained frames:%d bytes:%d", frames, size)
}
})
}
func TestTcpTunnelAdmitToImplantContextKeepsProtocolErrorsFailFast(t *testing.T) {
tunnel := newUnitSocksTunnel(2304, "session")
tunnel.toImplant = newSocksFrameQueue(2, 2)
t.Cleanup(func() { tunnel.close() })
for sequence := uint64(0); sequence < 2; sequence++ {
if err := tunnel.AdmitToImplant(&sliverpb.SocksData{TunnelID: tunnel.ID, Sequence: sequence, Data: []byte("x")}); err != nil {
t.Fatalf("admit frame %d: %v", sequence, err)
}
}
tests := []struct {
name string
frame *sliverpb.SocksData
want error
}{
{name: "conflicting duplicate", frame: &sliverpb.SocksData{TunnelID: tunnel.ID, Sequence: 0, Data: []byte("y")}, want: ErrSocksSequenceConflict},
{name: "outside sequence window", frame: &sliverpb.SocksData{TunnelID: tunnel.ID, Sequence: 4, Data: []byte("x")}, want: ErrTunnelSequenceWindow},
{name: "oversized frame", frame: &sliverpb.SocksData{TunnelID: tunnel.ID, Sequence: 2, Data: make([]byte, MaxSocksFrameBytes+1)}, want: ErrTunnelFrameTooLarge},
{name: "terminal payload", frame: &sliverpb.SocksData{TunnelID: tunnel.ID, Sequence: 2, Data: []byte("x"), CloseConn: true}, want: ErrSocksTerminalPayload},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
if err := tunnel.AdmitToImplantContext(ctx, test.frame); !errors.Is(err, test.want) {
t.Fatalf("admission error = %v, want %v", err, test.want)
}
})
}
impossible := newUnitSocksTunnel(2305, "session")
impossible.toImplant = newSocksFrameQueue(2, 1)
t.Cleanup(func() { impossible.close() })
if err := impossible.AdmitToImplantContext(context.Background(), &sliverpb.SocksData{
TunnelID: impossible.ID,
Sequence: 0,
Data: []byte("xx"),
}); !errors.Is(err, ErrTunnelPendingBytes) {
t.Fatalf("permanently over-budget frame = %v, want %v", err, ErrTunnelPendingBytes)
}
}
func TestSocksFrameQueueRejectsOversizedPayloadAndCloseReleasesState(t *testing.T) {
tunnel := newUnitSocksTunnel(24, "session")
if err := tunnel.ProcessDataFromImplant(&sliverpb.SocksData{
TunnelID: tunnel.ID,
Sequence: 0,
Data: make([]byte, MaxSocksFrameBytes+1),
}); !errors.Is(err, ErrTunnelFrameTooLarge) {
t.Fatalf("oversized payload = %v, want ErrTunnelFrameTooLarge", err)
}
terminalWithData := &sliverpb.SocksData{
TunnelID: tunnel.ID,
Sequence: 0,
Data: []byte("not allowed on terminal"),
CloseConn: true,
}
if err := tunnel.AdmitToImplant(terminalWithData); !errors.Is(err, ErrSocksTerminalPayload) {
t.Fatalf("operator terminal payload = %v, want ErrSocksTerminalPayload", err)
}
if err := tunnel.ProcessDataFromImplant(terminalWithData); !errors.Is(err, ErrSocksTerminalPayload) {
t.Fatalf("implant terminal payload = %v, want ErrSocksTerminalPayload", err)
}
if err := tunnel.ProcessDataFromImplant(&sliverpb.SocksData{TunnelID: tunnel.ID, Sequence: 1, Data: []byte("pending")}); err != nil {
t.Fatalf("admit pending frame: %v", err)
}
if err := tunnel.AdmitToImplant(&sliverpb.SocksData{TunnelID: tunnel.ID, Sequence: 0, Data: []byte("ready")}); err != nil {
t.Fatalf("admit ready frame: %v", err)
}
if !tunnel.close() {
t.Fatal("close tunnel")
}
for name, queue := range map[string]*socksFrameQueue{"to implant": tunnel.toImplant, "from implant": tunnel.fromImplant} {
_, frames, size := queue.snapshot()
if frames != 0 || size != 0 {
t.Fatalf("%s queue retained frames=%d bytes=%d", name, frames, size)
}
}
}
func TestSocksOrderingStateIsScopedToExactTunnelGeneration(t *testing.T) {
old := newUnitSocksTunnel(25, "session")
if err := old.ProcessDataFromImplant(&sliverpb.SocksData{
TunnelID: old.ID,
Sequence: 1,
Data: []byte("old pending"),
}); err != nil {
t.Fatalf("admit old generation frame: %v", err)
}
old.close()
current := newUnitSocksTunnel(old.ID, old.SessionID)
if err := current.ProcessDataFromImplant(&sliverpb.SocksData{
TunnelID: current.ID,
Sequence: 0,
Data: []byte("current"),
}); err != nil {
t.Fatalf("admit current generation frame: %v", err)
}
select {
case frame := <-current.FromImplant():
current.CompleteFromImplant(frame)
if string(frame.Data) != "current" || frame.Sequence != 0 {
t.Fatalf("current generation frame = %+v", frame)
}
case <-time.After(time.Second):
t.Fatal("old generation ordering state blocked current generation")
}
if err := old.ProcessDataFromImplant(&sliverpb.SocksData{TunnelID: old.ID, Sequence: 0}); !errors.Is(err, ErrTunnelClosed) {
t.Fatalf("old generation accepted delayed frame: %v", err)
}
}
// FuzzSocksTunnelAdmissionState exercises only the server framing actor: it
// performs no network operations or untrusted dials and is safe to run with
// `go test -fuzz=FuzzSocksTunnelAdmissionState ./server/core`.
//
//nolint:gocyclo // The fuzz target validates all admission-state invariants together.
func FuzzSocksTunnelAdmissionState(f *testing.F) {
f.Add([]byte{0x00, 0x00, 'a', 0x01, 0x01, 'b', 0x10, 0x00, 'c'})
f.Add([]byte{0x00, 0x01, 'a', 0x00, 0x01, 'b'}) // conflicting pending duplicate
f.Add([]byte{0x00, 0x80, 'a'}) // edge of the future window
// Admission rejects this immutable probe before retaining or modifying it.
// Reuse it so every fuzz execution does not allocate another 64 KiB buffer.
oversized := make([]byte, MaxSocksFrameBytes+1)
f.Fuzz(func(t *testing.T, operations []byte) {
tunnel := newUnitSocksTunnel(0xfeed, "fuzz-session")
if len(operations) > 3*maxSocksPendingFrames {
operations = operations[:3*maxSocksPendingFrames]
}
for offset := 0; offset+2 < len(operations); offset += 3 {
flags := operations[offset]
sequence := uint64(operations[offset+1])
payload := []byte{operations[offset+2]}
if flags&0x40 != 0 {
payload = oversized
}
frame := &sliverpb.SocksData{
TunnelID: tunnel.ID,
Sequence: sequence,
Data: payload,
CloseConn: flags&0x20 != 0,
}
queue := tunnel.toImplant
var err error
if flags&0x80 == 0 {
err = tunnel.AdmitToImplant(frame)
} else {
queue = tunnel.fromImplant
err = tunnel.ProcessDataFromImplant(frame)
}
if len(payload) > MaxSocksFrameBytes && !errors.Is(err, ErrTunnelFrameTooLarge) {
t.Fatalf("oversized fuzz frame error = %v, want ErrTunnelFrameTooLarge", err)
}
_, frames, size := queue.snapshot()
if frames < 0 || frames > maxSocksPendingFrames || size < 0 || size > maxSocksPendingBytes {
t.Fatalf("unbounded queue state: frames=%d bytes=%d err=%v", frames, size, err)
}
if flags&0x10 != 0 {
select {
case ready := <-queue.ready:
queue.complete(ready)
default:
}
}
}
if !tunnel.close() {
t.Fatal("fuzz tunnel did not close")
}
for _, queue := range []*socksFrameQueue{tunnel.toImplant, tunnel.fromImplant} {
_, frames, size := queue.snapshot()
if frames != 0 || size != 0 {
t.Fatalf("closed fuzz queue retained frames=%d bytes=%d", frames, size)
}
}
if err := tunnel.AdmitToImplant(&sliverpb.SocksData{}); !errors.Is(err, ErrTunnelClosed) {
t.Fatalf("closed fuzz tunnel admission = %v, want ErrTunnelClosed", err)
}
})
}
+929 -67
View File
File diff suppressed because it is too large. Load diff
+909 -16
View File
@@ -1,11 +1,13 @@
package core
import (
"errors"
"sync"
"testing"
"time"
"github.com/bishopfox/sliver/protobuf/rpcpb"
"github.com/bishopfox/sliver/protobuf/sliverpb"
)
type testTunnelDataServer struct {
@@ -42,6 +44,70 @@ func TestTunnelsCreateKnownSession(t *testing.T) {
if tunnel.SessionID != session.ID {
t.Fatalf("expected SessionID %q, got %q", session.ID, tunnel.SessionID)
}
if tunnel.ImplantConnection() != session.Connection {
t.Fatalf("expected creating implant connection %p, got %p", session.Connection, tunnel.ImplantConnection())
}
}
func TestTunnelsCreateCapturesExactTerminalCapability(t *testing.T) {
session := newTestSession()
session.Capabilities = sliverpb.CapabilityTunnelTerminalV1
Sessions.Add(session)
t.Cleanup(func() {
Sessions.RemoveIf(session)
session.Connection.Close()
})
tunnel, err := Tunnels.Create(session.ID)
if err != nil {
t.Fatalf("create capable tunnel: %v", err)
}
t.Cleanup(func() { Tunnels.CloseIf(tunnel) })
if !tunnel.TunnelTerminalEnabled() {
t.Fatal("created tunnel did not retain the exact implant terminal capability")
}
// Capability belongs to the creating transport generation, not a mutable
// session snapshot consulted during later teardown.
session.Capabilities = 0
if !tunnel.TunnelTerminalEnabled() {
t.Fatal("tunnel terminal capability changed with the session snapshot")
}
}
func TestTunnelImplantConnectionSurvivesSessionReplacement(t *testing.T) {
creatingSession := newTestSession()
Sessions.Add(creatingSession)
t.Cleanup(func() {
if Sessions.Get(creatingSession.ID) == creatingSession {
Sessions.RemoveIf(creatingSession)
}
creatingSession.Connection.Close()
})
tunnel, err := Tunnels.Create(creatingSession.ID)
if err != nil {
t.Fatalf("create tunnel: %v", err)
}
t.Cleanup(func() { Tunnels.CloseIf(tunnel) })
replacement := newTestSession()
replacement.ID = creatingSession.ID
Sessions.Add(replacement)
t.Cleanup(func() {
Sessions.Remove(replacement.ID)
replacement.Connection.Close()
})
if tunnel.ImplantConnection() != creatingSession.Connection {
t.Fatalf("tunnel implant connection after session replacement = %p, want creating connection %p", tunnel.ImplantConnection(), creatingSession.Connection)
}
if tunnel.ImplantConnection() == replacement.Connection {
t.Fatal("tunnel implant connection changed to the replacement generation")
}
if got := Sessions.Get(creatingSession.ID); got != replacement {
t.Fatalf("registered session after replacement = %p, want %p", got, replacement)
}
}
func TestTunnelsCloseUnboundTunnelDoesNotBlock(t *testing.T) {
@@ -84,6 +150,44 @@ func TestTunnelsCloseUnboundTunnelDoesNotBlock(t *testing.T) {
}
}
func TestTunnelsCloseForSessionIsScopedAndIdempotent(t *testing.T) {
first := NewTunnel(101, "disconnecting-session")
second := NewTunnel(102, "disconnecting-session")
other := NewTunnel(103, "other-session")
registry := &tunnels{
tunnels: map[uint64]*Tunnel{first.ID: first, second.ID: second, other.ID: other},
mutex: &sync.Mutex{},
}
if closed := registry.CloseForSession("disconnecting-session"); closed != 2 {
t.Fatalf("closed tunnel count = %d, want 2", closed)
}
for _, tunnel := range []*Tunnel{first, second} {
select {
case <-tunnel.Done():
default:
t.Fatalf("tunnel %d did not close with its session", tunnel.ID)
}
if got := registry.Get(tunnel.ID); got != nil {
t.Fatalf("closed tunnel %d remains registered", tunnel.ID)
}
}
if got := registry.Get(other.ID); got != other {
t.Fatalf("unrelated tunnel = %p, want %p", got, other)
}
select {
case <-other.Done():
t.Fatal("unrelated session tunnel was closed")
default:
}
if closed := registry.CloseForSession("disconnecting-session"); closed != 0 {
t.Fatalf("idempotent close count = %d, want 0", closed)
}
if closed := registry.CloseForSession(""); closed != 0 {
t.Fatalf("empty-session close count = %d, want 0", closed)
}
}
func TestTunnelSignalsClientBoundAfterAcknowledgement(t *testing.T) {
tunnel := NewTunnel(1, "test-session")
client := &testTunnelDataServer{}
@@ -113,25 +217,149 @@ func TestTunnelSignalsClientBoundAfterAcknowledgement(t *testing.T) {
}
}
func TestTunnelClientBindLeaseStateTransitions(t *testing.T) {
t.Run("expiry-before-reservation", func(t *testing.T) {
tunnel := NewTunnel(201, "test-session")
registry := &tunnels{
tunnels: map[uint64]*Tunnel{tunnel.ID: tunnel},
mutex: &sync.Mutex{},
}
expiry := make(chan time.Time, 1)
expiry <- time.Time{}
registry.waitForClientBindLease(tunnel, expiry)
if got := registry.Get(tunnel.ID); got != nil {
t.Fatalf("expired tunnel remains registered: %p", got)
}
select {
case <-tunnel.Done():
default:
t.Fatal("expired tunnel did not close")
}
if tunnel.BindClient(&testTunnelDataServer{}) {
t.Fatal("client reserved a tunnel after its bind lease expired")
}
if !tunnel.ClientBindLeaseExpired() {
t.Fatal("closed tunnel did not retain its bind-expired state")
}
})
t.Run("expiry-after-reservation", func(t *testing.T) {
tunnel := NewTunnel(202, "test-session")
client := &testTunnelDataServer{}
if !tunnel.BindClient(client) {
t.Fatal("client failed to reserve tunnel")
}
registry := &tunnels{
tunnels: map[uint64]*Tunnel{tunnel.ID: tunnel},
mutex: &sync.Mutex{},
}
expiry := make(chan time.Time, 1)
expiry <- time.Time{}
registry.waitForClientBindLease(tunnel, expiry)
if tunnel.MarkClientBound(client) {
t.Fatal("reserved client completed a bind after its lease expired")
}
if !tunnel.ClientBindLeaseExpired() {
t.Fatal("reserved tunnel did not report bind expiry")
}
if got := registry.Get(tunnel.ID); got != nil {
t.Fatalf("expired reserved tunnel remains registered: %p", got)
}
select {
case <-tunnel.Done():
default:
t.Fatal("expired reserved tunnel did not close")
}
})
t.Run("completed-bind-wins", func(t *testing.T) {
tunnel := NewTunnel(203, "test-session")
client := &testTunnelDataServer{}
if !tunnel.BindClient(client) || !tunnel.MarkClientBound(client) {
t.Fatal("client failed to complete tunnel bind")
}
registry := &tunnels{
tunnels: map[uint64]*Tunnel{tunnel.ID: tunnel},
mutex: &sync.Mutex{},
}
expiry := make(chan time.Time, 1)
expiry <- time.Time{}
registry.waitForClientBindLease(tunnel, expiry)
if got := registry.Get(tunnel.ID); got != tunnel {
t.Fatalf("completed tunnel after lease signal = %p, want %p", got, tunnel)
}
select {
case <-tunnel.Done():
t.Fatal("lease signal closed a completed client bind")
default:
}
if tunnel.ClientBindLeaseExpired() {
t.Fatal("completed client bind was marked expired")
}
tunnel.Close()
})
}
func TestTunnelsClientBindLeaseCloseIsGenerationScoped(t *testing.T) {
const tunnelID = uint64(204)
old := NewTunnel(tunnelID, "old-session")
current := NewTunnel(tunnelID, "current-session")
registry := &tunnels{
tunnels: map[uint64]*Tunnel{tunnelID: current},
mutex: &sync.Mutex{},
}
expiry := make(chan time.Time, 1)
expiry <- time.Time{}
registry.waitForClientBindLease(old, expiry)
if got := registry.Get(tunnelID); got != current {
t.Fatalf("stale lease changed current generation: got=%p want=%p", got, current)
}
select {
case <-current.Done():
t.Fatal("stale lease closed the current tunnel generation")
default:
}
old.Close()
current.Close()
}
func TestTunnelCloseUnblocksSenders(t *testing.T) {
tunnel := NewTunnel(1, "test-session")
fromStarted := make(chan struct{})
fromResult := make(chan bool, 1)
go func() {
close(fromStarted)
fromResult <- tunnel.SendDataFromImplant(nil)
fromResult <- tunnel.SendDataFromImplant(&sliverpb.TunnelData{Sequence: 0, Data: []byte("blocked")})
}()
toStarted := make(chan struct{})
toResult := make(chan bool, 1)
go func() {
close(toStarted)
toResult <- tunnel.SendDataToImplant(nil)
}()
<-fromStarted
<-toStarted
deadline := time.Now().Add(time.Second)
blocked := false
for time.Now().Before(deadline) {
tunnel.fromImplantBudget.Lock()
fromBlocked := tunnel.fromImplantFrames == 1
tunnel.fromImplantBudget.Unlock()
toBlocked := !tunnel.toImplantQueue.TryLock()
if !toBlocked {
tunnel.toImplantQueue.Unlock()
}
if fromBlocked && toBlocked {
blocked = true
break
}
time.Sleep(time.Millisecond)
}
if !blocked {
tunnel.Close()
t.Fatal("tunnel senders did not reach their blocked channel operations")
}
tunnel.Close()
for name, result := range map[string]<-chan bool{
@@ -181,9 +409,10 @@ func TestTunnelsCloseForClientOnlyClosesOwnedTunnels(t *testing.T) {
}
}
func TestTunnelClientDataRefreshesCloseActivity(t *testing.T) {
func TestTunnelClientDataRefreshesOnlyToImplantCloseActivity(t *testing.T) {
tunnel := NewTunnel(13, "session")
before := tunnel.GetLastMessageTime()
beforeToImplant := tunnel.LastToImplantTime()
beforeFromImplant := tunnel.LastFromImplantTime()
time.Sleep(time.Millisecond)
received := make(chan []byte, 1)
go func() { received <- <-tunnel.ToImplant }()
@@ -194,20 +423,681 @@ func TestTunnelClientDataRefreshesCloseActivity(t *testing.T) {
if got := <-received; string(got) != string(payload) {
t.Fatalf("queued data = %q, want %q", got, payload)
}
if after := tunnel.GetLastMessageTime(); !after.After(before) {
t.Fatalf("client data did not refresh close activity: before=%v after=%v", before, after)
if after := tunnel.LastToImplantTime(); !after.After(beforeToImplant) {
t.Fatalf("client data did not refresh to-implant close activity: before=%v after=%v", beforeToImplant, after)
}
if after := tunnel.LastFromImplantTime(); !after.Equal(beforeFromImplant) {
t.Fatalf("client data changed from-implant close activity: before=%v after=%v", beforeFromImplant, after)
}
}
func TestTunnelTouchRefreshesCloseActivity(t *testing.T) {
func TestTunnelImplantDataRefreshesOnlyFromImplantCloseActivity(t *testing.T) {
tunnel := NewTunnel(14, "session")
beforeToImplant := tunnel.LastToImplantTime()
beforeFromImplant := tunnel.LastFromImplantTime()
time.Sleep(time.Millisecond)
received := make(chan *sliverpb.TunnelData, 1)
go func() { received <- <-tunnel.FromImplant }()
frame := &sliverpb.TunnelData{TunnelID: tunnel.ID, Sequence: 0, Data: []byte("keepalive")}
if err := tunnel.ProcessDataFromImplant(frame); err != nil {
t.Fatalf("process implant data: %v", err)
}
if got := <-received; string(got.Data) != string(frame.Data) {
t.Fatalf("delivered data = %q, want %q", got.Data, frame.Data)
}
if after := tunnel.LastFromImplantTime(); !after.After(beforeFromImplant) {
t.Fatalf("implant data did not refresh from-implant close activity: before=%v after=%v", beforeFromImplant, after)
}
if after := tunnel.LastToImplantTime(); !after.Equal(beforeToImplant) {
t.Fatalf("implant data changed to-implant close activity: before=%v after=%v", beforeToImplant, after)
}
}
func TestTunnelDirectionalCloseTouchesAreIndependent(t *testing.T) {
tunnel := NewTunnel(141, "session")
stale := time.Now().Add(-2 * delayBeforeClose)
tunnel.mutex.Lock()
tunnel.lastDataMessageTime = time.Now().Add(-2 * delayBeforeClose)
tunnel.lastToImplantTime = stale
tunnel.lastFromImplantTime = stale
tunnel.mutex.Unlock()
tunnel.Touch()
if age := time.Since(tunnel.GetLastMessageTime()); age < 0 || age >= delayBeforeClose {
t.Fatalf("Touch did not establish a fresh close grace period: age=%v", age)
tunnel.touchToImplant()
toImplant := tunnel.LastToImplantTime()
if age := time.Since(toImplant); age < 0 || age >= delayBeforeClose {
t.Fatalf("TouchToImplant did not establish a fresh close grace period: age=%v", age)
}
if fromImplant := tunnel.LastFromImplantTime(); !fromImplant.Equal(stale) {
t.Fatalf("TouchToImplant changed from-implant activity: got=%v want=%v", fromImplant, stale)
}
tunnel.touchFromImplant()
if age := time.Since(tunnel.LastFromImplantTime()); age < 0 || age >= delayBeforeClose {
t.Fatalf("TouchFromImplant did not establish a fresh close grace period: age=%v", age)
}
if got := tunnel.LastToImplantTime(); !got.Equal(toImplant) {
t.Fatalf("TouchFromImplant changed to-implant activity: got=%v want=%v", got, toImplant)
}
}
func TestTunnelClaimFromImplantCloseIsIdempotent(t *testing.T) {
tunnel := NewTunnel(144, "session")
before := tunnel.LastFromImplantTime()
time.Sleep(time.Millisecond)
if !tunnel.ClaimFromImplantClose() {
t.Fatal("first implant terminal did not claim close ownership")
}
claimedAt := tunnel.LastFromImplantTime()
if !claimedAt.After(before) {
t.Fatalf("first terminal did not establish close grace: before=%v after=%v", before, claimedAt)
}
time.Sleep(time.Millisecond)
results := make(chan bool, 100)
for range 100 {
go func() { results <- tunnel.ClaimFromImplantClose() }()
}
for range 100 {
if <-results {
t.Fatal("duplicate implant terminal claimed another close scheduler")
}
}
if got := tunnel.LastFromImplantTime(); !got.Equal(claimedAt) {
t.Fatalf("duplicate terminal refreshed close grace: got=%v want=%v", got, claimedAt)
}
}
//nolint:gocyclo // The assertions cover every stage of one ordered terminal lifecycle.
func TestTunnelSequencedTerminalWaitsForPrecedingFrames(t *testing.T) {
tunnel := NewTunnel(145, "session")
ready, err := tunnel.MarkFromImplantTerminal(&sliverpb.TunnelData{
TunnelID: tunnel.ID,
Sequence: 2,
Closed: true,
})
if err != nil {
t.Fatalf("mark sequenced terminal: %v", err)
}
if ready {
t.Fatal("terminal was ready before preceding frames arrived")
}
if err := tunnel.ProcessDataFromImplant(&sliverpb.TunnelData{
TunnelID: tunnel.ID,
Sequence: 1,
Data: []byte("second"),
}); err != nil {
t.Fatalf("queue second frame: %v", err)
}
received := make(chan []*sliverpb.TunnelData, 1)
go func() {
frames := make([]*sliverpb.TunnelData, 0, 2)
for range 2 {
frames = append(frames, <-tunnel.FromImplant)
}
received <- frames
}()
if err := tunnel.ProcessDataFromImplant(&sliverpb.TunnelData{
TunnelID: tunnel.ID,
Sequence: 0,
Data: []byte("first"),
}); err != nil {
t.Fatalf("process first frame: %v", err)
}
var frames []*sliverpb.TunnelData
select {
case frames = <-received:
if len(frames) != 2 || frames[0].Sequence != 0 || string(frames[0].Data) != "first" ||
frames[1].Sequence != 1 || string(frames[1].Data) != "second" {
t.Fatalf("sequenced terminal frames = %+v", frames)
}
case <-time.After(time.Second):
t.Fatal("preceding frames did not drain in order")
}
select {
case <-tunnel.FromImplantTerminalReady():
t.Fatal("terminal became ready before preceding frames were forwarded")
default:
}
for _, frame := range frames {
if err := tunnel.CompleteDataFromImplantForward(frame.Sequence); err != nil {
t.Fatalf("complete forwarded frame %d: %v", frame.Sequence, err)
}
}
select {
case <-tunnel.FromImplantTerminalReady():
case <-time.After(time.Second):
t.Fatal("terminal did not become ready after preceding frames were forwarded")
}
if err := tunnel.ProcessDataFromImplant(&sliverpb.TunnelData{
TunnelID: tunnel.ID,
Sequence: 2,
Data: []byte("after terminal"),
}); !errors.Is(err, ErrTunnelTerminal) {
t.Fatalf("data at terminal error = %v, want ErrTunnelTerminal", err)
}
}
func TestTunnelConnectionWatcherClosesOnlyOldGeneration(t *testing.T) {
originalConnection := NewImplantConnection("test", "original")
replacementConnection := NewImplantConnection("test", "replacement")
t.Cleanup(originalConnection.Close)
t.Cleanup(replacementConnection.Close)
original := newTunnel(146, "same-session", originalConnection)
replacement := newTunnel(original.ID, original.SessionID, replacementConnection)
registry := &tunnels{
tunnels: map[uint64]*Tunnel{original.ID: original},
mutex: &sync.Mutex{},
}
go registry.waitForImplantConnection(original)
registry.mutex.Lock()
registry.tunnels[original.ID] = replacement
registry.mutex.Unlock()
originalConnection.Close()
select {
case <-original.Done():
case <-time.After(time.Second):
t.Fatal("old implant connection teardown did not close its detached tunnel")
}
if got := registry.Get(original.ID); got != replacement {
t.Fatalf("old connection watcher changed replacement registry entry: got=%p want=%p", got, replacement)
}
select {
case <-replacement.Done():
t.Fatal("old connection watcher closed replacement tunnel")
default:
}
select {
case <-replacementConnection.Done():
t.Fatal("old connection watcher closed replacement transport")
default:
}
replacement.Close()
}
func TestTunnelsClientCloseIgnoresFromImplantActivity(t *testing.T) {
tunnel := NewTunnel(142, "session")
registry := &tunnels{
tunnels: map[uint64]*Tunnel{tunnel.ID: tunnel},
mutex: &sync.Mutex{},
}
stale := time.Now().Add(-2 * delayBeforeClose)
tunnel.mutex.Lock()
tunnel.lastToImplantTime = stale
tunnel.lastFromImplantTime = stale
tunnel.mutex.Unlock()
received := make(chan *sliverpb.TunnelData, 1)
go func() { received <- <-tunnel.FromImplant }()
frame := &sliverpb.TunnelData{TunnelID: tunnel.ID, Sequence: 0, Data: []byte("target-keepalive")}
if err := tunnel.ProcessDataFromImplant(frame); err != nil {
t.Fatalf("process target keepalive: %v", err)
}
if got := <-received; string(got.Data) != string(frame.Data) {
t.Fatalf("delivered target keepalive = %q, want %q", got.Data, frame.Data)
}
if got := tunnel.LastToImplantTime(); !got.Equal(stale) {
t.Fatalf("target keepalive changed client-close activity: got=%v want=%v", got, stale)
}
if got := tunnel.LastFromImplantTime(); !got.After(stale) {
t.Fatalf("target keepalive did not refresh implant-close activity: got=%v stale=%v", got, stale)
}
registry.ScheduleCloseTunnelToImplant(tunnel)
if got := registry.Get(tunnel.ID); got != nil {
t.Fatalf("client-requested close was extended by from-implant activity: %+v", got)
}
select {
case <-tunnel.Done():
default:
t.Fatal("client-requested close did not close tunnel")
}
}
func TestTunnelQuiesceDataToImplantJoinsAcceptedForward(t *testing.T) {
tunnel := NewTunnel(149, "session")
received := make(chan struct{})
release := make(chan struct{})
go func() {
<-tunnel.ToImplant
close(received)
<-release
tunnel.CompleteDataToImplant()
}()
accepted := make(chan bool, 1)
go func() { accepted <- tunnel.SendDataToImplant([]byte("final")) }()
select {
case <-received:
case <-time.After(time.Second):
t.Fatal("forwarding worker did not receive accepted final payload")
}
if !<-accepted {
t.Fatal("final payload was not accepted")
}
quiesced := make(chan struct{})
go func() {
tunnel.QuiesceDataToImplant()
close(quiesced)
}()
select {
case <-quiesced:
t.Fatal("graceful close did not wait for the accepted final payload")
case <-time.After(25 * time.Millisecond):
}
close(release)
select {
case <-quiesced:
case <-time.After(time.Second):
t.Fatal("graceful close did not resume after final payload completion")
}
rejected := make(chan bool, 1)
go func() { rejected <- tunnel.SendDataToImplant([]byte("late")) }()
select {
case acceptedLate := <-rejected:
if acceptedLate {
t.Fatal("quiesced tunnel accepted a later payload")
}
case <-time.After(time.Second):
t.Fatal("late payload blocked after tunnel quiescence")
}
}
func TestTunnelTerminalSequenceTracksOnlySuccessfullyForwardedPrefix(t *testing.T) {
tunnel := newTunnelWithCapabilities(150, "session", nil, sliverpb.CapabilityTunnelTerminalV1)
first, err := tunnel.NextDataToImplant([]byte("first"))
if err != nil {
t.Fatalf("assign first frame: %v", err)
}
if got := tunnel.ToImplantTerminalSequence(); got != 0 {
t.Fatalf("terminal included assigned but unforwarded frame: got %d, want 0", got)
}
if err := tunnel.CompleteDataToImplantForward(first.Sequence); err != nil {
t.Fatalf("complete first frame: %v", err)
}
if got := tunnel.ToImplantTerminalSequence(); got != 1 {
t.Fatalf("terminal after first forwarded frame = %d, want 1", got)
}
second, err := tunnel.NextDataToImplant([]byte("second"))
if err != nil {
t.Fatalf("assign second frame: %v", err)
}
if got := tunnel.ToImplantTerminalSequence(); got != 1 {
t.Fatalf("terminal included second unforwarded frame: got %d, want 1", got)
}
if err := tunnel.CompleteDataToImplantForward(second.Sequence); err != nil {
t.Fatalf("complete second frame: %v", err)
}
if got := tunnel.ToImplantTerminalSequence(); got != 2 {
t.Fatalf("terminal after forwarded prefix = %d, want 2", got)
}
}
//nolint:gocyclo // The test drives the terminal deadline through a blocked ordered delivery.
func TestTunnelTerminalDeadlineBreaksBlockedAdmissionBeforeMarkReturns(t *testing.T) {
connection := NewImplantConnection("mtls", "blocked-terminal-mark")
t.Cleanup(connection.Close)
tunnel := newTunnelWithCapabilities(151, "session", connection, sliverpb.CapabilityTunnelTerminalV1)
registry := &tunnels{
tunnels: map[uint64]*Tunnel{tunnel.ID: tunnel},
mutex: &sync.Mutex{},
}
dataDone := make(chan error, 1)
go func() {
dataDone <- tunnel.ProcessDataFromImplant(&sliverpb.TunnelData{
TunnelID: tunnel.ID,
Sequence: 0,
Data: []byte("blocked operator delivery"),
})
}()
deadline := time.Now().Add(time.Second)
for tunnel.fromImplantMutex.TryLock() {
tunnel.fromImplantMutex.Unlock()
if time.Now().After(deadline) {
t.Fatal("data admission did not block while holding the ordering mutex")
}
time.Sleep(time.Millisecond)
}
expiry := make(chan time.Time, 1)
registry.armFromImplantTerminalClose(tunnel, expiry)
markDone := make(chan error, 1)
go func() {
_, err := tunnel.MarkFromImplantTerminal(&sliverpb.TunnelData{
TunnelID: tunnel.ID,
Sequence: 1,
Closed: true,
})
markDone <- err
}()
expiry <- time.Now()
select {
case <-tunnel.Done():
case <-time.After(time.Second):
t.Fatal("terminal deadline did not close the blocked tunnel")
}
select {
case err := <-dataDone:
if !errors.Is(err, ErrTunnelClosed) {
t.Fatalf("blocked data result = %v, want ErrTunnelClosed", err)
}
case <-time.After(time.Second):
t.Fatal("blocked data admission did not wake on terminal deadline")
}
select {
case err := <-markDone:
if !errors.Is(err, ErrTunnelClosed) {
t.Fatalf("blocked terminal mark result = %v, want ErrTunnelClosed", err)
}
case <-time.After(time.Second):
t.Fatal("terminal mark did not return after deadline closure")
}
select {
case <-connection.Done():
case <-time.After(time.Second):
t.Fatal("incomplete terminal deadline did not fail the exact connection closed")
}
}
func TestTunnelsImplantCloseIgnoresToImplantActivity(t *testing.T) {
tunnel := NewTunnel(143, "session")
registry := &tunnels{
tunnels: map[uint64]*Tunnel{tunnel.ID: tunnel},
mutex: &sync.Mutex{},
}
tunnel.mutex.Lock()
tunnel.lastToImplantTime = time.Now()
tunnel.lastFromImplantTime = time.Now().Add(-2 * delayBeforeClose)
tunnel.mutex.Unlock()
registry.ScheduleCloseTunnelFromImplant(tunnel)
if got := registry.Get(tunnel.ID); got != nil {
t.Fatalf("implant-requested close was extended by to-implant activity: %+v", got)
}
select {
case <-tunnel.Done():
default:
t.Fatal("implant-requested close did not close tunnel")
}
}
func TestTunnelProcessDataFromImplantRejectsOversizedAndOutOfWindowFrames(t *testing.T) {
tunnel := NewTunnel(15, "session")
err := tunnel.ProcessDataFromImplant(&sliverpb.TunnelData{
Sequence: 0,
Data: make([]byte, MaxTunnelFrameBytes+1),
})
if !errors.Is(err, ErrTunnelFrameTooLarge) {
t.Fatalf("oversized frame error = %v, want ErrTunnelFrameTooLarge", err)
}
err = tunnel.ProcessDataFromImplant(&sliverpb.TunnelData{
Sequence: maxTunnelPendingFrames,
Data: []byte("outside-window"),
})
if !errors.Is(err, ErrTunnelSequenceWindow) {
t.Fatalf("out-of-window frame error = %v, want ErrTunnelSequenceWindow", err)
}
if len(tunnel.pendingFromImplant) != 0 {
t.Fatalf("rejected frames changed pending state: frames=%d", len(tunnel.pendingFromImplant))
}
}
//nolint:gocyclo // The test validates every state transition across a full bounded receive window.
func TestTunnelProcessDataFromImplantReordersFullReceiveWindow(t *testing.T) {
tunnel := NewTunnel(16, "session")
for sequence := maxTunnelPendingFrames - 1; sequence > 0; sequence-- {
if err := tunnel.ProcessDataFromImplant(&sliverpb.TunnelData{
Sequence: uint64(sequence),
Data: []byte{byte(sequence)},
}); err != nil {
t.Fatalf("queue sequence %d: %v", sequence, err)
}
}
received := make(chan []*sliverpb.TunnelData, 1)
go func() {
frames := make([]*sliverpb.TunnelData, 0, maxTunnelPendingFrames)
for range maxTunnelPendingFrames {
frames = append(frames, <-tunnel.FromImplant)
}
received <- frames
}()
processed := make(chan error, 1)
go func() {
processed <- tunnel.ProcessDataFromImplant(&sliverpb.TunnelData{Sequence: 0, Data: []byte{0}})
}()
select {
case err := <-processed:
if err != nil {
t.Fatalf("process sequence zero: %v", err)
}
case <-time.After(time.Second):
t.Fatal("full receive window did not drain")
}
select {
case frames := <-received:
for sequence, frame := range frames {
if frame.Sequence != uint64(sequence) || len(frame.Data) != 1 || frame.Data[0] != byte(sequence) {
t.Fatalf("frame %d = sequence %d data %v", sequence, frame.Sequence, frame.Data)
}
}
case <-time.After(time.Second):
t.Fatal("timed out collecting reordered frames")
}
if tunnel.FromImplantSequence != maxTunnelPendingFrames {
t.Fatalf("next receive sequence = %d, want %d", tunnel.FromImplantSequence, maxTunnelPendingFrames)
}
if len(tunnel.pendingFromImplant) != 0 {
t.Fatalf("drained receive state retained %d frames", len(tunnel.pendingFromImplant))
}
if tunnel.fromImplantFrames != 0 || tunnel.fromImplantBytes != 0 {
t.Fatalf("drained receive budget retained frames=%d bytes=%d", tunnel.fromImplantFrames, tunnel.fromImplantBytes)
}
}
func TestTunnelProcessDataFromImplantBoundsBlockedHandlers(t *testing.T) {
tunnel := NewTunnel(17, "session")
results := make(chan error, maxTunnelPendingFrames)
for range maxTunnelPendingFrames {
go func() {
results <- tunnel.ProcessDataFromImplant(&sliverpb.TunnelData{Sequence: 0, Data: []byte("blocked")})
}()
}
deadline := time.Now().Add(time.Second)
admitted := 0
for time.Now().Before(deadline) {
tunnel.fromImplantBudget.Lock()
admitted = tunnel.fromImplantFrames
tunnel.fromImplantBudget.Unlock()
if admitted == maxTunnelPendingFrames {
break
}
time.Sleep(time.Millisecond)
}
if admitted != maxTunnelPendingFrames {
tunnel.Close()
t.Fatalf("admitted blocked handlers = %d, want %d", admitted, maxTunnelPendingFrames)
}
if err := tunnel.ProcessDataFromImplant(&sliverpb.TunnelData{Sequence: 0}); !errors.Is(err, ErrTunnelIngressLimit) {
tunnel.Close()
t.Fatalf("handler beyond admission window error = %v, want ErrTunnelIngressLimit", err)
}
tunnel.Close()
for range maxTunnelPendingFrames {
select {
case err := <-results:
if !errors.Is(err, ErrTunnelClosed) {
t.Errorf("blocked handler result = %v, want ErrTunnelClosed", err)
}
case <-time.After(time.Second):
t.Fatal("blocked handler did not exit after tunnel close")
}
}
if tunnel.fromImplantFrames != 0 || tunnel.fromImplantBytes != 0 {
t.Fatalf("closed receive budget retained frames=%d bytes=%d", tunnel.fromImplantFrames, tunnel.fromImplantBytes)
}
}
func TestTunnelProcessDataFromImplantBoundsCombinedReceiveBytes(t *testing.T) {
tunnel := NewTunnel(171, "session")
payload := make([]byte, MaxTunnelFrameBytes)
for sequence := 1; sequence < maxTunnelPendingFrames; sequence++ {
if err := tunnel.ProcessDataFromImplant(&sliverpb.TunnelData{
Sequence: uint64(sequence),
Data: payload,
}); err != nil {
t.Fatalf("queue full-size sequence %d: %v", sequence, err)
}
}
blocked := make(chan error, 1)
go func() {
blocked <- tunnel.ProcessDataFromImplant(&sliverpb.TunnelData{Sequence: 0, Data: payload})
}()
deadline := time.Now().Add(time.Second)
reservedFrames := 0
reservedBytes := 0
for time.Now().Before(deadline) {
tunnel.fromImplantBudget.Lock()
reservedFrames = tunnel.fromImplantFrames
reservedBytes = tunnel.fromImplantBytes
tunnel.fromImplantBudget.Unlock()
if reservedFrames == maxTunnelPendingFrames && reservedBytes == maxTunnelPendingBytes {
break
}
time.Sleep(time.Millisecond)
}
if reservedFrames != maxTunnelPendingFrames || reservedBytes != maxTunnelPendingBytes {
tunnel.Close()
t.Fatalf("combined receive reservation = %d frames/%d bytes, want %d/%d", reservedFrames, reservedBytes, maxTunnelPendingFrames, maxTunnelPendingBytes)
}
if err := tunnel.ProcessDataFromImplant(&sliverpb.TunnelData{Sequence: 0}); !errors.Is(err, ErrTunnelIngressLimit) {
tunnel.Close()
t.Fatalf("frame beyond combined receive window error = %v, want ErrTunnelIngressLimit", err)
}
tunnel.Close()
select {
case err := <-blocked:
if !errors.Is(err, ErrTunnelClosed) {
t.Fatalf("blocked full-window frame result = %v, want ErrTunnelClosed", err)
}
case <-time.After(time.Second):
t.Fatal("full-byte-window handler did not exit after close")
}
tunnel.fromImplantBudget.Lock()
defer tunnel.fromImplantBudget.Unlock()
if tunnel.fromImplantFrames != 0 || tunnel.fromImplantBytes != 0 {
t.Fatalf("closed receive budget retained frames=%d bytes=%d", tunnel.fromImplantFrames, tunnel.fromImplantBytes)
}
}
//nolint:gocyclo // The test exercises cache eviction, replay, mutation isolation, and acknowledgement bounds together.
func TestTunnelOutboundResendCacheBoundsAndAcknowledgements(t *testing.T) {
tunnel := NewTunnel(18, "session")
for sequence := uint64(0); sequence <= maxTunnelResendFrames; sequence++ {
input := []byte{byte(sequence)}
frame, err := tunnel.NextDataToImplant(input)
if err != nil {
t.Fatalf("reserve sequence %d: %v", sequence, err)
}
if frame.Sequence != sequence {
t.Fatalf("assigned sequence = %d, want %d", frame.Sequence, sequence)
}
input[0]++
}
if got := len(tunnel.toImplantCache); got != maxTunnelResendFrames {
t.Fatalf("resend cache frames = %d, want %d", got, maxTunnelResendFrames)
}
if _, ok, err := tunnel.ResendDataToImplant(0); err != nil || ok {
t.Fatalf("evicted resend = (_, %v, %v), want cache miss", ok, err)
}
resend, ok, err := tunnel.ResendDataToImplant(1)
if err != nil || !ok {
t.Fatalf("retained resend = (_, %v, %v), want hit", ok, err)
}
if len(resend.Data) != 1 || resend.Data[0] != 1 {
t.Fatalf("cached frame was mutated through producer input: %v", resend.Data)
}
resend.Data[0] = 0xff
resendAgain, ok, err := tunnel.ResendDataToImplant(1)
if err != nil || !ok || resendAgain.Data[0] != 1 {
t.Fatalf("cached frame was mutated through resend result: frame=%v ok=%v err=%v", resendAgain, ok, err)
}
if err := tunnel.AcknowledgeDataToImplant(64); err != nil {
t.Fatalf("acknowledge sequence 64: %v", err)
}
if got := len(tunnel.toImplantCache); got != maxTunnelResendFrames+1-64 {
t.Fatalf("cache after ACK = %d frames, want %d", got, maxTunnelResendFrames+1-64)
}
if err := tunnel.AcknowledgeDataToImplant(32); err != nil {
t.Fatalf("stale acknowledgement: %v", err)
}
if err := tunnel.AcknowledgeDataToImplant(maxTunnelResendFrames + 2); !errors.Is(err, ErrTunnelAcknowledgement) {
t.Fatalf("future acknowledgement error = %v, want ErrTunnelAcknowledgement", err)
}
if _, _, err := tunnel.ResendDataToImplant(maxTunnelResendFrames + 1); !errors.Is(err, ErrTunnelAcknowledgement) {
t.Fatalf("future resend error = %v, want ErrTunnelAcknowledgement", err)
}
if err := tunnel.AcknowledgeDataToImplant(maxTunnelResendFrames + 1); err != nil {
t.Fatalf("acknowledge all frames: %v", err)
}
if got := len(tunnel.toImplantCache); got != 0 {
t.Fatalf("fully acknowledged cache retained %d frames", got)
}
}
//nolint:gocyclo // The test keeps old and replacement generations in one lifecycle assertion.
func TestTunnelProtocolStateIsGenerationOwnedAndCleared(t *testing.T) {
const tunnelID = uint64(19)
old := NewTunnel(tunnelID, "old-session")
registry := &tunnels{
tunnels: map[uint64]*Tunnel{tunnelID: old},
mutex: &sync.Mutex{},
}
if err := old.ProcessDataFromImplant(&sliverpb.TunnelData{Sequence: 1, Data: []byte("old-pending")}); err != nil {
t.Fatalf("queue old inbound frame: %v", err)
}
if _, err := old.NextDataToImplant([]byte("old-resend")); err != nil {
t.Fatalf("queue old outbound frame: %v", err)
}
if !registry.CloseIf(old) {
t.Fatal("failed to close old generation")
}
if len(old.pendingFromImplant) != 0 || old.fromImplantFrames != 0 || old.fromImplantBytes != 0 || len(old.toImplantCache) != 0 {
t.Fatalf("closed generation retained inbound=%d reserved-frames=%d reserved-bytes=%d outbound=%d", len(old.pendingFromImplant), old.fromImplantFrames, old.fromImplantBytes, len(old.toImplantCache))
}
current := NewTunnel(tunnelID, "current-session")
registry.mutex.Lock()
registry.tunnels[tunnelID] = current
registry.mutex.Unlock()
if registry.CloseIf(old) {
t.Fatal("stale close detached current generation")
}
if got := registry.Get(tunnelID); got != current {
t.Fatalf("current generation changed after stale close: got=%p want=%p", got, current)
}
if err := old.ProcessDataFromImplant(&sliverpb.TunnelData{Sequence: 0}); !errors.Is(err, ErrTunnelClosed) {
t.Fatalf("stale inbound result = %v, want ErrTunnelClosed", err)
}
frame, err := current.NextDataToImplant([]byte("current"))
if err != nil || frame.Sequence != 0 {
t.Fatalf("current generation first frame = %+v, %v", frame, err)
}
if !registry.CloseIf(current) {
t.Fatal("failed to close current generation")
}
if err := current.AcknowledgeDataToImplant(1); !errors.Is(err, ErrTunnelClosed) {
t.Fatalf("closed generation acknowledgement = %v, want ErrTunnelClosed", err)
}
}
@@ -236,6 +1126,9 @@ func TestSocksTunnelsCreateKnownSession(t *testing.T) {
if tunnel.SessionID != session.ID {
t.Fatalf("expected SessionID %q, got %q", session.ID, tunnel.SessionID)
}
if tunnel.ImplantConnection() != session.Connection {
t.Fatal("SOCKS tunnel did not retain its creating session connection")
}
}
func TestPivotGraphEntryToProtobufUnknownSession(t *testing.T) {
-84
View File
@@ -1,84 +0,0 @@
package handlers
/*
Sliver Implant Framework
Copyright (C) 2022 Bishop Fox
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import (
"sync"
"github.com/bishopfox/sliver/protobuf/sliverpb"
)
var (
// TunnelID -> Sequence Number -> Data
tunnelDataCache = dataCache{mutex: &sync.RWMutex{}, cache: map[uint64]map[uint64]*sliverpb.TunnelData{}}
)
type dataCache struct {
mutex *sync.RWMutex
cache map[uint64]map[uint64]*sliverpb.TunnelData
}
func (c *dataCache) Add(tunnelID uint64, sequence uint64, tunnelData *sliverpb.TunnelData) {
c.mutex.Lock()
defer c.mutex.Unlock()
if _, ok := c.cache[tunnelID]; !ok {
c.cache[tunnelID] = map[uint64]*sliverpb.TunnelData{}
}
c.cache[tunnelID][sequence] = tunnelData
}
func (c *dataCache) Get(tunnelID uint64, sequence uint64) (*sliverpb.TunnelData, bool) {
c.mutex.RLock()
defer c.mutex.RUnlock()
if _, ok := c.cache[tunnelID]; !ok {
return nil, false
}
val, ok := c.cache[tunnelID][sequence]
return val, ok
}
func (c *dataCache) DeleteTun(tunnelID uint64) {
c.mutex.Lock()
defer c.mutex.Unlock()
delete(c.cache, tunnelID)
}
func (c *dataCache) DeleteSeq(tunnelID uint64, sequence uint64) {
c.mutex.Lock()
defer c.mutex.Unlock()
if _, ok := c.cache[tunnelID]; !ok {
return
}
delete(c.cache[tunnelID], sequence)
}
func (c *dataCache) Len(tunnelID uint64) int {
c.mutex.RLock()
defer c.mutex.RUnlock()
return len(c.cache[tunnelID])
}
+154
View File
@@ -23,18 +23,172 @@ package handlers
*/
import (
"errors"
"sync"
"sync/atomic"
"time"
"github.com/bishopfox/sliver/protobuf/sliverpb"
"github.com/bishopfox/sliver/server/core"
"github.com/bishopfox/sliver/server/core/rtunnels"
)
type ServerHandler func(*core.ImplantConnection, []byte) *sliverpb.Envelope
var (
tunnelHandlerMutex = &sync.Mutex{}
// reverseTunnelOpenings lets tunnel frames wait for their own bounded broker
// dial without holding the global tunnel handler mutex or blocking unrelated
// tunnels. Values are *reverseTunnelOpening keyed by tunnel ID.
reverseTunnelOpenings sync.Map
reverseTunnelOpeningAttempts = newReverseTunnelAdmission(
maxReverseTunnelOpeningsPerSession,
maxReverseTunnelOpeningsGlobal,
)
reverseTunnelOpeningWaiters = newReverseTunnelAdmission(
maxReverseTunnelOpeningWaitersPerSession,
maxReverseTunnelOpeningWaitersGlobal,
)
reverseTunnelRejectionSlots = make(chan struct{}, maxConcurrentReverseTunnelRejections)
)
const (
// Match the full bounded tunnel reorder/transport concurrency window. A
// legitimate source can fill every stream while the first broker dial is
// still completing; admission below this value turns ordinary traffic into
// cancellation of an otherwise authorized opening.
maxReverseTunnelOpeningWaiters = 128
maxReverseTunnelOpeningsPerSession = 32
maxReverseTunnelOpeningsGlobal = 128
maxReverseTunnelOpeningWaitersPerSession = 128
maxReverseTunnelOpeningWaitersGlobal = 512
maxTunnelDataMessageBytes = 128 * 1024
maxConcurrentReverseTunnelRejections = 128
)
var (
reverseTunnelOpeningWaitTimeout = rtunnels.DefaultDialTimeout + time.Second
reverseTunnelRejectionTimeout = rtunnels.DefaultDialTimeout
)
var (
errReverseTunnelOpeningLimit = errors.New("reverse tunnel opening limit reached")
errReverseTunnelOpeningWaiterLimit = errors.New("reverse tunnel opening waiter limit reached")
errReverseTunnelOpeningWaitTimeout = errors.New("reverse tunnel opening wait timed out")
)
type reverseTunnelOpening struct {
sessionID string
ready chan struct{}
waiters chan struct{}
cancel func()
closing atomic.Bool
terminal atomic.Bool
}
func (opening *reverseTunnelOpening) claimTerminal() bool {
return opening != nil && opening.terminal.CompareAndSwap(false, true)
}
func newReverseTunnelOpening(sessionID string, cancel func()) *reverseTunnelOpening {
return &reverseTunnelOpening{
sessionID: sessionID,
ready: make(chan struct{}),
waiters: make(chan struct{}, maxReverseTunnelOpeningWaiters),
cancel: cancel,
}
}
func (opening *reverseTunnelOpening) requestClose() {
if opening == nil {
return
}
opening.closing.Store(true)
if opening.cancel != nil {
opening.cancel()
}
}
func (opening *reverseTunnelOpening) wait(timeout time.Duration) error {
if opening == nil {
return errReverseTunnelOpeningWaitTimeout
}
waiterAdmission := reverseTunnelOpeningWaiters
if !waiterAdmission.acquire(opening.sessionID) {
return errReverseTunnelOpeningWaiterLimit
}
defer waiterAdmission.release(opening.sessionID)
select {
case opening.waiters <- struct{}{}:
defer func() { <-opening.waiters }()
default:
return errReverseTunnelOpeningWaiterLimit
}
return opening.waitReady(timeout)
}
// waitReady is used after the opening's single terminal slot is claimed. That
// reserved slot lets a legitimate terminal wait even when every data-frame
// waiter is occupied without allowing duplicate terminals to create unbounded
// blocked handlers.
func (opening *reverseTunnelOpening) waitReady(timeout time.Duration) error {
if opening == nil {
return errReverseTunnelOpeningWaitTimeout
}
if timeout <= 0 {
timeout = reverseTunnelOpeningWaitTimeout
}
timer := time.NewTimer(timeout)
defer timer.Stop()
select {
case <-opening.ready:
return nil
case <-timer.C:
return errReverseTunnelOpeningWaitTimeout
}
}
type reverseTunnelAdmission struct {
mutex sync.Mutex
perSession map[string]int
total int
sessionMax int
globalMax int
}
func newReverseTunnelAdmission(sessionMax int, globalMax int) *reverseTunnelAdmission {
return &reverseTunnelAdmission{
perSession: map[string]int{},
sessionMax: sessionMax,
globalMax: globalMax,
}
}
func (admission *reverseTunnelAdmission) acquire(sessionID string) bool {
admission.mutex.Lock()
defer admission.mutex.Unlock()
if admission.total >= admission.globalMax || admission.perSession[sessionID] >= admission.sessionMax {
return false
}
admission.total++
admission.perSession[sessionID]++
return true
}
func (admission *reverseTunnelAdmission) release(sessionID string) {
admission.mutex.Lock()
defer admission.mutex.Unlock()
if admission.perSession[sessionID] == 0 {
return
}
admission.perSession[sessionID]--
admission.total--
if admission.perSession[sessionID] == 0 {
delete(admission.perSession, sessionID)
}
}
// GetHandlers - Returns a map of server-side msg handlers
func GetHandlers() map[uint32]ServerHandler {
return map[uint32]ServerHandler{
+66 -10
View File
@@ -61,6 +61,14 @@ var (
// NOTE: the data passed as an argument to this handler is already extracted from the most recent
// envelope so we can just start parsing PivotPeerEnvelope's
func pivotPeerEnvelopeHandler(implantConn *core.ImplantConnection, data []byte) *sliverpb.Envelope {
if implantConn == nil {
return nil
}
select {
case <-implantConn.Done():
return nil
default:
}
pivotLog.Debugf("received pivot peer envelope ...")
peerEnvelope := &sliverpb.PivotPeerEnvelope{}
err := proto.Unmarshal(data, peerEnvelope)
@@ -81,6 +89,9 @@ func pivotPeerEnvelopeHandler(implantConn *core.ImplantConnection, data []byte)
}
func sessionEnvelopeHandler(implantConn *core.ImplantConnection, peerEnvelope *sliverpb.PivotPeerEnvelope) *sliverpb.Envelope {
if implantConn == nil || peerEnvelope == nil {
return nil
}
pivotSessionID := uuid.FromBytesOrNil(peerEnvelope.PivotSessionID).String()
if pivotSessionID == "" {
pivotLog.Errorf("failed to parse pivot session id from peer envelope")
@@ -93,6 +104,23 @@ func sessionEnvelopeHandler(implantConn *core.ImplantConnection, peerEnvelope *s
return nil
}
pivot := pivotEntry.(*core.Pivot)
if pivot == nil || pivot.ImplantConn == nil || pivot.ImmediateImplantConn == nil || pivot.ImmediateImplantConn != implantConn {
pivotLog.Warnf("rejected pivot session envelope from non-owning connection")
return nil
}
select {
case <-pivot.ImplantConn.Done():
core.ClosePivotSession(pivotSessionID)
return nil
case <-pivot.ImmediateImplantConn.Done():
core.ClosePivotSession(pivotSessionID)
return nil
default:
}
if pivot.CipherCtx == nil {
pivotLog.Warnf("rejected pivot session envelope without a cipher context")
return nil
}
plaintext, err := pivot.CipherCtx.Decrypt(peerEnvelope.Data)
if err != nil {
pivotLog.Errorf("failed to decrypt pivot session data: %v", err)
@@ -104,6 +132,13 @@ func sessionEnvelopeHandler(implantConn *core.ImplantConnection, peerEnvelope *s
pivotLog.Errorf("failed to unmarshal pivot session data: %v", err)
return nil
}
select {
case <-pivot.ImplantConn.Done():
return nil
case <-pivot.ImmediateImplantConn.Done():
return nil
default:
}
go handlePivotEnvelope(pivot, envelope)
@@ -111,20 +146,28 @@ func sessionEnvelopeHandler(implantConn *core.ImplantConnection, peerEnvelope *s
}
func handlePivotEnvelope(pivot *core.Pivot, envelope *sliverpb.Envelope) {
if pivot == nil || envelope == nil || pivot.ImplantConn == nil || pivot.ImmediateImplantConn == nil {
return
}
pivotLog.Debugf("pivot session %s received envelope: %v", pivot.ID, envelope.Type)
handlers := GetNonPivotHandlers()
select {
case <-pivot.ImplantConn.Done():
return
case <-pivot.ImmediateImplantConn.Done():
return
default:
}
pivot.ImplantConn.UpdateLastMessage()
if envelope.ID != 0 {
pivot.ImplantConn.RespMutex.RLock()
defer pivot.ImplantConn.RespMutex.RUnlock()
if resp, ok := pivot.ImplantConn.Resp[envelope.ID]; ok {
resp <- envelope
}
pivot.ImplantConn.DeliverResponse(envelope)
} else if handler, ok := handlers[envelope.Type]; ok {
respEnvelope := handler(pivot.ImplantConn, envelope.Data)
if respEnvelope != nil {
go func() {
pivot.ImplantConn.Send <- respEnvelope
if err := pivot.ImplantConn.SendEnvelopeUntil(respEnvelope, pivot.ImmediateImplantConn.Done(), core.DefaultImplantSendTimeout); err != nil {
pivot.ImplantConn.Close()
}
}()
}
} else if envelope.Type == sliverpb.MsgPivotServerPing {
@@ -153,6 +196,9 @@ func pivotPeerFailureHandler(implantConn *core.ImplantConnection, data []byte) *
targetPivotIDs := map[string]struct{}{}
core.PivotSessions.Range(func(key, value interface{}) bool {
pivot := value.(*core.Pivot)
if pivot.ImmediateImplantConn != implantConn {
return true
}
reporterIdx := pivotPeerIndex(pivot.Peers, reportingSession.PeerID)
if reporterIdx <= 0 {
return true
@@ -173,7 +219,7 @@ func pivotPeerFailureHandler(implantConn *core.ImplantConnection, data []byte) *
return true
})
if len(targetSessionIDs) == 0 {
if len(targetPivotIDs) == 0 {
pivotLog.Warnf("ignoring unauthorized pivot peer failure from %d for %d", reportingSession.PeerID, peerFailure.PeerID)
return nil
}
@@ -182,7 +228,7 @@ func pivotPeerFailureHandler(implantConn *core.ImplantConnection, data []byte) *
core.Sessions.Remove(sessionID)
}
for pivotID := range targetPivotIDs {
core.PivotSessions.Delete(pivotID)
core.ClosePivotSession(pivotID)
}
return nil
}
@@ -197,6 +243,16 @@ func pivotPeerIndex(peers []*sliverpb.PivotPeer, peerID int64) int {
}
func pivotServerPingHandler(pivot *core.Pivot) {
if pivot == nil || pivot.ImplantConn == nil || pivot.ImmediateImplantConn == nil {
return
}
select {
case <-pivot.ImplantConn.Done():
return
case <-pivot.ImmediateImplantConn.Done():
return
default:
}
pivot.ImplantConn.UpdateLastMessage()
pivot.ImmediateImplantConn.UpdateLastMessage()
}
@@ -259,7 +315,6 @@ func serverKeyExchange(implantConn *core.ImplantConnection, peerEnvelope *sliver
pivotSession.ImplantConn = core.NewImplantConnection(core.PivotTransportName, pivotRemoteAddr)
pivotSession.ImmediateImplantConn = implantConn
core.PivotSessions.Store(pivotSession.ID, pivotSession)
keyExRespEnvelope := MustMarshal(&sliverpb.Envelope{
Type: sliverpb.MsgPivotServerKeyExchange,
Data: MustMarshal(&sliverpb.PivotServerKeyExchange{
@@ -271,7 +326,7 @@ func serverKeyExchange(implantConn *core.ImplantConnection, peerEnvelope *sliver
pivotLog.Warn("Failed to encrypt pivot server key exchange response")
return nil
}
peerEnvelopeData, _ := proto.Marshal(&sliverpb.PivotPeerEnvelope{
peerEnvelopeData, err := proto.Marshal(&sliverpb.PivotPeerEnvelope{
Peers: pivotSession.Peers,
Data: ciphertext,
})
@@ -280,6 +335,7 @@ func serverKeyExchange(implantConn *core.ImplantConnection, peerEnvelope *sliver
return nil
}
core.PivotSessions.Store(pivotSession.ID, pivotSession)
pivotSession.Start()
return &sliverpb.Envelope{
Type: sliverpb.MsgPivotPeerEnvelope,
+227 -11
View File
@@ -6,9 +6,54 @@ import (
"github.com/bishopfox/sliver/protobuf/sliverpb"
"github.com/bishopfox/sliver/server/core"
"github.com/gofrs/uuid"
"google.golang.org/protobuf/proto"
)
func TestPivotSessionEnvelopeRequiresOwningImmediateConnection(t *testing.T) {
owner := core.NewImplantConnection("mtls", "owner")
intruder := core.NewImplantConnection("mtls", "intruder")
pivot := core.NewPivotSession([]*sliverpb.PivotPeer{{PeerID: 1}, {PeerID: 2}})
pivot.ImplantConn = core.NewImplantConnection(core.PivotTransportName, "pivot")
pivot.ImmediateImplantConn = owner
core.PivotSessions.Store(pivot.ID, pivot)
t.Cleanup(func() {
owner.Close()
intruder.Close()
pivot.ImplantConn.Close()
core.PivotSessions.CompareAndDelete(pivot.ID, pivot)
})
peerEnvelope := &sliverpb.PivotPeerEnvelope{
PivotSessionID: uuid.FromStringOrNil(pivot.ID).Bytes(),
Data: []byte("not encrypted"),
}
if response := sessionEnvelopeHandler(intruder, peerEnvelope); response != nil {
t.Fatal("non-owning connection received a pivot response")
}
if !pivotSessionExists(pivot.ID) {
t.Fatal("non-owning connection removed the pivot")
}
select {
case <-pivot.ImplantConn.Done():
t.Fatal("non-owning connection closed the pivot")
default:
}
owner.Close()
if response := sessionEnvelopeHandler(owner, peerEnvelope); response != nil {
t.Fatal("closed owning connection received a pivot response")
}
if pivotSessionExists(pivot.ID) {
t.Fatal("closed owning connection retained the pivot")
}
select {
case <-pivot.ImplantConn.Done():
default:
t.Fatal("closed owning connection did not close the pivot")
}
}
func TestPivotPeerFailureHandlerRemovesAuthorizedImmediateChild(t *testing.T) {
resetPivotFailureTestState(t)
@@ -44,6 +89,18 @@ func TestPivotPeerFailureHandlerRemovesAuthorizedImmediateChild(t *testing.T) {
if !pivotSessionExists(unrelatedChildPivot.ID) {
t.Fatalf("unrelated pivot session was removed")
}
for _, pivot := range []*core.Pivot{childPivot, grandchildPivot} {
select {
case <-pivot.ImplantConn.Done():
default:
t.Fatalf("removed pivot connection %s remained open", pivot.ID)
}
}
select {
case <-unrelatedChildPivot.ImplantConn.Done():
t.Fatal("unrelated pivot connection was closed")
default:
}
}
func TestPivotPeerFailureHandlerRejectsNonImmediateChild(t *testing.T) {
@@ -67,12 +124,157 @@ func TestPivotPeerFailureHandlerRejectsNonImmediateChild(t *testing.T) {
if !pivotSessionExists(grandchildPivot.ID) {
t.Fatalf("grandchild pivot session was removed by non-immediate failure report")
}
for _, pivot := range []*core.Pivot{childPivot, grandchildPivot} {
select {
case <-pivot.ImplantConn.Done():
t.Fatalf("pivot connection %s was closed by non-immediate failure report", pivot.ID)
default:
}
}
}
func TestPivotPeerFailureHandlerRequiresOwningConnectionIdentity(t *testing.T) {
resetPivotFailureTestState(t)
ownerConn, _ := addPivotFailureTestSession(t, 300, "wg")
intruderConn, _ := addPivotFailureTestSession(t, 300, "mtls")
childSession, childPivot := addPivotFailureTestPivotSession(t, 200, 200, 300)
childPivot.ImmediateImplantConn = ownerConn
pivotPeerFailureHandler(intruderConn, mustMarshalPivotPeerFailure(t, 200))
if got := core.Sessions.Get(childSession.ID); got == nil {
t.Fatal("same-PeerID connection removed another connection's pivot session")
}
if !pivotSessionExists(childPivot.ID) {
t.Fatal("same-PeerID connection removed another connection's pivot registry entry")
}
select {
case <-childPivot.ImplantConn.Done():
t.Fatal("same-PeerID connection closed another connection's pivot")
default:
}
pivotPeerFailureHandler(ownerConn, mustMarshalPivotPeerFailure(t, 200))
if got := core.Sessions.Get(childSession.ID); got != nil {
t.Fatal("owning connection did not remove its failed pivot session")
}
if pivotSessionExists(childPivot.ID) {
t.Fatal("owning connection did not remove its failed pivot registry entry")
}
}
func TestSessionRemoveCascadesThroughRegisteredPivotDescendants(t *testing.T) {
resetPivotFailureTestState(t)
parentConn, parentSession := addPivotFailureTestSession(t, 300, "wg")
childSession, childPivot := addPivotFailureTestPivotSession(t, 200, 200, 300)
grandchildSession, grandchildPivot := addPivotFailureTestPivotSession(t, 100, 100, 200, 300)
core.Sessions.Remove(parentSession.ID)
for _, sessionID := range []string{parentSession.ID, childSession.ID, grandchildSession.ID} {
if got := core.Sessions.Get(sessionID); got != nil {
t.Fatalf("session %s survived parent removal", sessionID)
}
}
for _, pivot := range []*core.Pivot{childPivot, grandchildPivot} {
if pivotSessionExists(pivot.ID) {
t.Fatalf("pivot %s survived parent removal", pivot.ID)
}
select {
case <-pivot.ImplantConn.Done():
default:
t.Fatalf("pivot connection %s survived parent removal", pivot.ID)
}
}
select {
case <-parentConn.Done():
t.Fatal("removing a session closed its shared parent transport connection")
default:
}
}
func TestSessionRemoveClosesPivotParentAndDescendants(t *testing.T) {
resetPivotFailureTestState(t)
topLevelConn, topLevelSession := addPivotFailureTestSession(t, 300, "wg")
pivotSession, pivot := addPivotFailureTestPivotSession(t, 200, 200, 300)
childSession, childPivot := addPivotFailureTestPivotSession(t, 100, 100, 200, 300)
pivot.ImmediateImplantConn = topLevelConn
childPivot.ImmediateImplantConn = pivot.ImplantConn
core.Sessions.Remove(pivotSession.ID)
if got := core.Sessions.Get(topLevelSession.ID); got == nil {
t.Fatal("removing a pivot session removed its shared top-level transport session")
}
for _, sessionID := range []string{pivotSession.ID, childSession.ID} {
if got := core.Sessions.Get(sessionID); got != nil {
t.Fatalf("session %s survived pivot-parent removal", sessionID)
}
}
for _, removedPivot := range []*core.Pivot{pivot, childPivot} {
if pivotSessionExists(removedPivot.ID) {
t.Fatalf("pivot %s survived pivot-parent removal", removedPivot.ID)
}
select {
case <-removedPivot.ImplantConn.Done():
default:
t.Fatalf("pivot connection %s survived pivot-parent removal", removedPivot.ID)
}
}
select {
case <-topLevelConn.Done():
t.Fatal("removing a pivot session closed its shared top-level transport connection")
default:
}
}
func TestPivotPeerFailureClosesUnregisteredImmediateChild(t *testing.T) {
resetPivotFailureTestState(t)
reporterConn, _ := addPivotFailureTestSession(t, 300, "wg")
unregistered := core.NewPivotSession([]*sliverpb.PivotPeer{{PeerID: 200}, {PeerID: 300}})
unregistered.OriginID = 200
unregistered.ImplantConn = core.NewImplantConnection(core.PivotTransportName, "unregistered")
unregistered.ImmediateImplantConn = reporterConn
core.PivotSessions.Store(unregistered.ID, unregistered)
unrelated := core.NewPivotSession([]*sliverpb.PivotPeer{{PeerID: 800}, {PeerID: 900}})
unrelated.OriginID = 800
unrelated.ImplantConn = core.NewImplantConnection(core.PivotTransportName, "unrelated")
core.PivotSessions.Store(unrelated.ID, unrelated)
t.Cleanup(func() {
unregistered.ImplantConn.Close()
unrelated.ImplantConn.Close()
core.PivotSessions.CompareAndDelete(unregistered.ID, unregistered)
core.PivotSessions.CompareAndDelete(unrelated.ID, unrelated)
})
pivotPeerFailureHandler(reporterConn, mustMarshalPivotPeerFailure(t, 200))
if pivotSessionExists(unregistered.ID) {
t.Fatal("unregistered immediate child remained in pivot map")
}
select {
case <-unregistered.ImplantConn.Done():
default:
t.Fatal("unregistered immediate child connection remained open")
}
if !pivotSessionExists(unrelated.ID) {
t.Fatal("unrelated unregistered pivot was removed")
}
select {
case <-unrelated.ImplantConn.Done():
t.Fatal("unrelated unregistered pivot connection was closed")
default:
}
}
func addPivotFailureTestSession(t *testing.T, peerID int64, transport string) (*core.ImplantConnection, *core.Session) {
t.Helper()
conn := core.NewImplantConnection(transport, fmt.Sprintf("peer-%d", peerID))
t.Cleanup(conn.Close)
session := core.NewSession(conn)
session.Name = fmt.Sprintf("peer-%d", peerID)
session.PeerID = peerID
@@ -94,7 +296,22 @@ func addPivotFailureTestPivotSession(t *testing.T, originID int64, chain ...int6
pivot := core.NewPivotSession(peers)
pivot.OriginID = originID
pivot.ImplantConn = core.NewImplantConnection(core.PivotTransportName, fmt.Sprintf("pivot-%d", originID))
if len(chain) > 1 {
for _, session := range core.Sessions.All() {
if session.PeerID == chain[1] {
pivot.ImmediateImplantConn = session.Connection
break
}
}
if pivot.ImmediateImplantConn == nil {
t.Fatalf("missing immediate parent session for pivot peer %d", chain[1])
}
}
core.PivotSessions.Store(pivot.ID, pivot)
t.Cleanup(func() {
core.ClosePivotSession(pivot.ID)
pivot.ImplantConn.Close()
})
session := core.NewSession(pivot.ImplantConn)
session.Name = fmt.Sprintf("peer-%d", originID)
@@ -125,21 +342,20 @@ func pivotSessionExists(pivotID string) bool {
func resetPivotFailureTestState(t *testing.T) {
t.Helper()
for _, session := range core.Sessions.All() {
core.Sessions.Remove(session.ID)
}
core.PivotSessions.Range(func(key, value interface{}) bool {
core.PivotSessions.Delete(key)
return true
})
t.Cleanup(func() {
cleanup := func() {
for _, session := range core.Sessions.All() {
core.Sessions.Remove(session.ID)
if session.Connection != nil {
session.Connection.Close()
}
}
core.PivotSessions.Range(func(key, value interface{}) bool {
core.PivotSessions.Delete(key)
if pivotID, ok := key.(string); ok {
core.ClosePivotSession(pivotID)
}
return true
})
})
}
cleanup()
t.Cleanup(cleanup)
}
@@ -0,0 +1,143 @@
package handlers
import (
"bytes"
"net"
"testing"
"time"
"github.com/bishopfox/sliver/protobuf/sliverpb"
"github.com/bishopfox/sliver/server/core"
"github.com/bishopfox/sliver/server/core/rtunnels"
"google.golang.org/protobuf/proto"
)
func TestLegacyCapabilityReverseTunnelQueuesFinalDataWithoutRacingTerminalEnvelope(t *testing.T) {
registry, dialer, broker := newRecordingReverseForwardBroker(t)
connection, session := addBufferedTestSession(t)
session.Capabilities = 0 // A pre-terminal-sequence implant.
authorizationID := beginActiveAuthorization(t, registry, session.ID, "127.0.0.1:4701", 31)
tunnelID := core.NewTunnelID()
sendReverseTunnelRequest(t, connection, broker, &sliverpb.TunnelData{
TunnelID: tunnelID,
CreateReverse: true,
Rportfwd: &sliverpb.RPortfwd{
AuthorizationID: authorizationID.String(),
},
})
peer := recordingReverseTunnelPeer(t, dialer)
if err := peer.SetWriteDeadline(time.Now().Add(time.Second)); err != nil {
t.Fatalf("set target write deadline: %v", err)
}
finalData := []byte("final bytes before target EOF")
if _, err := peer.Write(finalData); err != nil {
t.Fatalf("write final target data: %v", err)
}
if err := peer.Close(); err != nil {
t.Fatalf("close target peer: %v", err)
}
envelope := receiveReverseTunnelEnvelope(t, connection)
if envelope.Type != sliverpb.MsgTunnelData {
t.Fatalf("first envelope type = %d, want %d", envelope.Type, sliverpb.MsgTunnelData)
}
data := &sliverpb.TunnelData{}
if err := proto.Unmarshal(envelope.Data, data); err != nil {
t.Fatalf("unmarshal final tunnel data: %v", err)
}
if data.TunnelID != tunnelID || data.Sequence != 0 || !bytes.Equal(data.Data, finalData) {
t.Fatalf("final tunnel data = %+v, want tunnel=%d sequence=0 data=%q", data, tunnelID, finalData)
}
waitForReverseTunnelRemoval(t, tunnelID)
select {
case unexpected := <-connection.Send:
t.Fatalf("legacy implant received racing terminal envelope type %d after final data", unexpected.Type)
default:
}
}
func TestTunnelTerminalCapabilityQueuesSequencedCloseAfterFinalData(t *testing.T) {
registry, dialer, broker := newRecordingReverseForwardBroker(t)
connection, session := addBufferedTestSession(t)
session.Capabilities = sliverpb.CapabilityTunnelTerminalV1
authorizationID := beginActiveAuthorization(t, registry, session.ID, "127.0.0.1:4702", 32)
tunnelID := core.NewTunnelID()
sendReverseTunnelRequest(t, connection, broker, &sliverpb.TunnelData{
TunnelID: tunnelID,
CreateReverse: true,
Rportfwd: &sliverpb.RPortfwd{
AuthorizationID: authorizationID.String(),
},
})
peer := recordingReverseTunnelPeer(t, dialer)
if err := peer.SetWriteDeadline(time.Now().Add(time.Second)); err != nil {
t.Fatalf("set target write deadline: %v", err)
}
finalData := []byte("sequenced final bytes")
if _, err := peer.Write(finalData); err != nil {
t.Fatalf("write final target data: %v", err)
}
if err := peer.Close(); err != nil {
t.Fatalf("close target peer: %v", err)
}
dataEnvelope := receiveReverseTunnelEnvelope(t, connection)
closeEnvelope := receiveReverseTunnelEnvelope(t, connection)
if dataEnvelope.Type != sliverpb.MsgTunnelData {
t.Fatalf("first envelope type = %d, want %d", dataEnvelope.Type, sliverpb.MsgTunnelData)
}
if closeEnvelope.Type != sliverpb.MsgTunnelClose {
t.Fatalf("second envelope type = %d, want %d", closeEnvelope.Type, sliverpb.MsgTunnelClose)
}
data := &sliverpb.TunnelData{}
if err := proto.Unmarshal(dataEnvelope.Data, data); err != nil {
t.Fatalf("unmarshal tunnel data: %v", err)
}
closed := &sliverpb.TunnelData{}
if err := proto.Unmarshal(closeEnvelope.Data, closed); err != nil {
t.Fatalf("unmarshal tunnel close: %v", err)
}
if data.TunnelID != tunnelID || data.Sequence != 0 || !bytes.Equal(data.Data, finalData) {
t.Fatalf("final tunnel data = %+v, want tunnel=%d sequence=0 data=%q", data, tunnelID, finalData)
}
if !closed.Closed || closed.TunnelID != tunnelID || closed.Sequence != 1 {
t.Fatalf("terminal envelope = %+v, want closed tunnel=%d sequence=1", closed, tunnelID)
}
waitForReverseTunnelRemoval(t, tunnelID)
}
func recordingReverseTunnelPeer(t *testing.T, dialer *recordingReverseForwardDialer) net.Conn {
t.Helper()
dialer.mutex.Lock()
defer dialer.mutex.Unlock()
if len(dialer.peers) != 1 {
t.Fatalf("recording dialer peers = %d, want 1", len(dialer.peers))
}
return dialer.peers[0]
}
func receiveReverseTunnelEnvelope(t *testing.T, connection *core.ImplantConnection) *sliverpb.Envelope {
t.Helper()
select {
case envelope := <-connection.Send:
return envelope
case <-time.After(time.Second):
t.Fatal("timed out waiting for reverse-tunnel envelope")
return nil
}
}
func waitForReverseTunnelRemoval(t *testing.T, tunnelID uint64) {
t.Helper()
deadline := time.Now().Add(time.Second)
for rtunnels.GetRTunnel(tunnelID) != nil && time.Now().Before(deadline) {
time.Sleep(time.Millisecond)
}
if tunnel := rtunnels.GetRTunnel(tunnelID); tunnel != nil {
closeTestReverseTunnel(tunnelID)
t.Fatalf("reverse tunnel %d remained published after target EOF", tunnelID)
}
}
+409
View File
@@ -0,0 +1,409 @@
package handlers
import (
"context"
"errors"
"go/ast"
"go/parser"
"go/token"
"io"
"net"
"os"
"path/filepath"
"strings"
"sync"
"testing"
"time"
"github.com/bishopfox/sliver/protobuf/sliverpb"
"github.com/bishopfox/sliver/server/core"
"github.com/bishopfox/sliver/server/core/rtunnels"
"google.golang.org/protobuf/proto"
)
func TestReverseTunnelAuthorizationControlsDialDestination(t *testing.T) {
const (
operatorTarget = "127.0.0.1:4444"
tunnelID = uint64(0x1001)
)
registry, dialer, broker := newRecordingReverseForwardBroker(t)
connection, session := addBufferedTestSession(t)
authorizationID := beginActiveAuthorization(t, registry, session.ID, operatorTarget, 1)
sendReverseTunnelRequest(t, connection, broker, &sliverpb.TunnelData{
TunnelID: tunnelID,
CreateReverse: true,
Rportfwd: &sliverpb.RPortfwd{
AuthorizationID: authorizationID.String(),
Host: "169.254.169.254",
Port: 80,
},
})
assertDialAddresses(t, dialer, operatorTarget)
tunnel := rtunnels.GetRTunnel(tunnelID)
if tunnel == nil {
t.Fatalf("authorized request did not create reverse tunnel %d", tunnelID)
}
if got := tunnel.AuthorizationID(); got != authorizationID {
t.Fatalf("tunnel authorization = %q, want %q", got, authorizationID)
}
t.Cleanup(func() {
closeTestReverseTunnel(tunnelID)
})
}
func TestReverseTunnelAuthorizationRejectsInvalidCapabilities(t *testing.T) {
tests := []struct {
name string
prepareID func(t *testing.T, registry *rtunnels.Registry, ownerSession string) rtunnels.AuthorizationID
attacker bool
}{
{
name: "unknown authorization",
prepareID: func(_ *testing.T, _ *rtunnels.Registry, _ string) rtunnels.AuthorizationID {
return rtunnels.AuthorizationID("unknown-authorization")
},
},
{
name: "wrong session",
prepareID: func(t *testing.T, registry *rtunnels.Registry, ownerSession string) rtunnels.AuthorizationID {
return beginActiveAuthorization(t, registry, ownerSession, "127.0.0.1:4444", 2)
},
attacker: true,
},
{
name: "revoked authorization",
prepareID: func(t *testing.T, registry *rtunnels.Registry, ownerSession string) rtunnels.AuthorizationID {
authorizationID := beginActiveAuthorization(t, registry, ownerSession, "127.0.0.1:4444", 3)
if !registry.Revoke(ownerSession, authorizationID) {
t.Fatal("Revoke() = false, want true")
}
return authorizationID
},
},
}
for index, test := range tests {
t.Run(test.name, func(t *testing.T) {
registry, dialer, broker := newRecordingReverseForwardBroker(t)
ownerConnection, ownerSession := addBufferedTestSession(t)
connection := ownerConnection
if test.attacker {
attackerConnection, _ := addBufferedTestSession(t)
connection = attackerConnection
}
authorizationID := test.prepareID(t, registry, ownerSession.ID)
tunnelID := uint64(0x2000 + index)
sendReverseTunnelRequest(t, connection, broker, &sliverpb.TunnelData{
TunnelID: tunnelID,
CreateReverse: true,
Rportfwd: &sliverpb.RPortfwd{
AuthorizationID: authorizationID.String(),
Host: "169.254.169.254",
Port: 80,
},
})
assertDialAddresses(t, dialer)
if tunnel := rtunnels.GetRTunnel(tunnelID); tunnel != nil {
closeTestReverseTunnel(tunnelID)
t.Fatalf("rejected request created reverse tunnel %d", tunnelID)
}
})
}
}
func TestLegacyReverseTunnelRequiresCanonicalOperatorDestination(t *testing.T) {
t.Run("canonical match dials stored destination", func(t *testing.T) {
const tunnelID = uint64(0x3001)
registry, dialer, broker := newRecordingReverseForwardBroker(t)
connection, session := addBufferedTestSession(t)
beginActiveAuthorization(t, registry, session.ID, "Example.COM.:443", 4)
sendReverseTunnelRequest(t, connection, broker, &sliverpb.TunnelData{
TunnelID: tunnelID,
CreateReverse: true,
Rportfwd: &sliverpb.RPortfwd{
Host: "EXAMPLE.com",
Port: 443,
},
})
assertDialAddresses(t, dialer, "example.com:443")
t.Cleanup(func() {
closeTestReverseTunnel(tunnelID)
})
})
t.Run("mismatch is rejected", func(t *testing.T) {
const tunnelID = uint64(0x3002)
registry, dialer, broker := newRecordingReverseForwardBroker(t)
connection, session := addBufferedTestSession(t)
beginActiveAuthorization(t, registry, session.ID, "127.0.0.1:4444", 5)
sendReverseTunnelRequest(t, connection, broker, &sliverpb.TunnelData{
TunnelID: tunnelID,
CreateReverse: true,
Rportfwd: &sliverpb.RPortfwd{
Host: "169.254.169.254",
Port: 80,
},
})
assertDialAddresses(t, dialer)
if tunnel := rtunnels.GetRTunnel(tunnelID); tunnel != nil {
closeTestReverseTunnel(tunnelID)
t.Fatalf("legacy address mismatch created reverse tunnel %d", tunnelID)
}
})
}
func TestNegotiatedAuthorizationRejectsLegacyDowngrade(t *testing.T) {
const tunnelID = uint64(0x3100)
registry, dialer, broker := newRecordingReverseForwardBroker(t)
connection, session := addBufferedTestSession(t)
authorizationID, err := registry.Begin(session.ID, "127.0.0.1:4444", 0)
if err != nil {
t.Fatalf("Begin() error = %v", err)
}
if err := registry.ActivateProtocol(session.ID, authorizationID, 6, true); err != nil {
t.Fatalf("ActivateProtocol() error = %v", err)
}
sendReverseTunnelRequest(t, connection, broker, &sliverpb.TunnelData{
TunnelID: tunnelID,
CreateReverse: true,
Rportfwd: &sliverpb.RPortfwd{Host: "127.0.0.1", Port: 4444},
})
assertDialAddresses(t, dialer)
if tunnel := rtunnels.GetRTunnel(tunnelID); tunnel != nil {
closeTestReverseTunnel(tunnelID)
t.Fatalf("capability-required listener accepted a legacy tunnel open")
}
}
func TestTunnelWriterUnblocksWhenAuthorizationCloses(t *testing.T) {
connection := core.NewImplantConnection("test", "n/a")
tunnel := rtunnels.NewAuthorizedRTunnel(0x3200, "session", rtunnels.AuthorizationID("authorization"), &countingHandlerWriteCloser{})
result := make(chan error, 1)
go func() {
_, err := (tunnelWriter{tun: tunnel, conn: connection}).Write([]byte("blocked"))
result <- err
}()
tunnel.Close()
select {
case err := <-result:
if !errors.Is(err, errReverseTunnelClosed) {
t.Fatalf("tunnelWriter.Write() error = %v, want %v", err, errReverseTunnelClosed)
}
case <-time.After(time.Second):
t.Fatal("closing authorization did not unblock the implant send")
}
}
func TestReverseTunnelCreationRejectsMalformedMessages(t *testing.T) {
connection, _ := addTestSession(t)
t.Run("invalid protobuf", func(t *testing.T) {
assertNoPanic(t, func() {
tunnelDataHandler(connection, []byte{0xff})
})
})
t.Run("nil tunnel data", func(t *testing.T) {
assertNoPanic(t, func() {
createReverseTunnelHandler(connection, nil)
})
})
t.Run("nil reverse metadata", func(t *testing.T) {
const tunnelID = uint64(0x8badf00d)
data, err := proto.Marshal(&sliverpb.TunnelData{
TunnelID: tunnelID,
CreateReverse: true,
})
if err != nil {
t.Fatalf("marshal tunnel data: %v", err)
}
assertNoPanic(t, func() {
tunnelDataHandler(connection, data)
})
if tunnel := rtunnels.GetRTunnel(tunnelID); tunnel != nil {
closeTestReverseTunnel(tunnelID)
t.Fatalf("malformed request created reverse tunnel %d", tunnelID)
}
})
select {
case envelope := <-connection.Send:
t.Fatalf("malformed request produced outbound envelope type %d", envelope.Type)
default:
}
}
func TestHandlersUseReverseForwardBrokerBoundary(t *testing.T) {
entries, err := os.ReadDir(".")
if err != nil {
t.Fatalf("read handlers directory: %v", err)
}
files := token.NewFileSet()
forbiddenCalls := map[string]bool{
"Dial": true,
"DialContext": true,
"DialTimeout": true,
"Acquire": true,
"AcquireLegacy": true,
"Lookup": true,
"LookupListener": true,
"List": true,
}
for _, entry := range entries {
name := entry.Name()
if entry.IsDir() || !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") {
continue
}
parsed, err := parser.ParseFile(files, filepath.Clean(name), nil, 0)
if err != nil {
t.Fatalf("parse %s: %v", name, err)
}
ast.Inspect(parsed, func(node ast.Node) bool {
if selector, ok := node.(*ast.SelectorExpr); ok && selector.Sel.Name == "DefaultRegistry" {
position := files.Position(selector.Pos())
t.Errorf("direct DefaultRegistry access is forbidden in handlers at %s; use the reverse-forward broker", position)
return true
}
call, ok := node.(*ast.CallExpr)
if !ok {
return true
}
selector, ok := call.Fun.(*ast.SelectorExpr)
if !ok {
return true
}
if !forbiddenCalls[selector.Sel.Name] {
return true
}
position := files.Position(call.Pos())
t.Errorf("direct %s call is forbidden in handlers at %s; use the reverse-forward broker", selector.Sel.Name, position)
return true
})
}
}
type recordingReverseForwardDialer struct {
mutex sync.Mutex
calls []string
peers []net.Conn
drains sync.WaitGroup
}
func (dialer *recordingReverseForwardDialer) DialContext(_ context.Context, network string, address string) (net.Conn, error) {
dialer.mutex.Lock()
defer dialer.mutex.Unlock()
dialer.calls = append(dialer.calls, network+" "+address)
connection, peer := net.Pipe()
dialer.peers = append(dialer.peers, peer)
dialer.drains.Add(1)
go func() {
defer dialer.drains.Done()
const maxTestRelayBytes = 1 << 20
_, _ = io.CopyN(io.Discard, peer, maxTestRelayBytes)
}()
return connection, nil
}
func (dialer *recordingReverseForwardDialer) addresses() []string {
dialer.mutex.Lock()
defer dialer.mutex.Unlock()
addresses := make([]string, 0, len(dialer.calls))
for _, call := range dialer.calls {
_, address, _ := strings.Cut(call, " ")
addresses = append(addresses, address)
}
return addresses
}
func (dialer *recordingReverseForwardDialer) close() {
dialer.mutex.Lock()
peers := dialer.peers
dialer.peers = nil
dialer.mutex.Unlock()
for _, peer := range peers {
_ = peer.Close()
}
dialer.drains.Wait()
}
func newRecordingReverseForwardBroker(t *testing.T) (*rtunnels.Registry, *recordingReverseForwardDialer, *rtunnels.Broker) {
t.Helper()
registry := rtunnels.NewRegistry()
dialer := &recordingReverseForwardDialer{}
broker := rtunnels.NewBroker(registry, dialer, time.Second)
t.Cleanup(dialer.close)
return registry, dialer, broker
}
func addBufferedTestSession(t *testing.T) (*core.ImplantConnection, *core.Session) {
t.Helper()
connection, session := addTestSession(t)
connection.Send = make(chan *sliverpb.Envelope, 8)
return connection, session
}
func beginActiveAuthorization(t *testing.T, registry *rtunnels.Registry, sessionID string, address string, listenerID uint32) rtunnels.AuthorizationID {
t.Helper()
authorizationID, err := registry.Begin(sessionID, address, 0)
if err != nil {
t.Fatalf("Begin(%q) error = %v", address, err)
}
if err := registry.Activate(sessionID, authorizationID, listenerID); err != nil {
t.Fatalf("Activate() error = %v", err)
}
return authorizationID
}
func sendReverseTunnelRequest(t *testing.T, connection *core.ImplantConnection, broker *rtunnels.Broker, request *sliverpb.TunnelData) {
t.Helper()
assertNoPanic(t, func() {
createReverseTunnelHandlerWithBroker(connection, request, broker)
})
}
func assertDialAddresses(t *testing.T, dialer *recordingReverseForwardDialer, want ...string) {
t.Helper()
got := dialer.addresses()
if len(got) != len(want) {
t.Fatalf("dial addresses = %v, want %v", got, want)
}
for index := range want {
if got[index] != want[index] {
t.Fatalf("dial addresses = %v, want %v", got, want)
}
}
}
func closeTestReverseTunnel(tunnelID uint64) {
tunnel := rtunnels.GetRTunnel(tunnelID)
if tunnel != nil && rtunnels.RemoveRTunnelIf(tunnelID, tunnel) {
tunnel.Close()
}
}
type countingHandlerWriteCloser struct{}
func (*countingHandlerWriteCloser) Write(buffer []byte) (int, error) {
return len(buffer), nil
}
func (*countingHandlerWriteCloser) Close() error {
return nil
}
+437 -190
View File
@@ -25,9 +25,11 @@ package handlers
import (
"context"
"encoding/json"
"fmt"
"errors"
"io"
"net"
"strconv"
"sync"
"time"
"github.com/bishopfox/sliver/protobuf/clientpb"
@@ -49,6 +51,11 @@ func registerSessionHandler(implantConn *core.ImplantConnection, data []byte) *s
if implantConn == nil {
return nil
}
select {
case <-implantConn.Done():
return nil
default:
}
register := &sliverpb.Register{}
err := proto.Unmarshal(data, register)
if err != nil {
@@ -81,10 +88,16 @@ func registerSessionHandler(implantConn *core.ImplantConnection, data []byte) *s
session.ConfigID = register.ConfigID
session.PeerID = register.PeerID
session.Capabilities = register.Capabilities
core.Sessions.Add(session)
implantConn.Cleanup = func() {
core.Sessions.Remove(session.ID)
registrationReady := make(chan struct{})
if !implantConn.SetCleanup(func() {
<-registrationReady
core.Sessions.RemoveIf(session)
}) {
sessionHandlerLog.Warnf("Rejected duplicate or closed session registration on connection %s", implantConn.ID)
return nil
}
defer close(registrationReady)
core.Sessions.Add(session)
go auditLogSession(session, register)
return nil
}
@@ -106,82 +119,256 @@ func auditLogSession(session *core.Session, register *sliverpb.Register) {
}
}
func isTunnelResourcePressure(err error) bool {
return errors.Is(err, core.ErrTunnelIngressLimit) || errors.Is(err, core.ErrTunnelPendingBytes)
}
// The handler mutex prevents a send on a closed channel, without it
// two handlers calls may race when a tunnel is quickly created and closed.
func tunnelDataHandler(implantConn *core.ImplantConnection, data []byte) *sliverpb.Envelope {
if implantConn == nil {
return nil
}
if len(data) > maxTunnelDataMessageBytes {
sessionHandlerLog.Warnf("Closing implant connection after oversized tunnel data message (%d bytes)", len(data))
implantConn.Close()
return nil
}
session := core.Sessions.FromImplantConnection(implantConn)
if session == nil {
sessionHandlerLog.Warnf("Received tunnel data from unknown session: %v", implantConn)
return nil
}
tunnelHandlerMutex.Lock()
defer tunnelHandlerMutex.Unlock()
tunnelData := &sliverpb.TunnelData{}
proto.Unmarshal(data, tunnelData)
if err := proto.Unmarshal(data, tunnelData); err != nil {
sessionHandlerLog.Warnf("Failed to decode tunnel data from session %s: %v", session.ID, err)
return nil
}
sessionHandlerLog.Debugf("[DATA] Sequence on tunnel %d, %d, data: %s", tunnelData.TunnelID, tunnelData.Sequence, tunnelData.Data)
if tunnelData.CreateReverse {
if actual, ok := reverseTunnelOpenings.Load(tunnelData.TunnelID); ok {
existing := actual.(*reverseTunnelOpening)
if existing.sessionID != session.ID {
sessionHandlerLog.Warnf("Session %s attempted to reuse reverse tunnel ID %d owned by another session", session.ID, tunnelData.TunnelID)
rejectReverseTunnel(implantConn, tunnelData.TunnelID, rtunnels.ErrDuplicateTunnelID)
}
// A retransmitted create is idempotent and must never add another
// goroutine waiting behind the same outbound dial.
return nil
}
if existing := rtunnels.GetRTunnel(tunnelData.TunnelID); existing != nil {
if existing.SessionID != session.ID {
sessionHandlerLog.Warnf("Session %s attempted to reuse active reverse tunnel ID %d owned by another session", session.ID, tunnelData.TunnelID)
rejectReverseTunnel(implantConn, tunnelData.TunnelID, rtunnels.ErrDuplicateTunnelID)
}
// The first create frame can be retransmitted. An already-published
// relay owned by this session makes that retransmission idempotent.
return nil
}
openingAdmission := reverseTunnelOpeningAttempts
if !openingAdmission.acquire(session.ID) {
rejectReverseTunnel(implantConn, tunnelData.TunnelID, errReverseTunnelOpeningLimit)
return nil
}
defer openingAdmission.release(session.ID)
openingContext, cancelOpening := context.WithCancel(context.Background())
opening := newReverseTunnelOpening(session.ID, cancelOpening)
actual, loaded := reverseTunnelOpenings.LoadOrStore(tunnelData.TunnelID, opening)
if loaded {
cancelOpening()
existing := actual.(*reverseTunnelOpening)
if existing.sessionID != session.ID {
sessionHandlerLog.Warnf("Session %s attempted to reuse reverse tunnel ID %d owned by another session", session.ID, tunnelData.TunnelID)
rejectReverseTunnel(implantConn, tunnelData.TunnelID, rtunnels.ErrDuplicateTunnelID)
}
return nil
}
defer func() {
cancelOpening()
close(opening.ready)
reverseTunnelOpenings.CompareAndDelete(tunnelData.TunnelID, opening)
}()
switch implantConn.TryClaimReverseTunnelID(tunnelData.TunnelID) {
case core.ReverseTunnelIDClaimed:
case core.ReverseTunnelIDDuplicate:
rejectReverseTunnel(implantConn, tunnelData.TunnelID, rtunnels.ErrDuplicateTunnelID)
return nil
case core.ReverseTunnelIDCapacityExhausted, core.ReverseTunnelIDConnectionClosed:
// Capacity exhaustion already fails the C2 connection closed. Do not
// queue a rejection behind a connection that can no longer deliver it.
return nil
}
response := createReverseTunnelHandlerWithContext(openingContext, implantConn, tunnelData, rtunnels.DefaultBroker)
if opening.closing.Load() {
if tunnel := rtunnels.GetRTunnel(tunnelData.TunnelID); tunnel != nil && tunnel.SessionID == session.ID {
_ = closeReverseTunnelRemote(tunnel)
}
}
return response
}
if value, ok := reverseTunnelOpenings.Load(tunnelData.TunnelID); ok {
opening := value.(*reverseTunnelOpening)
if opening.sessionID != session.ID {
sessionHandlerLog.Warnf("Session %s attempted to send data on opening reverse tunnel %d owned by another session", session.ID, tunnelData.TunnelID)
return nil
}
if err := opening.wait(reverseTunnelOpeningWaitTimeout); err != nil {
opening.requestClose()
rejectReverseTunnel(implantConn, tunnelData.TunnelID, err)
return nil
}
}
rtunnel := rtunnels.GetRTunnel(tunnelData.TunnelID)
if rtunnel != nil && session.ID == rtunnel.SessionID {
RTunnelDataHandler(tunnelData, rtunnel, implantConn)
return nil
} else if rtunnel != nil && session.ID != rtunnel.SessionID {
sessionHandlerLog.Warnf("Warning: Session %s attempted to send data on reverse tunnel it did not own", session.ID)
} else if rtunnel == nil && tunnelData.CreateReverse == true {
createReverseTunnelHandler(implantConn, data)
//RTunnelDataHandler(tunnelData, rtunnel, implantConn)
} else {
tunnel := core.Tunnels.Get(tunnelData.TunnelID)
if tunnel != nil {
if session.ID == tunnel.SessionID {
tunnel.SendDataFromImplant(tunnelData)
} else {
sessionHandlerLog.Warnf("Warning: Session %s attempted to send data on tunnel it did not own", session.ID)
return nil
}
tunnel := core.Tunnels.Get(tunnelData.TunnelID)
if tunnel != nil {
if session.ID == tunnel.SessionID && tunnel.ImplantConnection() == implantConn {
if err := tunnel.ProcessDataFromImplant(tunnelData); err != nil {
if errors.Is(err, core.ErrTunnelClosed) {
return nil
}
if isTunnelResourcePressure(err) {
sessionHandlerLog.Warnf("Closing generic tunnel %d after ingress resource pressure: %v", tunnel.ID, err)
core.Tunnels.CloseIf(tunnel)
return nil
}
sessionHandlerLog.Warnf("Closing session %s after invalid generic tunnel frame on %d: %v", session.ID, tunnel.ID, err)
core.Tunnels.CloseIf(tunnel)
implantConn.Close()
}
} else {
sessionHandlerLog.Warnf("Data sent on nil tunnel %d", tunnelData.TunnelID)
sessionHandlerLog.Warnf("Warning: Session %s attempted to send data on tunnel it did not own", session.ID)
}
} else {
sessionHandlerLog.Warnf("Data sent on nil tunnel %d", tunnelData.TunnelID)
}
return nil
}
func tunnelCloseHandler(implantConn *core.ImplantConnection, data []byte) *sliverpb.Envelope {
return tunnelCloseHandlerWithTerminalArm(implantConn, data, core.Tunnels.ArmFromImplantTerminalClose)
}
//nolint:gocyclo // Terminal ordering spans generic, opening reverse, and active reverse tunnel lifecycles.
func tunnelCloseHandlerWithTerminalArm(implantConn *core.ImplantConnection, data []byte, armTerminalClose func(*core.Tunnel)) *sliverpb.Envelope {
if implantConn == nil {
return nil
}
if len(data) > maxTunnelDataMessageBytes {
sessionHandlerLog.Warnf("Closing implant connection after oversized tunnel close message (%d bytes)", len(data))
implantConn.Close()
return nil
}
session := core.Sessions.FromImplantConnection(implantConn)
if session == nil {
sessionHandlerLog.Warnf("Received tunnel close from unknown session: %v", implantConn)
return nil
}
tunnelHandlerMutex.Lock()
defer tunnelHandlerMutex.Unlock()
tunnelData := &sliverpb.TunnelData{}
proto.Unmarshal(data, tunnelData)
if err := proto.Unmarshal(data, tunnelData); err != nil {
sessionHandlerLog.Warnf("Failed to decode tunnel close from session %s: %v", session.ID, err)
return nil
}
sessionHandlerLog.Debugf("[CLOSE] Sequence on tunnel %d, %d, data: %s", tunnelData.TunnelID, tunnelData.Sequence, tunnelData.Data)
if !tunnelData.Closed {
return nil
}
if value, ok := reverseTunnelOpenings.Load(tunnelData.TunnelID); ok {
opening := value.(*reverseTunnelOpening)
if opening.sessionID != session.ID {
sessionHandlerLog.Warnf("Session %s attempted to close opening reverse tunnel %d owned by another session", session.ID, tunnelData.TunnelID)
return nil
}
if !opening.claimTerminal() {
return nil
}
// Capability-bearing implants sequence terminal EOF after all earlier
// data, so their terminal must not cancel a still-pending authorized
// dial. Legacy closes have no ordering contract and retain abort behavior.
if session.Capabilities&sliverpb.CapabilityTunnelTerminalV1 == 0 {
opening.requestClose()
}
if err := opening.waitReady(reverseTunnelOpeningWaitTimeout); err != nil {
sessionHandlerLog.Warnf("Could not wait for closing reverse tunnel %d: %v", tunnelData.TunnelID, err)
return nil
}
}
tunnelHandlerMutex.Lock()
tunnel := core.Tunnels.Get(tunnelData.TunnelID)
if tunnel != nil {
if session.ID == tunnel.SessionID {
if session.ID == tunnel.SessionID && tunnel.ImplantConnection() == implantConn {
sessionHandlerLog.Infof("Closing tunnel %d", tunnel.ID)
// Tunnel close and final data envelopes are dispatched concurrently.
// Start a fresh quiet period so an overtaking close cannot discard the
// last frame from an otherwise idle shell.
tunnel.Touch()
go core.Tunnels.ScheduleClose(tunnel.ID)
if session.Capabilities&sliverpb.CapabilityTunnelTerminalV1 != 0 {
// Arm the exact-generation deadline before terminal validation can
// wait behind ordered data admission. A blocked operator stream must
// not leave this handler, or the shared tunnel dispatcher, stuck
// indefinitely before any timeout exists.
if armTerminalClose != nil {
armTerminalClose(tunnel)
}
ready, err := tunnel.MarkFromImplantTerminal(tunnelData)
if err != nil {
if !errors.Is(err, core.ErrTunnelClosed) {
sessionHandlerLog.Warnf("Closing session %s after invalid terminal sequence on generic tunnel %d: %v", session.ID, tunnel.ID, err)
core.Tunnels.CloseIf(tunnel)
implantConn.Close()
}
} else if ready {
core.Tunnels.CloseIf(tunnel)
}
} else if tunnel.ClaimFromImplantClose() {
// Legacy close and data envelopes are dispatched independently and
// have no ordering contract. Retain their bounded quiet-period
// fallback, while duplicate closes neither refresh nor reschedule it.
go core.Tunnels.ScheduleCloseTunnelFromImplant(tunnel)
}
} else {
sessionHandlerLog.Warnf("Warning: Session %s attempted to send data on tunnel it did not own", session.ID)
}
} else {
rtunnel := rtunnels.GetRTunnel(tunnelData.TunnelID)
if rtunnel != nil && session.ID == rtunnel.SessionID {
rtunnel.Close()
rtunnels.RemoveRTunnel(rtunnel.ID)
} else if rtunnel != nil && session.ID != rtunnel.SessionID {
sessionHandlerLog.Warnf("Warning: Session %s attempted to send data on reverse tunnel it did not own", session.ID)
} else {
sessionHandlerLog.Warnf("Close sent on nil tunnel %d", tunnelData.TunnelID)
tunnelHandlerMutex.Unlock()
return nil
}
tunnelHandlerMutex.Unlock()
rtunnel := rtunnels.GetRTunnel(tunnelData.TunnelID)
if rtunnel != nil && session.ID == rtunnel.SessionID {
ready, err := rtunnel.MarkPeerClose(tunnelData.Sequence)
if err != nil {
if !errors.Is(err, rtunnels.ErrReverseTunnelClosed) {
sessionHandlerLog.Warnf("Closing session %s after invalid terminal sequence on reverse tunnel %d: %v", session.ID, rtunnel.ID, err)
if closeReverseTunnelRemote(rtunnel) {
implantConn.Close()
}
}
return nil
}
if ready {
_ = closeReverseTunnelRemote(rtunnel)
return nil
}
rtunnel.StartPeerCloseDeadline(reverseTunnelSendTimeout, func() {
if closeReverseTunnelRemote(rtunnel) {
sessionHandlerLog.Warnf("Closing session %s after incomplete terminal sequence on reverse tunnel %d", session.ID, rtunnel.ID)
implantConn.Close()
}
})
} else if rtunnel != nil && session.ID != rtunnel.SessionID {
sessionHandlerLog.Warnf("Warning: Session %s attempted to send data on reverse tunnel it did not own", session.ID)
} else {
sessionHandlerLog.Warnf("Close sent on nil tunnel %d", tunnelData.TunnelID)
}
return nil
}
@@ -196,26 +383,70 @@ func pingHandler(implantConn *core.ImplantConnection, data []byte) *sliverpb.Env
return nil
}
func isCanonicalImplantSocksAcknowledgement(frame *sliverpb.SocksData) bool {
return frame != nil && frame.Ack != 0 && len(frame.Data) == 0 && !frame.CloseConn &&
frame.Sequence == 0 && frame.Capabilities == 0 && frame.Username == "" &&
frame.Password == "" && frame.Request == nil
}
func socksDataHandler(implantConn *core.ImplantConnection, data []byte) *sliverpb.Envelope {
if implantConn == nil {
return nil
}
if len(data) > maxTunnelDataMessageBytes {
sessionHandlerLog.Warnf("Closing implant connection after oversized SOCKS data message (%d bytes)", len(data))
implantConn.Close()
return nil
}
session := core.Sessions.FromImplantConnection(implantConn)
if session == nil {
sessionHandlerLog.Warnf("Received socks data from unknown session: %v", implantConn)
return nil
}
tunnelHandlerMutex.Lock()
defer tunnelHandlerMutex.Unlock()
socksData := &sliverpb.SocksData{}
proto.Unmarshal(data, socksData)
//if socksData.CloseConn{
// core.SocksTunnels.Close(socksData.TunnelID)
// return nil
//}
sessionHandlerLog.Debugf("socksDataHandler: %d bytes: %v", len(socksData.Data), socksData.Data)
if err := proto.Unmarshal(data, socksData); err != nil {
sessionHandlerLog.Warnf("Failed to decode SOCKS data from session %s: %v", session.ID, err)
return nil
}
sessionHandlerLog.Debugf(
"socksDataHandler: tunnel=%d sequence=%d bytes=%d close=%t",
socksData.TunnelID,
socksData.Sequence,
len(socksData.Data),
socksData.CloseConn,
)
socksTunnel := core.SocksTunnels.Get(socksData.TunnelID)
if socksTunnel != nil {
if session.ID == socksTunnel.SessionID {
socksTunnel.FromImplant <- socksData
if session.ID == socksTunnel.SessionID && socksTunnel.ImplantConnection() == implantConn {
if socksData.Ack != 0 {
if !isCanonicalImplantSocksAcknowledgement(socksData) {
sessionHandlerLog.Warnf("Closing SOCKS tunnel %d after malformed implant acknowledgement", socksTunnel.ID)
core.SocksTunnels.CloseIfAndFinalize(socksTunnel)
return nil
}
if err := socksTunnel.RelayImplantAcknowledgement(implantConn, socksData.Ack); err != nil {
if errors.Is(err, core.ErrTunnelClosed) {
return nil
}
sessionHandlerLog.Warnf("Closing SOCKS tunnel %d after invalid implant acknowledgement: %v", socksTunnel.ID, err)
core.SocksTunnels.CloseIfAndFinalize(socksTunnel)
}
return nil
}
if err := socksTunnel.ProcessDataFromImplant(socksData); err != nil {
if errors.Is(err, core.ErrTunnelClosed) {
return nil
}
if isTunnelResourcePressure(err) {
sessionHandlerLog.Warnf("Closing SOCKS tunnel %d after ingress resource pressure: %v", socksTunnel.ID, err)
core.SocksTunnels.CloseIfAndFinalize(socksTunnel)
return nil
}
sessionHandlerLog.Warnf("Closing session %s after invalid SOCKS tunnel frame on %d: %v", session.ID, socksTunnel.ID, err)
core.SocksTunnels.CloseIfAndFinalize(socksTunnel)
implantConn.Close()
}
} else {
sessionHandlerLog.Warnf("Warning: Session %s attempted to send data on tunnel it did not own", session.ID)
}
@@ -225,64 +456,95 @@ func socksDataHandler(implantConn *core.ImplantConnection, data []byte) *sliverp
return nil
}
func createReverseTunnelHandler(implantConn *core.ImplantConnection, data []byte) *sliverpb.Envelope {
func createReverseTunnelHandler(implantConn *core.ImplantConnection, req *sliverpb.TunnelData) *sliverpb.Envelope {
return createReverseTunnelHandlerWithBroker(implantConn, req, rtunnels.DefaultBroker)
}
func createReverseTunnelHandlerWithBroker(implantConn *core.ImplantConnection, req *sliverpb.TunnelData, broker *rtunnels.Broker) *sliverpb.Envelope {
return createReverseTunnelHandlerWithContext(context.Background(), implantConn, req, broker)
}
//nolint:gocyclo // Opening is a single transaction spanning validation, dialing, publication, and relay cleanup.
func createReverseTunnelHandlerWithContext(openingContext context.Context, implantConn *core.ImplantConnection, req *sliverpb.TunnelData, broker *rtunnels.Broker) *sliverpb.Envelope {
if implantConn == nil {
sessionHandlerLog.Warnf("Rejected malformed reverse tunnel creation request")
return nil
}
session := core.Sessions.FromImplantConnection(implantConn)
req := &sliverpb.TunnelData{}
proto.Unmarshal(data, req)
remoteAddress := fmt.Sprintf("%s:%d", req.Rportfwd.Host, req.Rportfwd.Port)
if !rtunnels.Check(session.ID, remoteAddress) {
sessionHandlerLog.Errorf("Session %s attempted to create reverse tunnel to %s without being initiated by a client", session.ID, remoteAddress)
if session == nil || req == nil || req.Rportfwd == nil || broker == nil {
sessionHandlerLog.Warnf("Rejected malformed reverse tunnel creation request")
return nil
}
var defaultDialer = new(net.Dialer)
ctx, cancelContext := context.WithCancel(context.Background())
dst, err := defaultDialer.DialContext(ctx, "tcp", remoteAddress)
//dst, err := net.Dial("tcp", remoteAddress)
legacyAddress := ""
if req.Rportfwd.AuthorizationID == "" {
legacyAddress = net.JoinHostPort(req.Rportfwd.Host, strconv.FormatUint(uint64(req.Rportfwd.Port), 10)) //nolint:staticcheck // Required for exact legacy wire compatibility.
}
if openingContext == nil {
openingContext = context.Background()
}
dst, resolvedAuthorizationID, err := broker.Open(
openingContext,
session.ID,
rtunnels.AuthorizationID(req.Rportfwd.AuthorizationID),
legacyAddress,
)
if err != nil {
tunnelClose, _ := proto.Marshal(&sliverpb.TunnelData{
Closed: true,
TunnelID: req.TunnelID,
})
implantConn.Send <- &sliverpb.Envelope{
Type: sliverpb.MsgTunnelClose,
Data: tunnelClose,
}
cancelContext()
rejectReverseTunnel(implantConn, req.TunnelID, err)
return nil
}
if err := openingContext.Err(); err != nil {
_ = dst.Close()
rejectReverseTunnel(implantConn, req.TunnelID, err)
return nil
}
if conn, ok := dst.(*net.TCPConn); ok {
// {{if .Config.Debug}}
//log.Printf("[portfwd] Configuring keep alive")
// {{end}}
keepAlive := rtunnels.GetKeepAlive(session.ID, remoteAddress)
if keepAlive < 0 {
conn.SetKeepAlive(false)
} else {
conn.SetKeepAlive(true)
if keepAlive == 0 {
conn.SetKeepAlivePeriod(30 * time.Second)
} else {
conn.SetKeepAlivePeriod(time.Duration(keepAlive) * time.Second)
tunnel := rtunnels.NewAuthorizedRTunnel(req.TunnelID, session.ID, resolvedAuthorizationID, dst, dst)
// Legacy implants dispatch tunnel envelopes from independent transport
// streams and ignore terminal sequences. A separate close envelope can
// therefore overtake the final data envelope. Keep their fallback entirely
// server-side: close and detach the relay, but leave already-queued data as
// the last wire message. Capability-bearing implants use sequenced close.
if session.Capabilities&sliverpb.CapabilityTunnelTerminalV1 != 0 {
tunnel.SetPeerCloseNotifier(func(sequence uint64) error {
data, err := proto.Marshal(&sliverpb.TunnelData{
Closed: true,
TunnelID: tunnel.ID,
Sequence: sequence,
})
if err != nil {
return err
}
}
err = queueTunnelEnvelope(implantConn, tunnel, &sliverpb.Envelope{Type: sliverpb.MsgTunnelClose, Data: data})
if err != nil && !tunnel.PeerTeardownPending() {
go implantConn.Close()
}
return err
})
}
tunnelHandlerMutex.Lock()
if core.Tunnels.Get(req.TunnelID) != nil {
tunnelHandlerMutex.Unlock()
tunnel.Close()
rejectReverseTunnel(implantConn, req.TunnelID, rtunnels.ErrDuplicateTunnelID)
return nil
}
if !rtunnels.TryAddRTunnel(tunnel) {
tunnelHandlerMutex.Unlock()
tunnel.Close()
rejectReverseTunnel(implantConn, req.TunnelID, rtunnels.ErrDuplicateTunnelID)
return nil
}
tunnelHandlerMutex.Unlock()
tunnel := rtunnels.NewRTunnel(req.TunnelID, session.ID, dst, dst)
rtunnels.AddRTunnel(tunnel)
var cleanupOnce sync.Once
cleanup := func(reason error) {
// {{if .Config.Debug}}
sessionHandlerLog.Infof("[portfwd] Closing tunnel %d (%s)", tunnel.ID, reason)
// {{end}}
tunnel := rtunnels.GetRTunnel(tunnel.ID)
rtunnels.RemoveRTunnel(tunnel.ID)
dst.Close()
cancelContext()
cleanupOnce.Do(func() {
// {{if .Config.Debug}}
sessionHandlerLog.Infof("[portfwd] Closing tunnel %d (%v)", tunnel.ID, reason)
// {{end}}
_, _ = closeReverseTunnelLocal(tunnel)
})
}
go func() {
@@ -299,108 +561,93 @@ func createReverseTunnelHandler(implantConn *core.ImplantConnection, data []byte
cleanup(err)
}()
tunnelDataCache.Add(tunnel.ID, req.Sequence, req)
// NOTE: The read/write semantics can be a little mind boggling, just remember we're reading
// from the server and writing to the tunnel's reader (e.g. stdout), so that's why ReadSequence
// is used here whereas WriteSequence is used for data written back to the server
// Go through cache and write all sequential data to the reader
for recv, ok := tunnelDataCache.Get(tunnel.ID, tunnel.ReadSequence()); ok; recv, ok = tunnelDataCache.Get(tunnel.ID, tunnel.ReadSequence()) {
// {{if .Config.Debug}}
//sessionHandlerLog.Infof("[tunnel] Write %d bytes to tunnel %d (read seq: %d)", len(recv.Data), recv.TunnelID, recv.Sequence)
// {{end}}
tunnel.Writer.Write(recv.Data)
// Delete the entry we just wrote from the cache
tunnelDataCache.DeleteSeq(tunnel.ID, tunnel.ReadSequence())
tunnel.IncReadSequence() // Increment sequence counter
// {{if .Config.Debug}}
//sessionHandlerLog.Infof("[message just received] %v", tunnelData)
// {{end}}
}
//If cache is building up it probably means a msg was lost and the server is currently hung waiting for it.
//Send a Resend packet to have the msg resent from the cache
if tunnelDataCache.Len(tunnel.ID) > 3 {
data, err := proto.Marshal(&sliverpb.TunnelData{
Sequence: tunnel.WriteSequence(), // The tunnel write sequence
Ack: tunnel.ReadSequence(),
Resend: true,
TunnelID: tunnel.ID,
Data: []byte{},
})
if err != nil {
// {{if .Config.Debug}}
//sessionHandlerLog.Infof("[shell] Failed to marshal protobuf %s", err)
// {{end}}
} else {
// {{if .Config.Debug}}
//sessionHandlerLog.Infof("[tunnel] Requesting resend of tunnelData seq: %d", tunnel.ReadSequence())
// {{end}}
implantConn.RequestResend(data)
}
}
RTunnelDataHandler(req, tunnel, implantConn)
return nil
}
func RTunnelDataHandler(tunnelData *sliverpb.TunnelData, tunnel *rtunnels.RTunnel, connection *core.ImplantConnection) {
// Since we have no guarantees that we will receive tunnel data in the correct order, we need
// to ensure we write the data back to the reader in the correct order. The server will ensure
// that TunnelData protobuf objects are numbered in the correct order using the Sequence property.
// Similarly we ensure that any data we write-back to the server is also numbered correctly. To
// reassemble the data, we just dump it into the cache and then advance the writer until we no longer
// have sequential data. So we can receive `n` number of incorrectly ordered Protobuf objects and
// correctly write them back to the reader.
// {{if .Config.Debug}}
//sessionHandlerLog.Infof("[tunnel] Cache tunnel %d (seq: %d)", tunnel.ID, tunnelData.Sequence)
// {{end}}
tunnelDataCache.Add(tunnel.ID, tunnelData.Sequence, tunnelData)
// NOTE: The read/write semantics can be a little mind boggling, just remember we're reading
// from the server and writing to the tunnel's reader (e.g. stdout), so that's why ReadSequence
// is used here whereas WriteSequence is used for data written back to the server
// Go through cache and write all sequential data to the reader
for recv, ok := tunnelDataCache.Get(tunnel.ID, tunnel.ReadSequence()); ok; recv, ok = tunnelDataCache.Get(tunnel.ID, tunnel.ReadSequence()) {
// {{if .Config.Debug}}
//sessionHandlerLog.Infof("[tunnel] Write %d bytes to tunnel %d (read seq: %d)", len(recv.Data), recv.TunnelID, recv.Sequence)
// {{end}}
tunnel.Writer.Write(recv.Data)
// Delete the entry we just wrote from the cache
tunnelDataCache.DeleteSeq(tunnel.ID, tunnel.ReadSequence())
tunnel.IncReadSequence() // Increment sequence counter
// {{if .Config.Debug}}
//sessionHandlerLog.Infof("[message just received] %v", tunnelData)
// {{end}}
func rejectReverseTunnel(implantConn *core.ImplantConnection, tunnelID uint64, reason error) {
sessionHandlerLog.Warnf("Rejected reverse tunnel %d: %v", tunnelID, reason)
data, err := proto.Marshal(&sliverpb.TunnelData{Closed: true, TunnelID: tunnelID})
if err != nil || implantConn == nil {
return
}
rejectionSlots := reverseTunnelRejectionSlots
select {
case rejectionSlots <- struct{}{}:
default:
sessionHandlerLog.Warnf("Closing implant connection because rejection delivery capacity is exhausted for reverse tunnel %d", tunnelID)
implantConn.Close()
return
}
//If cache is building up it probably means a msg was lost and the server is currently hung waiting for it.
//Send a Resend packet to have the msg resent from the cache
if tunnelDataCache.Len(tunnel.ID) > 3 {
data, err := proto.Marshal(&sliverpb.TunnelData{
Sequence: tunnel.WriteSequence(), // The tunnel write sequence
Ack: tunnel.ReadSequence(),
Resend: true,
TunnelID: tunnel.ID,
Data: []byte{},
})
if err != nil {
// {{if .Config.Debug}}
//sessionHandlerLog.Infof("[shell] Failed to marshal protobuf %s", err)
// {{end}}
} else {
// {{if .Config.Debug}}
//sessionHandlerLog.Infof("[tunnel] Requesting resend of tunnelData seq: %d", tunnel.ReadSequence())
// {{end}}
connection.RequestResend(data)
// HTTP implants cannot poll until their request handler returns. Deliver the
// rejection asynchronously, but cap both goroutine count and wait time. If
// the only close notification cannot be delivered, fail the connection
// closed so no server-side authorization or relay can be orphaned.
go func() {
defer func() { <-rejectionSlots }()
if err := implantConn.SendEnvelope(&sliverpb.Envelope{Type: sliverpb.MsgTunnelClose, Data: data}, reverseTunnelRejectionTimeout); err != nil {
if !errors.Is(err, core.ErrImplantConnectionClosed) {
sessionHandlerLog.Warnf("Closing implant connection after rejection delivery timed out for reverse tunnel %d", tunnelID)
implantConn.Close()
}
}
}()
}
func RTunnelDataHandler(tunnelData *sliverpb.TunnelData, tunnel *rtunnels.RTunnel, connection *core.ImplantConnection) {
if tunnelData == nil || tunnel == nil || connection == nil || tunnel.Writer == nil {
return
}
if tunnelData.Resend {
sessionHandlerLog.Warnf("Closing reverse tunnel %d after unsupported resend control frame", tunnel.ID)
_, _ = closeReverseTunnelLocal(tunnel)
return
}
pending, err := tunnel.ProcessInbound(tunnelData.Sequence, tunnelData.Data, func(payload []byte) error {
if deadlineWriter, ok := tunnel.Writer.(interface{ SetWriteDeadline(time.Time) error }); ok {
_ = deadlineWriter.SetWriteDeadline(time.Now().Add(rtunnels.DefaultDialTimeout))
defer func() {
_ = deadlineWriter.SetWriteDeadline(time.Time{})
}()
}
written, writeErr := tunnel.Writer.Write(payload)
if writeErr != nil {
return writeErr
}
if written != len(payload) {
return io.ErrShortWrite
}
return nil
})
if err != nil {
if errors.Is(err, rtunnels.ErrReverseTunnelClosed) {
return
}
closed, closeErr := closeReverseTunnelLocal(tunnel)
if !closed {
return
}
sessionHandlerLog.Warnf("Closing reverse tunnel %d after bounded inbound relay failure: %v", tunnel.ID, err)
if closeErr != nil {
sessionHandlerLog.Warnf("Failed to notify implant while closing reverse tunnel %d: %v", tunnel.ID, closeErr)
}
if errors.Is(err, rtunnels.ErrReverseTunnelTerminal) {
connection.Close()
}
return
}
_ = pending // reliable reverse transports retain bounded delayed frames
if tunnel.PeerCloseReady() {
_ = closeReverseTunnelRemote(tunnel)
}
}
func closeReverseTunnelLocal(tunnel *rtunnels.RTunnel) (bool, error) {
return rtunnels.CloseLocalIfActive(tunnel)
}
func closeReverseTunnelRemote(tunnel *rtunnels.RTunnel) bool {
return rtunnels.CloseRemoteIfActive(tunnel)
}
File diff suppressed because it is too large. Load diff
Loaded 100 of 152 files, more files were not shown because too many files have changed in this diff. Show more