Compare commits

..
Author SHA1 Message Date
moloch-- 3a93f312b4 Add optional Wireguard multiplayer 2026-03-15 09:38:54 -07:00
moloch-- b355fe70d6 Add Windows makefile 2026-03-13 16:14:11 -07:00
moloch-- 10b1079286 Tweak agents.md 2026-03-13 15:29:45 -07:00
Joe 9c042e6977 Merge pull request #2217 from BishopFox/fix/cert-revoke
Improve operator cert revocation
2026-03-13 12:04:26 -07:00
Joe 62583cac8f Merge branch 'master' into fix/cert-revoke 2026-03-13 11:02:34 -07:00
moloch 95764945b8 Improve operator cert revocation 2026-03-13 10:59:00 -07:00
Joe 26abda1c15 Merge pull request #2216 from skoveit/fix/timeout-int64p-mismatch
fix: timeout flag ignored in aliases and extensions due to type mismatch
2026-03-13 07:38:21 -07:00
moloch 01dd3cac72 Go mod tidy 2026-03-13 07:02:06 -07:00
0xOral f8faa1f767 fix: normalize all command timeout flags to Int64P across client codebase 2026-03-13 02:49:01 +02:00
0xOral a9e48e84cd fix: extension timeout flag ignored due to IntP/Int64P mismatch 2026-03-13 02:30:55 +02:00
0xOral 9d6704d925 fix: alias -t flag always capped at 30s due to IntP/Int64P type mismatch (#2183) 2026-03-13 02:04:35 +02:00
Joe ce5157e2e9 Merge pull request #2213 from skoveit/fix/wininet-handle-leak
fix(wininet): Fix handle leak in WinINet driver
2026-03-11 08:29:42 -07:00
0xOral 7d19051df8 fix(wininet): close connection and request handles after each request 2026-03-11 01:32:27 +02:00
Joe a0d3bd579f Merge pull request #2211 from skoveit/feat/configurable-portfwd-keepalive
feat: make TCP keepalive period configurable for portfwd and rportfwd
2026-03-08 10:36:42 -07:00
0xOral 82c309632a default keepalive value 30s for both rportfwd and portfwd 2026-03-08 17:27:27 +02:00
0xOral 5c0b8ec460 disable keepalive with --keepalive -1 value 2026-03-08 17:25:06 +02:00
0xOral 7791a86eb4 configurable keepalive: portfwd --keepalive 2026-03-08 17:20:35 +02:00
Joe 7ac4db3fa0 Merge pull request #2207 from BishopFox/fix/beacon-getsystem
Fix for getsystem in beacon mode
2026-03-05 18:31:39 -07:00
moloch-- 512bc822ef Implement fix for getsystem in beacon mode 2026-03-05 08:22:42 -08:00
Joe f2bb9c3733 Merge pull request #2205 from BishopFox/feature/lazy-lg-msg
Lazy spool large messages to disk
2026-03-04 15:40:56 -07:00
moloch-- 0bd4f100fd Lazy spool large messages to disk 2026-03-04 14:21:07 -08:00
Joe 3230322fd6 Merge pull request #2201 from BishopFox/feature/exec-shellcode
Add support for wasm-donut options in windows `execute-shellcode`
2026-03-02 08:23:49 -07:00
74 changed files with 5666 additions and 6246 deletions

No files matched your search

+1
View File
@@ -1,3 +1,4 @@
/.vscode
/.vscode/*
/.devcontainer
/.idea
+1 -1
View File
@@ -4,7 +4,7 @@
- `make` builds server + client using vendored modules and runs `go-assets.sh`.
- `make client` builds a CGO-free client; `make debug` keeps symbols.
- `make pb` regenerates protobuf stubs from `protobuf/`.
- `./go-tests.sh` runs the curated Go test suite with build tags.
- `./go-tests.sh` runs the Go test suite, this is the only way you should run unit tests.
- Docs site: `cd docs/sliver-docs && npm install && npm run dev` for local dev; `npm run build` for a static build.
## Coding Style & Naming Conventions
+333 -184
View File
@@ -1,184 +1,333 @@
#
# Makefile for Sliver
#
GO ?= go
ARTIFACT_SUFFIX ?=
ENV =
TAGS ?= -tags go_sqlite
CGO_ENABLED = 0
ifneq (,$(findstring cgo_sqlite,$(TAGS)))
CGO_ENABLED = 1
endif
#
# Prerequisites
#
# https://stackoverflow.com/questions/5618615/check-if-a-program-exists-from-a-makefile
EXECUTABLES = uname sed git date cut $(GO)
K := $(foreach exec,$(EXECUTABLES),\
$(if $(shell which $(exec)),some string,$(error "No $(exec) in PATH")))
#
# Build Information
#
GO_MAJOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f1)
GO_MINOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f2)
MIN_SUPPORTED_GO_MAJOR_VERSION = 1
MIN_SUPPORTED_GO_MINOR_VERSION = 25
GO_VERSION_VALIDATION_ERR_MSG = Golang version is not supported, please update to at least $(MIN_SUPPORTED_GO_MAJOR_VERSION).$(MIN_SUPPORTED_GO_MINOR_VERSION)
SLIVER_PUBLIC_KEY ?= RWTZPg959v3b7tLG7VzKHRB1/QT+d3c71Uzetfa44qAoX5rH7mGoQTTR
ARMORY_PUBLIC_KEY ?= RWSBpxpRWDrD7Fe+VvRE3c2VEDC2NK80rlNCj+BX0gz44Xw07r6KQD9L
ARMORY_REPO_URL ?= https://api.github.com/repos/sliverarmory/armory/releases
CLIENT_ASSETS_PKG = github.com/bishopfox/sliver/client/assets
SLIVER_UPDATE_PKG = github.com/bishopfox/sliver/client/command/update
LDFLAGS = -ldflags "-s -w \
-X $(SLIVER_UPDATE_PKG).SliverPublicKey=$(SLIVER_PUBLIC_KEY) \
-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
# Debug builds shouldn't be stripped (-s -w flags)
LDFLAGS_DEBUG = -ldflags "-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
SED_INPLACE := sed -i
STATIC_TARGET := linux
UNAME_S := $(shell uname -s)
UNAME_P := $(shell uname -p)
# Programs required for generating protobuf/grpc files
PB_COMPILERS = protoc protoc-gen-go protoc-gen-go-grpc
ifeq ($(MAKECMDGOALS), pb)
K := $(foreach exec,$(PB_COMPILERS),\
$(if $(shell which $(exec)),some string,$(error "Missing protobuf util $(exec) in PATH")))
endif
# *** Darwin ***
ifeq ($(UNAME_S),Darwin)
SED_INPLACE := sed -i ''
STATIC_TARGET := macos
endif
# If no target is specified, determine GOARCH
ifeq ($(UNAME_P),arm)
ifeq ($(MAKECMDGOALS), )
ifeq ($(origin GOARCH), undefined)
ENV += GOARCH=arm64
endif
endif
endif
ifeq ($(MAKECMDGOALS), linux)
# Redefine LDFLAGS to add the static part
LDFLAGS = -ldflags "-s -w \
-extldflags '-static' \
-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
endif
#
# Targets
#
.PHONY: default
default: clean validate-go-version
env -u GOOS -u GOARCH $(MAKE) GOOS= GOARCH= .downloaded_assets
$(ENV) $(if $(GOOS),GOOS=$(GOOS)) $(if $(GOARCH),GOARCH=$(GOARCH)) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
$(ENV) $(if $(GOOS),GOOS=$(GOOS)) $(if $(GOARCH),GOARCH=$(GOARCH)) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
# Allows you to build a CGO-free client for any target e.g. `GOOS=windows GOARCH=arm64 make client`
# NOTE: WireGuard is not supported on all platforms, but most 64-bit GOOS/GOARCH combinations should work.
.PHONY: client
client: clean .downloaded_assets validate-go-version
$(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client ./client
.PHONY: macos-amd64
macos-amd64: clean .downloaded_assets validate-go-version
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
.PHONY: macos-arm64
macos-arm64: clean .downloaded_assets validate-go-version
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
.PHONY: linux-amd64
linux-amd64: clean .downloaded_assets validate-go-version
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
.PHONY: linux-arm64
linux-arm64: clean .downloaded_assets validate-go-version
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
.PHONY: windows-amd64
windows-amd64: clean .downloaded_assets validate-go-version
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX).exe ./server
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX).exe ./client
.PHONY: clients
clients: clean .downloaded_assets validate-go-version
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_macos-amd64$(ARTIFACT_SUFFIX) ./client
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_macos-arm64$(ARTIFACT_SUFFIX) ./client
GOOS=linux GOARCH=386 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_linux-386$(ARTIFACT_SUFFIX) ./client
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_linux-amd64$(ARTIFACT_SUFFIX) ./client
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_linux-arm64$(ARTIFACT_SUFFIX) ./client
GOOS=windows GOARCH=386 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_windows-386$(ARTIFACT_SUFFIX).exe ./client
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_windows-amd64$(ARTIFACT_SUFFIX).exe ./client
GOOS=windows GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_windows-arm64$(ARTIFACT_SUFFIX).exe ./client
GOOS=freebsd GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_freebsd-amd64$(ARTIFACT_SUFFIX) ./client
GOOS=freebsd GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_freebsd-arm64$(ARTIFACT_SUFFIX) ./client
.PHONY: servers
servers: clean .downloaded_assets validate-go-version
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_windows-amd64$(ARTIFACT_SUFFIX).exe ./server
GOOS=windows GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_windows-arm64$(ARTIFACT_SUFFIX).exe ./server
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_linux-amd64$(ARTIFACT_SUFFIX) ./server
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_linux-arm64$(ARTIFACT_SUFFIX) ./server
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_darwin-arm64$(ARTIFACT_SUFFIX) ./server
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_darwin-amd64$(ARTIFACT_SUFFIX) ./server
.PHONY: pb
pb:
protoc -I protobuf/ protobuf/commonpb/common.proto --go_out=paths=source_relative:protobuf/
protoc -I protobuf/ protobuf/sliverpb/sliver.proto --go_out=paths=source_relative:protobuf/
protoc -I protobuf/ protobuf/clientpb/client.proto --go_out=paths=source_relative:protobuf/
protoc -I protobuf/ protobuf/dnspb/dns.proto --go_out=paths=source_relative:protobuf/
protoc -I protobuf/ protobuf/rpcpb/services.proto --go_out=paths=source_relative:protobuf/ --go-grpc_out=protobuf/ --go-grpc_opt=paths=source_relative
.PHONY: debug
debug: clean
$(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor $(TAGS),server $(LDFLAGS_DEBUG) -o sliver-server$(ARTIFACT_SUFFIX) ./server
$(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor $(TAGS),client $(LDFLAGS_DEBUG) -o sliver-client$(ARTIFACT_SUFFIX) ./client
validate-go-version:
@if [ $(GO_MAJOR_VERSION) -gt $(MIN_SUPPORTED_GO_MAJOR_VERSION) ]; then \
exit 0 ;\
elif [ $(GO_MAJOR_VERSION) -lt $(MIN_SUPPORTED_GO_MAJOR_VERSION) ]; then \
echo '$(GO_VERSION_VALIDATION_ERR_MSG)';\
exit 1; \
elif [ $(GO_MINOR_VERSION) -lt $(MIN_SUPPORTED_GO_MINOR_VERSION) ] ; then \
echo '$(GO_VERSION_VALIDATION_ERR_MSG)';\
exit 1; \
fi
.PHONY: clean-all
clean-all: clean
rm -rf ./server/assets/fs/darwin/amd64
rm -rf ./server/assets/fs/darwin/arm64
rm -rf ./server/assets/fs/windows/amd64
rm -rf ./server/assets/fs/linux/amd64
rm -f ./server/assets/fs/*.zip
rm -f ./.downloaded_assets
.PHONY: clean
clean:
rm -f sliver-client sliver-client_* sliver-server sliver-server_* sliver-*.exe
.downloaded_assets:
$(ENV) $(GO) run -mod=vendor ./util/cmd/assets
touch ./.downloaded_assets
#
# Makefile for Sliver
#
ifneq ($(OS),Windows_NT)
GO ?= go
ARTIFACT_SUFFIX ?=
ENV =
TAGS ?= -tags go_sqlite
CGO_ENABLED = 0
ifneq (,$(findstring cgo_sqlite,$(TAGS)))
CGO_ENABLED = 1
endif
#
# Prerequisites
#
# https://stackoverflow.com/questions/5618615/check-if-a-program-exists-from-a-makefile
EXECUTABLES = uname sed git date cut $(GO)
K := $(foreach exec,$(EXECUTABLES),\
$(if $(shell which $(exec)),some string,$(error "No $(exec) in PATH")))
#
# Build Information
#
GO_MAJOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f1)
GO_MINOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f2)
MIN_SUPPORTED_GO_MAJOR_VERSION = 1
MIN_SUPPORTED_GO_MINOR_VERSION = 25
GO_VERSION_VALIDATION_ERR_MSG = Golang version is not supported, please update to at least $(MIN_SUPPORTED_GO_MAJOR_VERSION).$(MIN_SUPPORTED_GO_MINOR_VERSION)
SLIVER_PUBLIC_KEY ?= RWTZPg959v3b7tLG7VzKHRB1/QT+d3c71Uzetfa44qAoX5rH7mGoQTTR
ARMORY_PUBLIC_KEY ?= RWSBpxpRWDrD7Fe+VvRE3c2VEDC2NK80rlNCj+BX0gz44Xw07r6KQD9L
ARMORY_REPO_URL ?= https://api.github.com/repos/sliverarmory/armory/releases
CLIENT_ASSETS_PKG = github.com/bishopfox/sliver/client/assets
SLIVER_UPDATE_PKG = github.com/bishopfox/sliver/client/command/update
LDFLAGS = -ldflags "-s -w \
-X $(SLIVER_UPDATE_PKG).SliverPublicKey=$(SLIVER_PUBLIC_KEY) \
-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
# Debug builds shouldn't be stripped (-s -w flags)
LDFLAGS_DEBUG = -ldflags "-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
SED_INPLACE := sed -i
STATIC_TARGET := linux
UNAME_S := $(shell uname -s)
UNAME_P := $(shell uname -p)
# Programs required for generating protobuf/grpc files
PB_COMPILERS = protoc protoc-gen-go protoc-gen-go-grpc
ifeq ($(MAKECMDGOALS), pb)
K := $(foreach exec,$(PB_COMPILERS),\
$(if $(shell which $(exec)),some string,$(error "Missing protobuf util $(exec) in PATH")))
endif
# *** Darwin ***
ifeq ($(UNAME_S),Darwin)
SED_INPLACE := sed -i ''
STATIC_TARGET := macos
endif
# If no target is specified, determine GOARCH
ifeq ($(UNAME_P),arm)
ifeq ($(MAKECMDGOALS), )
ifeq ($(origin GOARCH), undefined)
ENV += GOARCH=arm64
endif
endif
endif
ifeq ($(MAKECMDGOALS), linux)
# Redefine LDFLAGS to add the static part
LDFLAGS = -ldflags "-s -w \
-extldflags '-static' \
-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) \
-X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
endif
#
# Targets
#
.PHONY: default
default: clean validate-go-version
env -u GOOS -u GOARCH $(MAKE) GOOS= GOARCH= .downloaded_assets
$(ENV) $(if $(GOOS),GOOS=$(GOOS)) $(if $(GOARCH),GOARCH=$(GOARCH)) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
$(ENV) $(if $(GOOS),GOOS=$(GOOS)) $(if $(GOARCH),GOARCH=$(GOARCH)) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
# Allows you to build a CGO-free client for any target e.g. `GOOS=windows GOARCH=arm64 make client`
# NOTE: WireGuard is not supported on all platforms, but most 64-bit GOOS/GOARCH combinations should work.
.PHONY: client
client: clean .downloaded_assets validate-go-version
$(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client ./client
.PHONY: macos-amd64
macos-amd64: clean .downloaded_assets validate-go-version
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
.PHONY: macos-arm64
macos-arm64: clean .downloaded_assets validate-go-version
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
.PHONY: linux-amd64
linux-amd64: clean .downloaded_assets validate-go-version
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
.PHONY: linux-arm64
linux-arm64: clean .downloaded_assets validate-go-version
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX) ./server
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX) ./client
.PHONY: windows-amd64
windows-amd64: clean .downloaded_assets validate-go-version
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server$(ARTIFACT_SUFFIX).exe ./server
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client$(ARTIFACT_SUFFIX).exe ./client
.PHONY: clients
clients: clean .downloaded_assets validate-go-version
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_macos-amd64$(ARTIFACT_SUFFIX) ./client
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_macos-arm64$(ARTIFACT_SUFFIX) ./client
GOOS=linux GOARCH=386 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_linux-386$(ARTIFACT_SUFFIX) ./client
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_linux-amd64$(ARTIFACT_SUFFIX) ./client
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_linux-arm64$(ARTIFACT_SUFFIX) ./client
GOOS=windows GOARCH=386 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_windows-386$(ARTIFACT_SUFFIX).exe ./client
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_windows-amd64$(ARTIFACT_SUFFIX).exe ./client
GOOS=windows GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_windows-arm64$(ARTIFACT_SUFFIX).exe ./client
GOOS=freebsd GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_freebsd-amd64$(ARTIFACT_SUFFIX) ./client
GOOS=freebsd GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),client $(LDFLAGS) -o sliver-client_freebsd-arm64$(ARTIFACT_SUFFIX) ./client
.PHONY: servers
servers: clean .downloaded_assets validate-go-version
GOOS=windows GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_windows-amd64$(ARTIFACT_SUFFIX).exe ./server
GOOS=windows GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_windows-arm64$(ARTIFACT_SUFFIX).exe ./server
GOOS=linux GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_linux-amd64$(ARTIFACT_SUFFIX) ./server
GOOS=linux GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_linux-arm64$(ARTIFACT_SUFFIX) ./server
GOOS=darwin GOARCH=arm64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_darwin-arm64$(ARTIFACT_SUFFIX) ./server
GOOS=darwin GOARCH=amd64 $(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor -trimpath $(TAGS),server $(LDFLAGS) -o sliver-server_darwin-amd64$(ARTIFACT_SUFFIX) ./server
.PHONY: pb
pb:
protoc -I protobuf/ protobuf/commonpb/common.proto --go_out=paths=source_relative:protobuf/
protoc -I protobuf/ protobuf/sliverpb/sliver.proto --go_out=paths=source_relative:protobuf/
protoc -I protobuf/ protobuf/clientpb/client.proto --go_out=paths=source_relative:protobuf/
protoc -I protobuf/ protobuf/dnspb/dns.proto --go_out=paths=source_relative:protobuf/
protoc -I protobuf/ protobuf/rpcpb/services.proto --go_out=paths=source_relative:protobuf/ --go-grpc_out=protobuf/ --go-grpc_opt=paths=source_relative
.PHONY: debug
debug: clean
$(ENV) CGO_ENABLED=$(CGO_ENABLED) $(GO) build -mod=vendor $(TAGS),server $(LDFLAGS_DEBUG) -o sliver-server$(ARTIFACT_SUFFIX) ./server
$(ENV) CGO_ENABLED=0 $(GO) build -mod=vendor $(TAGS),client $(LDFLAGS_DEBUG) -o sliver-client$(ARTIFACT_SUFFIX) ./client
validate-go-version:
@if [ $(GO_MAJOR_VERSION) -gt $(MIN_SUPPORTED_GO_MAJOR_VERSION) ]; then \
exit 0 ;\
elif [ $(GO_MAJOR_VERSION) -lt $(MIN_SUPPORTED_GO_MAJOR_VERSION) ]; then \
echo '$(GO_VERSION_VALIDATION_ERR_MSG)';\
exit 1; \
elif [ $(GO_MINOR_VERSION) -lt $(MIN_SUPPORTED_GO_MINOR_VERSION) ] ; then \
echo '$(GO_VERSION_VALIDATION_ERR_MSG)';\
exit 1; \
fi
.PHONY: clean-all
clean-all: clean
rm -rf ./server/assets/fs/darwin/amd64
rm -rf ./server/assets/fs/darwin/arm64
rm -rf ./server/assets/fs/windows/amd64
rm -rf ./server/assets/fs/linux/amd64
rm -f ./server/assets/fs/*.zip
rm -f ./.downloaded_assets
.PHONY: clean
clean:
rm -f sliver-client sliver-client_* sliver-server sliver-server_* sliver-*.exe
.downloaded_assets:
$(ENV) $(GO) run -mod=vendor ./util/cmd/assets
touch ./.downloaded_assets
#
# >>> WINDOWS <<<
#
else
GO ?= go
ARTIFACT_SUFFIX ?=
ENV =
TAGS ?= -tags go_sqlite
CGO_ENABLED = 0
ifneq (,$(findstring cgo_sqlite,$(TAGS)))
CGO_ENABLED = 1
endif
SHELL := cmd.exe
.SHELLFLAGS := /C
GO_VERSION := $(patsubst go%,%,$(strip $(shell $(GO) env GOVERSION)))
GO_MAJOR_VERSION := $(word 1,$(subst ., ,$(GO_VERSION)))
GO_MINOR_VERSION := $(word 2,$(subst ., ,$(GO_VERSION)))
MIN_SUPPORTED_GO_MAJOR_VERSION = 1
MIN_SUPPORTED_GO_MINOR_VERSION = 25
GO_VERSION_VALIDATION_ERR_MSG = Golang version is not supported, please update to at least $(MIN_SUPPORTED_GO_MAJOR_VERSION).$(MIN_SUPPORTED_GO_MINOR_VERSION)
SLIVER_PUBLIC_KEY ?= RWTZPg959v3b7tLG7VzKHRB1/QT+d3c71Uzetfa44qAoX5rH7mGoQTTR
ARMORY_PUBLIC_KEY ?= RWSBpxpRWDrD7Fe+VvRE3c2VEDC2NK80rlNCj+BX0gz44Xw07r6KQD9L
ARMORY_REPO_URL ?= https://api.github.com/repos/sliverarmory/armory/releases
CLIENT_ASSETS_PKG = github.com/bishopfox/sliver/client/assets
SLIVER_UPDATE_PKG = github.com/bishopfox/sliver/client/command/update
LDFLAGS = -ldflags "-s -w -X $(SLIVER_UPDATE_PKG).SliverPublicKey=$(SLIVER_PUBLIC_KEY) -X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) -X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
LDFLAGS_DEBUG = -ldflags "-X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) -X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
PB_COMPILERS = protoc protoc-gen-go protoc-gen-go-grpc
COMMA := ,
ifeq ($(MAKECMDGOALS), linux)
LDFLAGS = -ldflags "-s -w -extldflags '-static' -X $(CLIENT_ASSETS_PKG).DefaultArmoryPublicKey=$(ARMORY_PUBLIC_KEY) -X $(CLIENT_ASSETS_PKG).DefaultArmoryRepoURL=$(ARMORY_REPO_URL)"
endif
define windows_exec
$(strip $(foreach envvar,$(1),set "$(envvar)" && ))$(2)
endef
define windows_go_build
$(call windows_exec,$(ENV) GOOS=$(1) GOARCH=$(2) CGO_ENABLED=$(3),"$(GO)" build $(4) $(TAGS)$(COMMA)$(5) $(6) -o $(7) ./$(5))
endef
.PHONY: default
default: clean validate-go-version
$(call windows_exec,$(ENV) GOOS= GOARCH=,"$(MAKE)" GOOS= GOARCH= .downloaded_assets)
$(call windows_go_build,$(GOOS),$(GOARCH),$(CGO_ENABLED),-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server$(ARTIFACT_SUFFIX))
$(call windows_go_build,$(GOOS),$(GOARCH),0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client$(ARTIFACT_SUFFIX))
.PHONY: client
client: clean .downloaded_assets validate-go-version
$(call windows_go_build,$(GOOS),$(GOARCH),0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client)
.PHONY: macos-amd64
macos-amd64: clean .downloaded_assets validate-go-version
$(call windows_go_build,darwin,amd64,$(CGO_ENABLED),-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server$(ARTIFACT_SUFFIX))
$(call windows_go_build,darwin,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client$(ARTIFACT_SUFFIX))
.PHONY: macos-arm64
macos-arm64: clean .downloaded_assets validate-go-version
$(call windows_go_build,darwin,arm64,$(CGO_ENABLED),-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server$(ARTIFACT_SUFFIX))
$(call windows_go_build,darwin,arm64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client$(ARTIFACT_SUFFIX))
.PHONY: linux-amd64
linux-amd64: clean .downloaded_assets validate-go-version
$(call windows_go_build,linux,amd64,$(CGO_ENABLED),-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server$(ARTIFACT_SUFFIX))
$(call windows_go_build,linux,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client$(ARTIFACT_SUFFIX))
.PHONY: linux-arm64
linux-arm64: clean .downloaded_assets validate-go-version
$(call windows_go_build,linux,arm64,$(CGO_ENABLED),-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server$(ARTIFACT_SUFFIX))
$(call windows_go_build,linux,arm64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client$(ARTIFACT_SUFFIX))
.PHONY: windows-amd64
windows-amd64: clean .downloaded_assets validate-go-version
$(call windows_go_build,windows,amd64,$(CGO_ENABLED),-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server$(ARTIFACT_SUFFIX).exe)
$(call windows_go_build,windows,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client$(ARTIFACT_SUFFIX).exe)
.PHONY: clients
clients: clean .downloaded_assets validate-go-version
$(call windows_go_build,darwin,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_macos-amd64$(ARTIFACT_SUFFIX))
$(call windows_go_build,darwin,arm64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_macos-arm64$(ARTIFACT_SUFFIX))
$(call windows_go_build,linux,386,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_linux-386$(ARTIFACT_SUFFIX))
$(call windows_go_build,linux,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_linux-amd64$(ARTIFACT_SUFFIX))
$(call windows_go_build,linux,arm64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_linux-arm64$(ARTIFACT_SUFFIX))
$(call windows_go_build,windows,386,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_windows-386$(ARTIFACT_SUFFIX).exe)
$(call windows_go_build,windows,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_windows-amd64$(ARTIFACT_SUFFIX).exe)
$(call windows_go_build,windows,arm64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_windows-arm64$(ARTIFACT_SUFFIX).exe)
$(call windows_go_build,freebsd,amd64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_freebsd-amd64$(ARTIFACT_SUFFIX))
$(call windows_go_build,freebsd,arm64,0,-mod=vendor -trimpath,client,$(LDFLAGS),sliver-client_freebsd-arm64$(ARTIFACT_SUFFIX))
.PHONY: servers
servers: clean .downloaded_assets validate-go-version
$(call windows_go_build,windows,amd64,0,-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server_windows-amd64$(ARTIFACT_SUFFIX).exe)
$(call windows_go_build,windows,arm64,0,-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server_windows-arm64$(ARTIFACT_SUFFIX).exe)
$(call windows_go_build,linux,amd64,0,-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server_linux-amd64$(ARTIFACT_SUFFIX))
$(call windows_go_build,linux,arm64,0,-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server_linux-arm64$(ARTIFACT_SUFFIX))
$(call windows_go_build,darwin,arm64,0,-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server_darwin-arm64$(ARTIFACT_SUFFIX))
$(call windows_go_build,darwin,amd64,0,-mod=vendor -trimpath,server,$(LDFLAGS),sliver-server_darwin-amd64$(ARTIFACT_SUFFIX))
.PHONY: pb
pb: validate-pb-compilers
protoc -I protobuf/ protobuf/commonpb/common.proto --go_out=paths=source_relative:protobuf/
protoc -I protobuf/ protobuf/sliverpb/sliver.proto --go_out=paths=source_relative:protobuf/
protoc -I protobuf/ protobuf/clientpb/client.proto --go_out=paths=source_relative:protobuf/
protoc -I protobuf/ protobuf/dnspb/dns.proto --go_out=paths=source_relative:protobuf/
protoc -I protobuf/ protobuf/rpcpb/services.proto --go_out=paths=source_relative:protobuf/ --go-grpc_out=protobuf/ --go-grpc_opt=paths=source_relative
.PHONY: debug
debug: clean
$(call windows_go_build,$(GOOS),$(GOARCH),$(CGO_ENABLED),-mod=vendor,server,$(LDFLAGS_DEBUG),sliver-server$(ARTIFACT_SUFFIX))
$(call windows_go_build,$(GOOS),$(GOARCH),0,-mod=vendor,client,$(LDFLAGS_DEBUG),sliver-client$(ARTIFACT_SUFFIX))
.PHONY: validate-pb-compilers
validate-pb-compilers:
@for %%P in ($(PB_COMPILERS)) do @where.exe %%P >NUL || (echo Missing protobuf util %%P in PATH & exit /b 1)
validate-go-version:
@if $(GO_MAJOR_VERSION) GTR $(MIN_SUPPORTED_GO_MAJOR_VERSION) (exit /b 0) else if $(GO_MAJOR_VERSION) LSS $(MIN_SUPPORTED_GO_MAJOR_VERSION) (echo $(GO_VERSION_VALIDATION_ERR_MSG) & exit /b 1) else if $(GO_MINOR_VERSION) LSS $(MIN_SUPPORTED_GO_MINOR_VERSION) (echo $(GO_VERSION_VALIDATION_ERR_MSG) & exit /b 1)
.PHONY: clean-all
clean-all: clean
-rmdir /S /Q server\assets\fs\darwin\amd64
-rmdir /S /Q server\assets\fs\darwin\arm64
-rmdir /S /Q server\assets\fs\windows\amd64
-rmdir /S /Q server\assets\fs\linux\amd64
-del /Q /F server\assets\fs\*.zip 2>NUL
-del /Q /F .downloaded_assets 2>NUL
.PHONY: clean
clean:
-del /Q /F sliver-client sliver-client_* sliver-server sliver-server_* sliver-*.exe 2>NUL
.downloaded_assets:
$(call windows_exec,$(ENV),"$(GO)" run -mod=vendor ./util/cmd/assets)
@type NUL > .downloaded_assets
endif
+17 -7
View File
@@ -34,15 +34,25 @@ const (
ConfigDirName = "configs"
)
// ClientWGConfig - Optional WireGuard wrapper configuration for multiplayer.
type ClientWGConfig struct {
ServerPubKey string `json:"server_pub_key"`
ClientPrivateKey string `json:"client_private_key"`
ClientPubKey string `json:"client_pub_key"`
ClientIP string `json:"client_ip"`
ServerIP string `json:"server_ip"`
}
// ClientConfig - Client JSON config
type ClientConfig struct {
Operator string `json:"operator"` // This value is actually ignored for the most part (cert CN is used instead)
LHost string `json:"lhost"`
LPort int `json:"lport"`
Token string `json:"token"`
CACertificate string `json:"ca_certificate"`
PrivateKey string `json:"private_key"`
Certificate string `json:"certificate"`
Operator string `json:"operator"` // This value is actually ignored for the most part (cert CN is used instead)
LHost string `json:"lhost"`
LPort int `json:"lport"`
Token string `json:"token"`
CACertificate string `json:"ca_certificate"`
PrivateKey string `json:"private_key"`
Certificate string `json:"certificate"`
WG *ClientWGConfig `json:"wg,omitempty"`
}
// GetConfigDir - Returns the path to the config dir
+2
View File
@@ -52,6 +52,8 @@ func init() {
rootCmd.TraverseChildren = true
rootCmd.Flags().String(RCFlagName, "", "path to rc script file")
rootCmd.PersistentFlags().Bool(disableWGFlag, false, "connect to multiplayer directly even if the operator config includes a WireGuard wrapper")
rootCmd.PersistentFlags().Bool(requireWGFlag, false, "require the operator config's WireGuard wrapper for multiplayer connections")
// Create the console client, without any RPC or commands bound to it yet.
// This created before anything so that multiple commands can make use of
+3
View File
@@ -44,6 +44,9 @@ func consoleCmd(con *console.SliverClient) *cobra.Command {
func consoleRunnerCmd(con *console.SliverClient, run bool) (pre, post func(cmd *cobra.Command, args []string) error) {
pre = func(cmd *cobra.Command, _ []string) error {
if err := applyMultiplayerConnectMode(cmd); err != nil {
return err
}
configs := assets.GetConfigs()
if len(configs) == 0 {
+5 -1
View File
@@ -43,6 +43,10 @@ func mcpCmd(con *console.SliverClient) *cobra.Command {
}
func runMCP(cmd *cobra.Command, con *console.SliverClient) error {
if err := applyMultiplayerConnectMode(cmd); err != nil {
return err
}
configPath, _ := cmd.Flags().GetString("config")
config, err := loadMCPClientConfig(configPath)
if err != nil {
@@ -55,7 +59,7 @@ func runMCP(cmd *cobra.Command, con *console.SliverClient) error {
fmt.Printf("Connection to server failed %s\n", err)
return nil
}
defer ln.Close()
defer transport.CloseGRPCConnection(ln)
con.Rpc = rpc
go handleConnectionLost(ln)
+42
View File
@@ -0,0 +1,42 @@
package cli
import (
"fmt"
"github.com/bishopfox/sliver/client/transport"
"github.com/spf13/cobra"
)
const (
requireWGFlag = "require-wg"
disableWGFlag = "disable-wg"
)
func applyMultiplayerConnectMode(cmd *cobra.Command) error {
if cmd == nil {
transport.SetMultiplayerConnectMode(transport.MultiplayerConnectAuto)
return nil
}
requireWG, err := cmd.Flags().GetBool(requireWGFlag)
if err != nil {
return err
}
disableWG, err := cmd.Flags().GetBool(disableWGFlag)
if err != nil {
return err
}
if requireWG && disableWG {
return fmt.Errorf("--%s and --%s cannot be used together", requireWGFlag, disableWGFlag)
}
mode := transport.MultiplayerConnectAuto
switch {
case requireWG:
mode = transport.MultiplayerConnectRequireWG
case disableWG:
mode = transport.MultiplayerConnectDisableWG
}
transport.SetMultiplayerConnectMode(mode)
return nil
}
+1 -1
View File
@@ -250,7 +250,7 @@ func LoadAlias(manifestPath string, cmd *cobra.Command, con *console.SliverClien
f.StringP("process-arguments", "A", "", "arguments to pass to the hosting process")
f.Uint32P("ppid", "P", 0, "parent process ID to use when creating the hosting process (Windows only)")
f.BoolP("save", "s", false, "Save output to disk")
f.IntP("timeout", "t", defaultTimeout, "command timeout in seconds")
f.Int64P("timeout", "t", defaultTimeout, "command timeout in seconds")
addAliasCmd.Flags().AddFlagSet(f)
// Setup completions for alias arguments
+1 -1
View File
@@ -28,7 +28,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
},
}
flags.Bind("beacons", true, beaconsCmd, func(f *pflag.FlagSet) {
f.IntP("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
flags.Bind("beacons", false, beaconsCmd, func(f *pflag.FlagSet) {
f.StringP("kill", "k", "", "kill the designated beacon")
+1 -1
View File
@@ -377,7 +377,7 @@ func ExtensionRegisterCommand(extCmd *ExtCommand, cmd *cobra.Command, con *conso
// Flags
f := pflag.NewFlagSet(extCmd.CommandName, pflag.ContinueOnError)
f.BoolP("save", "s", false, "Save output to disk")
f.IntP("timeout", "t", defaultTimeout, "command timeout in seconds")
f.Int64P("timeout", "t", defaultTimeout, "command timeout in seconds")
extensionCmd.Flags().AddFlagSet(f)
extensionCmd.Flags().ParseErrorsWhitelist.UnknownFlags = true
+1 -1
View File
@@ -36,7 +36,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
GroupID: consts.PayloadsHelpGroup,
}
flags.Bind("generate", true, generateCmd, func(f *pflag.FlagSet) {
f.IntP("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
// Session flags and completions.
+2 -2
View File
@@ -43,12 +43,12 @@ func Commands(con *console.SliverClient) []*cobra.Command {
GroupID: consts.NetworkHelpGroup,
}
flags.Bind("jobs", true, jobsCmd, func(f *pflag.FlagSet) {
f.IntP("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
flags.Bind("jobs", false, jobsCmd, func(f *pflag.FlagSet) {
f.Int32P("kill", "k", -1, "kill a background job")
f.BoolP("kill-all", "K", false, "kill all jobs")
f.IntP("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
flags.BindFlagCompletions(jobsCmd, func(comp *carapace.ActionMap) {
(*comp)["kill"] = JobsIDCompleter(con)
+1 -1
View File
@@ -24,7 +24,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
GroupID: consts.SliverHelpGroup,
}
flags.Bind("loot", true, lootCmd, func(f *pflag.FlagSet) {
f.IntP("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
flags.Bind("loot", false, lootCmd, func(f *pflag.FlagSet) {
f.StringP("filter", "f", "", "filter based on loot type")
+1 -1
View File
@@ -81,7 +81,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
}
pivotsCmd.AddCommand(pivotDetailsCmd)
flags.Bind("", false, pivotDetailsCmd, func(f *pflag.FlagSet) {
f.IntP("id", "i", 0, "id of the pivot listener to get details for")
f.Uint32P("id", "i", 0, "id of the pivot listener to get details for")
})
flags.BindFlagCompletions(pivotDetailsCmd, func(comp *carapace.ActionMap) {
(*comp)["id"] = PivotIDCompleter(con)
+1
View File
@@ -39,6 +39,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
flags.Bind("", false, addCmd, func(f *pflag.FlagSet) {
f.StringP("remote", "r", "", "remote target host:port (e.g., 10.0.0.1:445)")
f.StringP("bind", "b", "127.0.0.1:8080", "bind port forward to interface")
f.Int32P("keepalive", "k", 0, "keepalive period in seconds (default 30s, -1 to disable)")
})
flags.BindFlagCompletions(addCmd, func(comp *carapace.ActionMap) {
(*comp)["bind"] = completers.ClientInterfacesCompleter()
+11 -1
View File
@@ -68,13 +68,23 @@ func PortfwdAddCmd(cmd *cobra.Command, con *console.SliverClient, args []string)
bindAddr = fmt.Sprintf("127.0.0.1:%s", bindAddr)
}
keepAlive, _ := cmd.Flags().GetInt32("keepalive")
var keepAlivePeriod time.Duration
if keepAlive > 0 {
keepAlivePeriod = time.Duration(keepAlive) * time.Second
} else if keepAlive < 0 {
keepAlivePeriod = -1 * time.Second
} else {
keepAlivePeriod = 30 * time.Second
}
tcpProxy := &tcpproxy.Proxy{}
channelProxy := &core.ChannelProxy{
Rpc: con.Rpc,
Session: session,
RemoteAddr: remoteAddr,
BindAddr: bindAddr,
KeepAlivePeriod: 60 * time.Second,
KeepAlivePeriod: keepAlivePeriod,
DialTimeout: 30 * time.Second,
}
tcpProxy.AddRoute(bindAddr, channelProxy)
+1 -1
View File
@@ -74,7 +74,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
GetSystemCmd(cmd, con, args)
},
GroupID: consts.PrivilegesHelpGroup,
Annotations: flags.RestrictTargets(consts.WindowsCmdsFilter),
Annotations: flags.RestrictTargets(consts.WindowsCmdsFilter, consts.SessionCmdsFilter),
}
flags.Bind("", false, getSystemCmd, func(f *pflag.FlagSet) {
f.StringP("process", "p", "spoolsv.exe", "SYSTEM process to inject into")
+8
View File
@@ -37,6 +37,10 @@ func GetSystemCmd(cmd *cobra.Command, con *console.SliverClient, args []string)
if session == nil && beacon == nil {
return
}
if session == nil {
con.PrintErrorf("Command is only supported for sessions.\n")
return
}
targetOS := getOS(session, beacon)
if targetOS != "windows" {
con.PrintErrorf("Command only supported on Windows.\n")
@@ -45,6 +49,10 @@ func GetSystemCmd(cmd *cobra.Command, con *console.SliverClient, args []string)
process, _ := cmd.Flags().GetString("process")
config := con.GetActiveSessionConfig()
if config == nil {
con.PrintErrorf("Failed to derive active session config.\n")
return
}
/* If the HTTP C2 Config name is not defined, then put in the default value
This will have no effect on implants that do not use HTTP C2
+1 -1
View File
@@ -52,7 +52,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
f.BoolP("loot", "X", false, "save output as loot")
f.StringP("loot-name", "N", "", "name to assign when adding the memory dump to the loot store (optional)")
f.Int32P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
flags.BindFlagCompletions(procdumpCmd, func(comp *carapace.ActionMap) {
(*comp)["save"] = carapace.ActionFiles()
+2 -2
View File
@@ -55,7 +55,7 @@ func ProcdumpCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
return
}
timeout, _ := cmd.Flags().GetInt32("timeout")
timeout, _ := cmd.Flags().GetInt64("timeout")
if timeout < 1 {
con.PrintErrorf("Invalid timeout argument\n")
@@ -68,7 +68,7 @@ func ProcdumpCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
dump, err := con.Rpc.ProcessDump(context.Background(), &sliverpb.ProcessDumpReq{
Request: con.ActiveTarget.Request(cmd),
Pid: int32(pid),
Timeout: timeout,
Timeout: int32(timeout),
})
ctrl <- true
<-ctrl
+1 -1
View File
@@ -22,7 +22,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
Annotations: flags.RestrictTargets(consts.WindowsCmdsFilter),
}
flags.Bind("registry", true, registryCmd, func(f *pflag.FlagSet) {
f.IntP("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
registryReadCmd := &cobra.Command{
+1
View File
@@ -39,6 +39,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
flags.Bind("", false, rportfwdAddCmd, func(f *pflag.FlagSet) {
f.StringP("remote", "r", "", "remote address <ip>:<port> connection is forwarded to")
f.StringP("bind", "b", "", "bind address <ip>:<port> for implants to listen on")
f.Int32P("keepalive", "k", 0, "keepalive period in seconds (default 30s, -1 to disable)")
})
flags.BindFlagCompletions(rportfwdAddCmd, func(comp *carapace.ActionMap) {
(*comp)["remote"] = completers.ClientInterfacesCompleter()
+3
View File
@@ -53,10 +53,13 @@ func StartRportFwdListenerCmd(cmd *cobra.Command, con *console.SliverClient, arg
if portNumberOnlyRegexp.MatchString(forwardAddress) {
forwardAddress = fmt.Sprintf("127.0.0.1:%s", forwardAddress)
}
keepAlive, _ := cmd.Flags().GetInt32("keepalive")
rportfwdListener, err := con.Rpc.StartRportFwdListener(context.Background(), &sliverpb.RportFwdStartListenerReq{
Request: con.ActiveTarget.Request(cmd),
BindAddress: bindAddress,
ForwardAddress: forwardAddress,
KeepAlive: keepAlive,
})
if err != nil {
con.PrintWarnf("%s\n", err)
+2 -2
View File
@@ -158,7 +158,7 @@ func serverSwitch(_ *cobra.Command, con *console.SliverClient) {
}
if err := con.SetConnection(rpc, conn, &console.ConnectionDetails{ConfigKey: inst.ConfigKey, Config: inst.Config}); err != nil {
_ = conn.Close()
_ = transport.CloseGRPCConnection(conn)
con.PrintErrorf("Switch failed: %s\n", err)
return
}
@@ -180,7 +180,7 @@ func serverSwitch(_ *cobra.Command, con *console.SliverClient) {
}
if err := con.SetConnection(rpc, conn, &console.ConnectionDetails{ConfigKey: result.ConfigKey, Config: cfg}); err != nil {
_ = conn.Close()
_ = transport.CloseGRPCConnection(conn)
con.PrintErrorf("Switch failed: %s\n", err)
return
}
+1 -1
View File
@@ -27,7 +27,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
GroupID: consts.SliverHelpGroup,
}
flags.Bind("sessions", true, sessionsCmd, func(f *pflag.FlagSet) {
f.IntP("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
flags.Bind("sessions", false, sessionsCmd, func(f *pflag.FlagSet) {
f.StringP("interact", "i", "", "interact with a session")
+1 -1
View File
@@ -23,7 +23,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
Annotations: flags.RestrictTargets(consts.BeaconCmdsFilter),
}
flags.Bind("tasks", true, tasksCmd, func(f *pflag.FlagSet) {
f.IntP("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
f.BoolP("overflow", "O", false, "overflow terminal width (display truncated rows)")
f.IntP("skip-pages", "S", 0, "skip the first n page(s)")
f.StringP("filter", "f", "", "filter based on task type (case-insensitive prefix matching)")
+1 -1
View File
@@ -28,7 +28,7 @@ func Commands(con *console.SliverClient) []*cobra.Command {
f.StringP("save", "s", "", "save downloaded files to specific directory (default downloads dir)")
f.BoolP("insecure", "I", false, "skip tls certificate validation")
f.Bool("force", false, "skip version comparison and download the latest release")
f.IntP("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
f.Int64P("timeout", "t", flags.DefaultTimeout, "grpc timeout in seconds")
})
flags.BindFlagCompletions(updateCmd, func(comp *carapace.ActionMap) {
(*comp)["proxy"] = completers.LocalProxyCompleter()
+1 -1
View File
@@ -60,7 +60,7 @@ const (
func UpdateCmd(cmd *cobra.Command, con *console.SliverClient, args []string) {
VerboseVersionsCmd(cmd, con, args)
timeoutF, _ := cmd.Flags().GetInt("timeout")
timeoutF, _ := cmd.Flags().GetInt64("timeout")
timeout := time.Duration(timeoutF) * time.Second
insecure, _ := cmd.Flags().GetBool("insecure")
+2 -1
View File
@@ -11,6 +11,7 @@ import (
"github.com/bishopfox/sliver/client/assets"
"github.com/bishopfox/sliver/client/core"
"github.com/bishopfox/sliver/client/transport"
"github.com/bishopfox/sliver/protobuf/rpcpb"
"google.golang.org/grpc"
"google.golang.org/grpc/connectivity"
@@ -123,7 +124,7 @@ func (con *SliverClient) detachConnectionLocked() {
}
if con.grpcConn != nil {
_ = con.grpcConn.Close()
_ = transport.CloseGRPCConnection(con.grpcConn)
con.grpcConn = nil
}
con.Rpc = nil
+3 -1
View File
@@ -113,7 +113,9 @@ const (
CrackStatusEvent = "crack-status"
// WireGuardNewPeer - New Wireguard peer added.
WireGuardNewPeer = "wireguard-newpeer"
WireGuardNewPeer = "wireguard-newpeer"
MultiplayerWireGuardNewPeer = "multiplayer-wireguard-newpeer"
MultiplayerWireGuardRemoved = "multiplayer-wireguard-removed"
)
// Commands.
+1
View File
@@ -216,6 +216,7 @@ func (p *ChannelProxy) dialImplant(ctx context.Context) (*TunnelIO, error) {
Port: p.Port(),
Protocol: sliverpb.PortFwdProtoTCP,
TunnelID: tunnel.ID,
KeepAlive: int32(p.KeepAlivePeriod.Seconds()),
})
if err != nil {
return nil, err
+73
View File
@@ -0,0 +1,73 @@
package transport
import (
"errors"
"sync"
"google.golang.org/grpc"
)
// MultiplayerConnectMode controls how the client reaches multiplayer.
type MultiplayerConnectMode int
const (
MultiplayerConnectAuto MultiplayerConnectMode = iota
MultiplayerConnectDisableWG
MultiplayerConnectRequireWG
)
type connectionCloser interface {
Close() error
}
var (
multiplayerConnectModeMu sync.RWMutex
multiplayerConnectMode = MultiplayerConnectAuto
connClosers sync.Map // *grpc.ClientConn -> connectionCloser
)
// SetMultiplayerConnectMode configures future multiplayer client dials.
func SetMultiplayerConnectMode(mode MultiplayerConnectMode) {
multiplayerConnectModeMu.Lock()
multiplayerConnectMode = mode
multiplayerConnectModeMu.Unlock()
}
func getMultiplayerConnectMode() MultiplayerConnectMode {
multiplayerConnectModeMu.RLock()
mode := multiplayerConnectMode
multiplayerConnectModeMu.RUnlock()
return mode
}
func registerConnCloser(conn *grpc.ClientConn, closer connectionCloser) {
if conn == nil || closer == nil {
return
}
connClosers.Store(conn, closer)
}
func unregisterConnCloser(conn *grpc.ClientConn) connectionCloser {
if conn == nil {
return nil
}
if closer, ok := connClosers.LoadAndDelete(conn); ok {
return closer.(connectionCloser)
}
return nil
}
// CloseGRPCConnection closes a client connection and any transport-specific
// resources attached to it, such as the multiplayer WireGuard tunnel.
func CloseGRPCConnection(conn *grpc.ClientConn) error {
if conn == nil {
return nil
}
var errs []error
if closer := unregisterConnCloser(conn); closer != nil {
errs = append(errs, closer.Close())
}
errs = append(errs, conn.Close())
return errors.Join(errs...)
}
+37 -3
View File
@@ -22,6 +22,7 @@ import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"log"
"os"
@@ -63,10 +64,36 @@ func (TokenAuth) RequireTransportSecurity() bool {
// MTLSConnect - Connect to the sliver server
func MTLSConnect(config *assets.ClientConfig) (rpcpb.SliverRPCClient, *grpc.ClientConn, error) {
tlsConfig, err := getTLSConfig(config.CACertificate, config.Certificate, config.PrivateKey)
if config == nil {
return nil, nil, errors.New("client config is required")
}
switch getMultiplayerConnectMode() {
case MultiplayerConnectDisableWG:
return directMTLSConnect(config)
case MultiplayerConnectRequireWG:
return wireGuardMTLSConnect(config)
default:
if config.WG != nil {
return wireGuardMTLSConnect(config)
}
return directMTLSConnect(config)
}
}
func directMTLSConnect(config *assets.ClientConfig) (rpcpb.SliverRPCClient, *grpc.ClientConn, error) {
options, err := newMTLSDialOptions(config)
if err != nil {
return nil, nil, err
}
return dialRPCClient(fmt.Sprintf("%s:%d", config.LHost, config.LPort), options, nil)
}
func newMTLSDialOptions(config *assets.ClientConfig) ([]grpc.DialOption, error) {
tlsConfig, err := getTLSConfig(config.CACertificate, config.Certificate, config.PrivateKey)
if err != nil {
return nil, err
}
transportCreds := credentials.NewTLS(tlsConfig)
callCreds := credentials.PerRPCCredentials(TokenAuth{token: config.Token})
options := []grpc.DialOption{
@@ -78,11 +105,18 @@ func MTLSConnect(config *assets.ClientConfig) (rpcpb.SliverRPCClient, *grpc.Clie
if kp, ok := getKeepaliveParams(); ok {
options = append(options, grpc.WithKeepaliveParams(kp))
}
return options, nil
}
connection, err := grpc.NewClient(fmt.Sprintf("%s:%d", config.LHost, config.LPort), options...)
func dialRPCClient(target string, options []grpc.DialOption, closer connectionCloser) (rpcpb.SliverRPCClient, *grpc.ClientConn, error) {
connection, err := grpc.NewClient(target, options...)
if err != nil {
if closer != nil {
_ = closer.Close()
}
return nil, nil, err
}
registerConnCloser(connection, closer)
// Preserve the legacy grpc.DialContext + grpc.WithBlock behavior: block
// until the channel becomes READY or the timeout expires.
@@ -97,7 +131,7 @@ func MTLSConnect(config *assets.ClientConfig) (rpcpb.SliverRPCClient, *grpc.Clie
break
}
if !connection.WaitForStateChange(ctx, state) {
_ = connection.Close()
_ = CloseGRPCConnection(connection)
return nil, nil, ctx.Err()
}
}
+155
View File
@@ -0,0 +1,155 @@
package transport
import (
"bufio"
"bytes"
"context"
"errors"
"fmt"
"net"
"net/netip"
"strconv"
"strings"
"sync"
"github.com/bishopfox/sliver/client/assets"
"github.com/bishopfox/sliver/protobuf/rpcpb"
"golang.zx2c4.com/wireguard/conn"
"golang.zx2c4.com/wireguard/device"
"google.golang.org/grpc"
)
const (
multiplayerWireGuardDefaultServerIP = "100.64.0.1"
multiplayerWireGuardMTU = 1420
multiplayerWireGuardKeepalive = 25
)
type wireGuardTunnel struct {
dev *device.Device
net *transportNet
closeOnce sync.Once
}
func (t *wireGuardTunnel) Close() error {
if t == nil {
return nil
}
t.closeOnce.Do(func() {
if t.dev != nil {
t.dev.Close()
}
})
return nil
}
func (t *wireGuardTunnel) DialContext(ctx context.Context, address string) (net.Conn, error) {
if t == nil || t.net == nil {
return nil, errors.New("wireguard tunnel is not initialized")
}
return t.net.DialContext(ctx, "tcp", address)
}
func wireGuardMTLSConnect(config *assets.ClientConfig) (rpcpb.SliverRPCClient, *grpc.ClientConn, error) {
tunnel, target, err := newWireGuardTunnel(config)
if err != nil {
return nil, nil, err
}
options, err := newMTLSDialOptions(config)
if err != nil {
_ = tunnel.Close()
return nil, nil, err
}
options = append(options, grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) {
return tunnel.DialContext(ctx, addr)
}))
return dialRPCClient(target, options, tunnel)
}
func newWireGuardTunnel(config *assets.ClientConfig) (*wireGuardTunnel, string, error) {
if config == nil || config.WG == nil {
return nil, "", errors.New("wireguard transport requested but operator config has no wg block")
}
if config.LPort <= 0 || 65535 < config.LPort {
return nil, "", fmt.Errorf("invalid multiplayer port %d", config.LPort)
}
clientIP, err := netip.ParseAddr(strings.TrimSpace(config.WG.ClientIP))
if err != nil {
return nil, "", fmt.Errorf("invalid wireguard client IP %q: %w", config.WG.ClientIP, err)
}
serverIPValue := strings.TrimSpace(config.WG.ServerIP)
if serverIPValue == "" {
serverIPValue = multiplayerWireGuardDefaultServerIP
}
serverIP, err := netip.ParseAddr(serverIPValue)
if err != nil {
return nil, "", fmt.Errorf("invalid wireguard server IP %q: %w", serverIPValue, err)
}
endpoint, err := resolveWireGuardEndpoint(config.LHost, config.LPort)
if err != nil {
return nil, "", err
}
tun, tNet, err := createTransportNetTUN([]netip.Addr{clientIP}, multiplayerWireGuardMTU)
if err != nil {
return nil, "", err
}
dev := device.NewDevice(tun, conn.NewDefaultBind(), device.NewLogger(device.LogLevelSilent, "[client/wg] "))
wgConf := bytes.NewBuffer(nil)
fmt.Fprintf(wgConf, "private_key=%s\n", strings.TrimSpace(config.WG.ClientPrivateKey))
fmt.Fprintf(wgConf, "public_key=%s\n", strings.TrimSpace(config.WG.ServerPubKey))
fmt.Fprintf(wgConf, "endpoint=%s\n", endpoint.String())
fmt.Fprintf(wgConf, "allowed_ip=%s\n", multiplayerWireGuardAllowedIP(serverIP))
fmt.Fprintf(wgConf, "persistent_keepalive_interval=%d\n", multiplayerWireGuardKeepalive)
if err := dev.IpcSetOperation(bufio.NewReader(wgConf)); err != nil {
dev.Close()
return nil, "", err
}
if err := dev.Up(); err != nil {
dev.Close()
return nil, "", err
}
target := net.JoinHostPort(serverIP.String(), strconv.Itoa(config.LPort))
return &wireGuardTunnel{dev: dev, net: tNet}, target, nil
}
func resolveWireGuardEndpoint(host string, port int) (netip.AddrPort, error) {
host = strings.Trim(strings.TrimSpace(host), "[]")
if host == "" {
return netip.AddrPort{}, errors.New("wireguard endpoint host is required")
}
if addr, err := netip.ParseAddr(host); err == nil {
return netip.AddrPortFrom(addr, uint16(port)), nil
}
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
defer cancel()
ips, err := net.DefaultResolver.LookupNetIP(ctx, "ip", host)
if err != nil {
return netip.AddrPort{}, fmt.Errorf("failed to resolve wireguard endpoint %q: %w", host, err)
}
for _, ip := range ips {
if ip.IsValid() {
return netip.AddrPortFrom(ip, uint16(port)), nil
}
}
return netip.AddrPort{}, fmt.Errorf("wireguard endpoint %q did not resolve to a usable IP", host)
}
func multiplayerWireGuardAllowedIP(addr netip.Addr) string {
if addr.Is6() {
return fmt.Sprintf("%s/128", addr)
}
return fmt.Sprintf("%s/32", addr)
}
+226
View File
@@ -0,0 +1,226 @@
package transport
import (
"context"
"crypto/rand"
"fmt"
"math/big"
insecurerand "math/rand"
"net"
"net/netip"
"os"
"syscall"
"golang.zx2c4.com/wireguard/tun"
"gvisor.dev/gvisor/pkg/buffer"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
"gvisor.dev/gvisor/pkg/tcpip/header"
"gvisor.dev/gvisor/pkg/tcpip/link/channel"
"gvisor.dev/gvisor/pkg/tcpip/network/ipv4"
"gvisor.dev/gvisor/pkg/tcpip/network/ipv6"
"gvisor.dev/gvisor/pkg/tcpip/stack"
"gvisor.dev/gvisor/pkg/tcpip/transport/icmp"
"gvisor.dev/gvisor/pkg/tcpip/transport/tcp"
"gvisor.dev/gvisor/pkg/tcpip/transport/udp"
)
type transportTun struct {
ep *channel.Endpoint
stack *stack.Stack
events chan tun.Event
notifyHandle *channel.NotificationHandle
incomingPacket chan *buffer.View
mtu int
hasV4 bool
hasV6 bool
}
type transportNet transportTun
func createTransportNetTUN(localAddresses []netip.Addr, mtu int) (tun.Device, *transportNet, error) {
n, err := rand.Int(rand.Reader, big.NewInt(0xFFFFFFFF))
if err != nil {
return nil, nil, fmt.Errorf("failed to create random number: %w", err)
}
clock := tcpip.NewStdClock()
rng := insecurerand.New(insecurerand.NewSource(n.Int64()))
opts := stack.Options{
NetworkProtocols: []stack.NetworkProtocolFactory{ipv4.NewProtocol, ipv6.NewProtocol},
TransportProtocols: []stack.TransportProtocolFactory{tcp.NewProtocol, udp.NewProtocol, icmp.NewProtocol6, icmp.NewProtocol4},
HandleLocal: true,
IPTables: stack.DefaultTables(clock, rng),
}
dev := &transportTun{
ep: channel.New(1024, uint32(mtu), ""),
stack: stack.New(opts),
events: make(chan tun.Event, 10),
incomingPacket: make(chan *buffer.View),
mtu: mtu,
}
sackEnabledOpt := tcpip.TCPSACKEnabled(true)
if tcpipErr := dev.stack.SetTransportProtocolOption(tcp.ProtocolNumber, &sackEnabledOpt); tcpipErr != nil {
return nil, nil, fmt.Errorf("could not enable TCP SACK: %v", tcpipErr)
}
dev.notifyHandle = dev.ep.AddNotify(dev)
if tcpipErr := dev.stack.CreateNIC(1, dev.ep); tcpipErr != nil {
return nil, nil, fmt.Errorf("CreateNIC: %v", tcpipErr)
}
for _, ip := range localAddresses {
var protoNumber tcpip.NetworkProtocolNumber
if ip.Is4() {
protoNumber = ipv4.ProtocolNumber
} else if ip.Is6() {
protoNumber = ipv6.ProtocolNumber
}
protoAddr := tcpip.ProtocolAddress{
Protocol: protoNumber,
AddressWithPrefix: tcpip.AddrFromSlice(ip.AsSlice()).WithPrefix(),
}
if tcpipErr := dev.stack.AddProtocolAddress(1, protoAddr, stack.AddressProperties{}); tcpipErr != nil {
return nil, nil, fmt.Errorf("AddProtocolAddress(%v): %v", ip, tcpipErr)
}
if ip.Is4() {
dev.hasV4 = true
} else if ip.Is6() {
dev.hasV6 = true
}
}
if dev.hasV4 {
dev.stack.AddRoute(tcpip.Route{Destination: header.IPv4EmptySubnet, NIC: 1})
}
if dev.hasV6 {
dev.stack.AddRoute(tcpip.Route{Destination: header.IPv6EmptySubnet, NIC: 1})
}
dev.events <- tun.EventUp
return dev, (*transportNet)(dev), nil
}
func (tun *transportTun) Name() (string, error) {
return "go", nil
}
func (tun *transportTun) File() *os.File {
return nil
}
func (tun *transportTun) Events() <-chan tun.Event {
return tun.events
}
func (tun *transportTun) Read(buf [][]byte, sizes []int, offset int) (int, error) {
view, ok := <-tun.incomingPacket
if !ok {
return 0, os.ErrClosed
}
n, err := view.Read(buf[0][offset:])
if err != nil {
return 0, err
}
sizes[0] = n
return 1, nil
}
func (tun *transportTun) Write(buf [][]byte, offset int) (int, error) {
for _, buf := range buf {
packet := buf[offset:]
if len(packet) == 0 {
continue
}
pkb := stack.NewPacketBuffer(stack.PacketBufferOptions{Payload: buffer.MakeWithData(packet)})
switch packet[0] >> 4 {
case 4:
tun.ep.InjectInbound(header.IPv4ProtocolNumber, pkb)
case 6:
tun.ep.InjectInbound(header.IPv6ProtocolNumber, pkb)
default:
return 0, syscall.EAFNOSUPPORT
}
}
return len(buf), nil
}
func (tun *transportTun) WriteNotify() {
pkt := tun.ep.Read()
if pkt == nil {
return
}
view := pkt.ToView()
pkt.DecRef()
tun.incomingPacket <- view
}
func (tun *transportTun) Close() error {
tun.stack.RemoveNIC(1)
tun.stack.Close()
tun.ep.RemoveNotify(tun.notifyHandle)
tun.ep.Close()
if tun.events != nil {
close(tun.events)
}
if tun.incomingPacket != nil {
close(tun.incomingPacket)
}
return nil
}
func (tun *transportTun) MTU() (int, error) {
return tun.mtu, nil
}
func (tun *transportTun) BatchSize() int {
return 1
}
func convertTransportFullAddr(endpoint netip.AddrPort) (tcpip.FullAddress, tcpip.NetworkProtocolNumber) {
var protoNumber tcpip.NetworkProtocolNumber
if endpoint.Addr().Is4() {
protoNumber = ipv4.ProtocolNumber
} else {
protoNumber = ipv6.ProtocolNumber
}
return tcpip.FullAddress{
NIC: 1,
Addr: tcpip.AddrFromSlice(endpoint.Addr().AsSlice()),
Port: endpoint.Port(),
}, protoNumber
}
func (netstack *transportNet) DialContextTCPAddrPort(ctx context.Context, addr netip.AddrPort) (*gonet.TCPConn, error) {
fa, pn := convertTransportFullAddr(addr)
return gonet.DialContextTCP(ctx, netstack.stack, fa, pn)
}
func (netstack *transportNet) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
switch network {
case "tcp", "tcp4", "tcp6":
default:
return nil, fmt.Errorf("unsupported network %q", network)
}
host, portString, err := net.SplitHostPort(address)
if err != nil {
return nil, err
}
port, err := net.LookupPort("tcp", portString)
if err != nil {
return nil, err
}
addr, err := netip.ParseAddr(host)
if err != nil {
return nil, err
}
return netstack.DialContextTCPAddrPort(ctx, netip.AddrPortFrom(addr, uint16(port)))
}
+17 -3
View File
@@ -20,10 +20,15 @@ set -u
set -o pipefail
SKIP_GENERATE=0
UNIT_ONLY=0
for arg in "$@"; do
if [ "$arg" = "--skip-generate" ]; then
SKIP_GENERATE=1
fi
if [ "$arg" = "--unit-only" ]; then
UNIT_ONLY=1
SKIP_GENERATE=1
fi
done
echo "----------------------------------------------------------------"
@@ -219,8 +224,13 @@ done
## Server c2 (unit + e2e)
run_test_cmd "./server/c2" go test -tags="server,$TAGS" ./server/c2 || exit 1
run_test_cmd "./server/c2 (e2e yamux)" go test -tags="server,$TAGS,sliver_e2e" ./server/c2 -run 'Test(MTLS|WG)Yamux_' -count=1 || exit 1
run_test_cmd "./server/c2 (e2e dns)" go test -tags="server,$TAGS,sliver_e2e" ./server/c2 -run 'TestDNS_' -count=1 || exit 1
if [ "$UNIT_ONLY" -eq 1 ]; then
echo
echo "Skipping ./server/c2 e2e tests (--unit-only)"
else
run_test_cmd "./server/c2 (e2e yamux)" go test -tags="server,$TAGS,sliver_e2e" ./server/c2 -run 'Test(MTLS|WG)Yamux_' -count=1 || exit 1
run_test_cmd "./server/c2 (e2e dns)" go test -tags="server,$TAGS,sliver_e2e" ./server/c2 -run 'TestDNS_' -count=1 || exit 1
fi
## Server generate
if [ "$SKIP_GENERATE" -eq 0 ]; then
@@ -235,5 +245,9 @@ if [ "$SKIP_GENERATE" -eq 0 ]; then
go test -timeout 6h -p "$GENERATE_GO_P" -parallel "$GENERATE_TEST_PARALLEL" -tags="server,$TAGS" ./server/generate || exit 1
else
echo
echo "Skipping ./server/generate tests (--skip-generate)"
if [ "$UNIT_ONLY" -eq 1 ]; then
echo "Skipping ./server/generate tests (--unit-only)"
else
echo "Skipping ./server/generate tests (--skip-generate)"
fi
fi
+1 -2
View File
@@ -211,7 +211,6 @@ require (
)
require (
charm.land/bubbles/v2 v2.0.0
charm.land/bubbletea/v2 v2.0.0
cloud.google.com/go/compute/metadata v0.9.0 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
@@ -231,7 +230,7 @@ require (
github.com/carapace-sh/carapace v1.11.0
github.com/carapace-sh/carapace-shlex v1.1.1 // indirect
github.com/catppuccin/go v0.3.0 // indirect
github.com/charmbracelet/colorprofile v0.4.2 // indirect
github.com/charmbracelet/colorprofile v0.4.2
github.com/charmbracelet/x/ansi v0.11.6
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
-4
View File
@@ -4,8 +4,6 @@ c2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd h1:ZLsPO6WdZ5zatV4UfVpr7oAw
c2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd/go.mod h1:SrHC2C7r5GkDk8R+NFVzYy/sdj0Ypg9htaPXQq5Cqeo=
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s=
charm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI=
charm.land/bubbletea/v2 v2.0.0 h1:p0d6CtWyJXJ9GfzMpUUqbP/XUUhhlk06+vCKWmox1wQ=
charm.land/bubbletea/v2 v2.0.0/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ=
charm.land/lipgloss/v2 v2.0.0 h1:sd8N/B3x892oiOjFfBQdXBQp3cAkvjGaU5TvVZC3ivo=
@@ -194,8 +192,6 @@ github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 h1:eyFRb
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8/go.mod h1:SQpCTRNBtzJkwku5ye4S3HEuthAlGy2n9VXZnWkEW98=
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U=
+10 -1
View File
@@ -97,11 +97,20 @@ func rportFwdStartListenerHandler(envelope *pb.Envelope, connection *transports.
}
}
tcpProxy := &tcpproxy.Proxy{}
var keepAlivePeriod time.Duration
if req.KeepAlive > 0 {
keepAlivePeriod = time.Duration(req.KeepAlive) * time.Second
} else if req.KeepAlive < 0 {
keepAlivePeriod = -1 * time.Second
} else {
keepAlivePeriod = 30 * time.Second
}
channelProxy := &rportfwd.ChannelProxy{
Conn: connection,
RemoteAddr: req.ForwardAddress,
BindAddr: req.BindAddress,
KeepAlivePeriod: 1000 * time.Second,
KeepAlivePeriod: keepAlivePeriod,
DialTimeout: 30 * time.Second,
}
tcpProxy.AddRoute(req.BindAddress, channelProxy)
@@ -81,9 +81,16 @@ func PortfwdReqHandler(envelope *sliverpb.Envelope, connection *transports.Conne
// {{if .Config.Debug}}
log.Printf("[portfwd] Configuring keep alive")
// {{end}}
conn.SetKeepAlive(true)
// TODO: Make KeepAlive configurable
conn.SetKeepAlivePeriod(30 * time.Second)
if portfwdReq.KeepAlive < 0 {
conn.SetKeepAlive(false)
} else {
conn.SetKeepAlive(true)
if portfwdReq.KeepAlive > 0 {
conn.SetKeepAlivePeriod(time.Duration(portfwdReq.KeepAlive) * time.Second)
} else {
conn.SetKeepAlivePeriod(30 * time.Second)
}
}
}
// Add tunnel
+10 -3
View File
@@ -140,9 +140,16 @@ func (p *ChannelProxy) HandleConn(src net.Conn) {
// {{if .Config.Debug}}
log.Printf("[portfwd] Configuring keep alive")
// {{end}}
conn.SetKeepAlive(true)
// TODO: Make KeepAlive configurable
conn.SetKeepAlivePeriod(30 * time.Second)
if p.KeepAlivePeriod < 0 {
conn.SetKeepAlive(false)
} else {
conn.SetKeepAlive(true)
if p.KeepAlivePeriod > 0 {
conn.SetKeepAlivePeriod(p.KeepAlivePeriod)
} else {
conn.SetKeepAlivePeriod(30 * time.Second)
}
}
}
// Add tunnel
// {{if .Config.Debug}}
@@ -58,6 +58,7 @@ func (c *Client) Do(request *http.Request) (*http.Response, error) {
var buf []byte
var err error
var reqHandle uintptr
var connHandle uintptr
var resp *Response
var rawBody []byte
@@ -81,9 +82,17 @@ func (c *Client) Do(request *http.Request) (*http.Response, error) {
Body: rawBody,
}
if reqHandle, err = buildRequest(c.handle, req); err != nil {
if connHandle, reqHandle, err = buildRequest(c.handle, req); err != nil {
if reqHandle != 0 {
InternetCloseHandle(reqHandle)
}
if connHandle != 0 {
InternetCloseHandle(connHandle)
}
return nil, err
}
defer InternetCloseHandle(reqHandle)
defer InternetCloseHandle(connHandle)
if c.Timeout > 0 {
buf = make([]byte, 4)
@@ -232,3 +241,11 @@ func (jar *Jar) SetCookies(u *url.URL, cookies []*Cookie) {
func (jar *Jar) Cookies(u *url.URL) []*Cookie {
return jar.cookies
}
// Close will close the client handle
func (c *Client) Close() {
if c.handle != 0 {
InternetCloseHandle(c.handle)
c.handle = 0
}
}
@@ -17,7 +17,7 @@ func convertFail(str string, err error) error {
)
}
func buildRequest(sessionHndl uintptr, r *Request) (uintptr, error) {
func buildRequest(sessionHndl uintptr, r *Request) (uintptr, uintptr, error) {
var connHndl uintptr
var err error
var flags uintptr
@@ -29,7 +29,7 @@ func buildRequest(sessionHndl uintptr, r *Request) (uintptr, error) {
// Parse URL
if uri, err = url.Parse(r.URL); err != nil {
return 0, fmt.Errorf("failed to parse url %s: %w", r.URL, err)
return 0, 0, fmt.Errorf("failed to parse url %s: %w", r.URL, err)
}
passwd, _ = uri.User.Password()
@@ -37,7 +37,7 @@ func buildRequest(sessionHndl uintptr, r *Request) (uintptr, error) {
if uri.Port() != "" {
if port, err = strconv.ParseInt(uri.Port(), 10, 64); err != nil {
err = fmt.Errorf("port %s invalid: %w", uri.Port(), err)
return 0, err
return 0, 0, err
}
}
@@ -58,7 +58,7 @@ func buildRequest(sessionHndl uintptr, r *Request) (uintptr, error) {
0,
)
if err != nil {
return 0, fmt.Errorf("failed to create connection: %w", err)
return 0, 0, fmt.Errorf("failed to create connection: %w", err)
}
// Send query string too
@@ -82,10 +82,10 @@ func buildRequest(sessionHndl uintptr, r *Request) (uintptr, error) {
0,
)
if err != nil {
return 0, fmt.Errorf("failed to open request: %w", err)
return connHndl, 0, fmt.Errorf("failed to open request: %w", err)
}
return reqHndl, nil
return connHndl, reqHndl, nil
}
var cookies []*Cookie
@@ -213,6 +213,20 @@ func HTTPSendRequestW(
return nil
}
// InternetCloseHandle is from wininet.h
func InternetCloseHandle(hndl uintptr) error {
var err error
var proc string = "InternetCloseHandle"
var success uintptr
success, _, err = wininet.NewProc(proc).Call(hndl)
if success == 0 {
return fmt.Errorf("%s: %w", proc, err)
}
return nil
}
// InternetConnectW is from wininet.h
func InternetConnectW(
sessionHndl uintptr,
@@ -437,7 +451,7 @@ func InternetErrorDlg(
)
// we expect 12032 if user hits okay, otherwise we return the response code as an error
if uint32(success) != ERROR_INTERNET_FORCE_RETRY {
return success, fmt.Errorf("%s: %s", proc, success)
return success, fmt.Errorf("%s: %v", proc, success)
}
*lppvData = buf
File diff suppressed because it is too large. Load diff
+1
View File
@@ -456,6 +456,7 @@ message ListenerJob {
message MultiplayerListenerReq {
string Host = 1;
uint32 Port = 2;
bool WireGuard = 3;
}
message MTLSListenerReq {
File diff suppressed because it is too large. Load diff
+2
View File
@@ -883,6 +883,7 @@ message PortfwdReq {
int32 Protocol = 2;
string Host = 3;
int32 KeepAlive = 4;
uint64 TunnelID = 8 [jstype = JS_STRING]; // Bind to this tunnel
commonpb.Request Request = 9;
}
@@ -1185,6 +1186,7 @@ message RportFwdStartListenerReq {
uint32 BindPort = 2;
uint32 ForwardPort = 3;
string ForwardAddress = 4;
int32 KeepAlive = 5;
commonpb.Request Request = 9;
}
+69
View File
@@ -0,0 +1,69 @@
package c2
import (
"io"
"os"
)
const (
// socketEnvelopeDiskSpoolThreshold is the point where envelope payloads are
// streamed to disk first to avoid large attacker-triggered allocations.
socketEnvelopeDiskSpoolThreshold = 64 * 1024 * 1024
// socketEnvelopeReadChunkSize bounds peak in-memory usage while reading large
// envelopes from the network.
socketEnvelopeReadChunkSize = 1 * 1024 * 1024
)
func readSocketEnvelopeData(reader io.Reader, dataLength int, inMemoryLimit int) ([]byte, error) {
if dataLength <= inMemoryLimit {
dataBuf := make([]byte, dataLength)
if _, err := io.ReadFull(reader, dataBuf); err != nil {
return nil, err
}
return dataBuf, nil
}
tmpFile, err := os.CreateTemp("", "sliver-envelope-*")
if err != nil {
return nil, err
}
tmpPath := tmpFile.Name()
defer func() {
_ = tmpFile.Close()
_ = os.Remove(tmpPath)
}()
chunkSize := socketEnvelopeReadChunkSize
if dataLength < chunkSize {
chunkSize = dataLength
}
chunkBuf := make([]byte, chunkSize)
remaining := dataLength
for remaining > 0 {
nextRead := chunkSize
if remaining < nextRead {
nextRead = remaining
}
if _, err := io.ReadFull(reader, chunkBuf[:nextRead]); err != nil {
return nil, err
}
if _, err := tmpFile.Write(chunkBuf[:nextRead]); err != nil {
return nil, err
}
remaining -= nextRead
}
if _, err := tmpFile.Seek(0, io.SeekStart); err != nil {
return nil, err
}
dataBuf := make([]byte, dataLength)
if _, err := io.ReadFull(tmpFile, dataBuf); err != nil {
return nil, err
}
return dataBuf, nil
}
+135
View File
@@ -0,0 +1,135 @@
package c2
import (
"bytes"
"encoding/binary"
"errors"
"io"
"math"
"net"
"runtime"
"testing"
"github.com/bishopfox/sliver/protobuf/sliverpb"
"github.com/bishopfox/sliver/util/minisign"
)
type readSizeRecorder struct {
reader io.Reader
maxRead int
}
func (r *readSizeRecorder) Read(p []byte) (int, error) {
if len(p) > r.maxRead {
r.maxRead = len(p)
}
return r.reader.Read(p)
}
func envelopeFrameWithDeclaredLength(dataLength uint32, payload []byte) []byte {
frame := make([]byte, minisign.RawSigSize+4+len(payload))
binary.LittleEndian.PutUint16(frame[:2], minisign.EdDSA)
binary.LittleEndian.PutUint32(frame[minisign.RawSigSize:minisign.RawSigSize+4], dataLength)
copy(frame[minisign.RawSigSize+4:], payload)
return frame
}
func measureEnvelopeReadAlloc(t *testing.T, readFunc func(net.Conn) (*sliverpb.Envelope, error), frame []byte) (uint64, error) {
t.Helper()
srv, cli := net.Pipe()
defer srv.Close()
defer cli.Close()
errCh := make(chan error, 1)
go func() {
_, err := readFunc(srv)
errCh <- err
}()
runtime.GC()
var before runtime.MemStats
runtime.ReadMemStats(&before)
go func() {
_, _ = cli.Write(frame)
_ = cli.Close()
}()
err := <-errCh
runtime.GC()
var after runtime.MemStats
runtime.ReadMemStats(&after)
if after.TotalAlloc < before.TotalAlloc {
return 0, err
}
return after.TotalAlloc - before.TotalAlloc, err
}
func assertLargeTruncatedFrameHasBoundedAlloc(t *testing.T, readFunc func(net.Conn) (*sliverpb.Envelope, error)) {
t.Helper()
declaredLength := uint32(socketEnvelopeDiskSpoolThreshold + (8 * 1024 * 1024))
frame := envelopeFrameWithDeclaredLength(declaredLength, []byte{0x01})
minAllocDelta := uint64(math.MaxUint64)
for i := 0; i < 3; i++ {
allocDelta, err := measureEnvelopeReadAlloc(t, readFunc, frame)
if err == nil {
t.Fatalf("expected read failure for truncated frame")
}
if !errors.Is(err, io.EOF) && !errors.Is(err, io.ErrUnexpectedEOF) {
t.Fatalf("expected EOF-like error for truncated frame, got %v", err)
}
if allocDelta < minAllocDelta {
minAllocDelta = allocDelta
}
}
const maxAllowedAlloc = 24 * 1024 * 1024
if minAllocDelta > maxAllowedAlloc {
t.Fatalf("expected bounded allocation <= %d bytes, got %d", maxAllowedAlloc, minAllocDelta)
}
}
func TestReadSocketEnvelopeDataKeepsSmallPayloadsInMemory(t *testing.T) {
payload := bytes.Repeat([]byte{0x7a}, 4096)
recorder := &readSizeRecorder{reader: bytes.NewReader(payload)}
got, err := readSocketEnvelopeData(recorder, len(payload), 8192)
if err != nil {
t.Fatalf("readSocketEnvelopeData returned err: %v", err)
}
if !bytes.Equal(got, payload) {
t.Fatalf("unexpected payload contents")
}
if recorder.maxRead != len(payload) {
t.Fatalf("expected in-memory read size %d, got %d", len(payload), recorder.maxRead)
}
}
func TestReadSocketEnvelopeDataSpoolsLargePayloadsInChunks(t *testing.T) {
payload := bytes.Repeat([]byte{0x42}, (2*socketEnvelopeReadChunkSize)+123)
recorder := &readSizeRecorder{reader: bytes.NewReader(payload)}
got, err := readSocketEnvelopeData(recorder, len(payload), socketEnvelopeReadChunkSize/2)
if err != nil {
t.Fatalf("readSocketEnvelopeData returned err: %v", err)
}
if !bytes.Equal(got, payload) {
t.Fatalf("unexpected payload contents")
}
if recorder.maxRead > socketEnvelopeReadChunkSize {
t.Fatalf("expected max read chunk <= %d, got %d", socketEnvelopeReadChunkSize, recorder.maxRead)
}
}
func TestMTLSSocketReadEnvelopeLargeTruncatedMessageBoundsAllocation(t *testing.T) {
assertLargeTruncatedFrameHasBoundedAlloc(t, socketReadEnvelope)
}
func TestWGSocketReadEnvelopeLargeTruncatedMessageBoundsAllocation(t *testing.T) {
assertLargeTruncatedFrameHasBoundedAlloc(t, socketWGReadEnvelope)
}
+2 -4
View File
@@ -358,10 +358,8 @@ func socketReadEnvelope(connection net.Conn) (*sliverpb.Envelope, error) {
return nil, errors.New("[pivot] invalid data length")
}
dataBuf := make([]byte, dataLength)
n, err = io.ReadFull(connection, dataBuf)
if err != nil || n != dataLength {
dataBuf, err := readSocketEnvelopeData(connection, dataLength, socketEnvelopeDiskSpoolThreshold)
if err != nil {
mtlsLog.Errorf("Socket error (read data): %v", err)
return nil, err
}
+2 -5
View File
@@ -452,11 +452,8 @@ func socketWGReadEnvelope(connection net.Conn) (*sliverpb.Envelope, error) {
return nil, errors.New("[wireguard] zero data length")
}
dataBuf := make([]byte, dataLength)
n, err = io.ReadFull(connection, dataBuf)
if err != nil || n != dataLength {
dataBuf, err := readSocketEnvelopeData(connection, dataLength, socketEnvelopeDiskSpoolThreshold)
if err != nil {
wgLog.Errorf("Socket error (read data): %v", err)
return nil, err
}
+58
View File
@@ -21,6 +21,7 @@ package certs
import (
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"github.com/bishopfox/sliver/server/db"
@@ -35,6 +36,17 @@ const (
serverNamespace = "server" // Operator servers
)
var (
// ErrOperatorClientCertificateNotFound indicates that the presented operator
// client certificate is no longer trusted because it is not present in the
// certificate store.
ErrOperatorClientCertificateNotFound = errors.New("operator client certificate not found in database")
// ErrInvalidOperatorClientCertificate indicates that the presented
// certificate is not shaped like an operator client leaf certificate.
ErrInvalidOperatorClientCertificate = errors.New("invalid operator client certificate")
)
// OperatorClientGenerateCertificate - Generate a certificate signed with a given CA
func OperatorClientGenerateCertificate(operator string) ([]byte, []byte, error) {
cert, key := GenerateECCCertificate(OperatorCA, operator, false, true, true)
@@ -52,6 +64,52 @@ func OperatorClientRemoveCertificate(operator string) error {
return RemoveCertificate(OperatorCA, ECCKey, fmt.Sprintf("%s.%s", clientNamespace, operator))
}
// ValidateOperatorClientCertificate ensures that the presented operator client
// certificate is still present in the database. A valid chain alone is not
// enough; the exact leaf certificate must still exist in storage.
func ValidateOperatorClientCertificate(peerCertificates []*x509.Certificate) error {
if len(peerCertificates) == 0 || peerCertificates[0] == nil {
return ErrInvalidOperatorClientCertificate
}
leaf := peerCertificates[0]
if leaf.IsCA || leaf.Subject.CommonName == "" || !hasExtKeyUsage(leaf, x509.ExtKeyUsageClientAuth) {
return ErrInvalidOperatorClientCertificate
}
pemBytes := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: leaf.Raw,
})
if len(pemBytes) == 0 {
return ErrInvalidOperatorClientCertificate
}
record := &models.Certificate{}
result := db.Session().Select("id").Where(&models.Certificate{
CommonName: fmt.Sprintf("%s.%s", clientNamespace, leaf.Subject.CommonName),
CAType: OperatorCA,
KeyType: ECCKey,
CertificatePEM: string(pemBytes),
}).First(record)
if result.Error == nil {
return nil
}
if errors.Is(result.Error, db.ErrRecordNotFound) {
return ErrOperatorClientCertificateNotFound
}
return result.Error
}
func hasExtKeyUsage(cert *x509.Certificate, usage x509.ExtKeyUsage) bool {
for _, extKeyUsage := range cert.ExtKeyUsage {
if extKeyUsage == usage {
return true
}
}
return false
}
// OperatorServerGetCertificate - Helper function to fetch a server cert
func OperatorServerGetCertificate(hostname string) ([]byte, []byte, error) {
return GetECCCertificate(OperatorCA, fmt.Sprintf("%s.%s", serverNamespace, hostname))
+117
View File
@@ -0,0 +1,117 @@
package certs
import (
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"testing"
"time"
"github.com/bishopfox/sliver/server/db"
"github.com/bishopfox/sliver/server/db/models"
)
func TestValidateOperatorClientCertificateAdversarial(t *testing.T) {
SetupCAs()
t.Run("accepts stored operator client leaf", func(t *testing.T) {
operatorName := uniqueOperatorCertificateName(t, "stored")
certPEM, _, err := OperatorClientGenerateCertificate(operatorName)
if err != nil {
t.Fatalf("generate operator client certificate: %v", err)
}
err = ValidateOperatorClientCertificate([]*x509.Certificate{mustParseCertificate(t, certPEM)})
if err != nil {
t.Fatalf("expected stored operator client certificate to be accepted, got %v", err)
}
})
t.Run("rejects valid stored certificate hidden behind an unstored first certificate", func(t *testing.T) {
operatorName := uniqueOperatorCertificateName(t, "hidden")
storedCertPEM, _, err := OperatorClientGenerateCertificate(operatorName)
if err != nil {
t.Fatalf("generate stored operator certificate: %v", err)
}
rogueCertPEM, _ := GenerateECCCertificate(OperatorCA, operatorName, false, true, true)
err = ValidateOperatorClientCertificate([]*x509.Certificate{
mustParseCertificate(t, rogueCertPEM),
mustParseCertificate(t, storedCertPEM),
})
if !errors.Is(err, ErrOperatorClientCertificateNotFound) {
t.Fatalf("expected first unstored certificate to be rejected, got %v", err)
}
})
t.Run("rejects same-common-name different-leaf certificate", func(t *testing.T) {
operatorName := uniqueOperatorCertificateName(t, "same-cn")
_, _, err := OperatorClientGenerateCertificate(operatorName)
if err != nil {
t.Fatalf("generate stored operator certificate: %v", err)
}
rogueCertPEM, _ := GenerateECCCertificate(OperatorCA, operatorName, false, true, true)
err = ValidateOperatorClientCertificate([]*x509.Certificate{mustParseCertificate(t, rogueCertPEM)})
if !errors.Is(err, ErrOperatorClientCertificateNotFound) {
t.Fatalf("expected rotated or forged same-CN leaf to be rejected, got %v", err)
}
})
t.Run("rejects stored operator server certificate", func(t *testing.T) {
hostName := uniqueOperatorCertificateName(t, "server")
serverCertPEM, _, err := OperatorServerGenerateCertificate(hostName)
if err != nil {
t.Fatalf("generate operator server certificate: %v", err)
}
err = ValidateOperatorClientCertificate([]*x509.Certificate{mustParseCertificate(t, serverCertPEM)})
if !errors.Is(err, ErrInvalidOperatorClientCertificate) {
t.Fatalf("expected stored operator server certificate to be rejected, got %v", err)
}
})
t.Run("rejects operator CA certificate even if inserted into the certificates table", func(t *testing.T) {
caCertPEM, caKeyPEM, err := GetCertificateAuthorityPEM(OperatorCA)
if err != nil {
t.Fatalf("get operator CA: %v", err)
}
caCert := mustParseCertificate(t, caCertPEM)
record := &models.Certificate{
CommonName: fmt.Sprintf("%s.%s", clientNamespace, caCert.Subject.CommonName),
CAType: OperatorCA,
KeyType: ECCKey,
CertificatePEM: string(caCertPEM),
PrivateKeyPEM: string(caKeyPEM),
}
if err := db.Session().Create(record).Error; err != nil {
t.Fatalf("insert CA cert into certificates table: %v", err)
}
err = ValidateOperatorClientCertificate([]*x509.Certificate{caCert})
if !errors.Is(err, ErrInvalidOperatorClientCertificate) {
t.Fatalf("expected CA certificate to be rejected, got %v", err)
}
})
}
func mustParseCertificate(t *testing.T, certPEM []byte) *x509.Certificate {
t.Helper()
block, _ := pem.Decode(certPEM)
if block == nil {
t.Fatal("failed to decode certificate PEM")
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
t.Fatalf("parse x509 certificate: %v", err)
}
return cert
}
func uniqueOperatorCertificateName(t *testing.T, prefix string) string {
t.Helper()
return fmt.Sprintf("%s-%d", prefix, time.Now().UnixNano())
}
+24
View File
@@ -64,6 +64,11 @@ func ImplantGenerateWGKeys(wgPeerTunIP string) (string, string, error) {
return privKey, pubKey, nil
}
// GenerateWGKeyPair - Generate a WireGuard keypair without persisting it.
func GenerateWGKeyPair() (string, string, error) {
return genWGKeys()
}
// GetWGSPeers - Get a map of Pubkey:TunIP for existing wg peers
func GetWGPeers() (map[string]string, error) {
@@ -84,6 +89,25 @@ func GetWGPeers() (map[string]string, error) {
return peers, nil
}
// GetOperatorWGPeers - Get a map of operator WG public keys to tunnel IPs.
func GetOperatorWGPeers() (map[string]string, error) {
peers := make(map[string]string)
operators := []models.Operator{}
dbSession := db.Session()
err := dbSession.Where("wg_pub_key <> '' AND wg_tun_ip <> ''").Find(&operators).Error
if errors.Is(err, db.ErrRecordNotFound) {
return nil, ErrWGPeerDoesNotExist
} else if err != nil {
return nil, err
}
for _, operator := range operators {
peers[operator.WGPubKey] = operator.WGTunIP
}
return peers, nil
}
// GetWGServerKeys - Get existing wg server keys
func GetWGServerKeys() (string, string, error) {
+4 -1
View File
@@ -52,6 +52,7 @@ const (
outputFlagStr = "output"
permissionsFlagStr = "permissions"
tailscaleFlagStr = "tailscale"
disableWGFlagStr = "disable-wg"
// Cert flags
caTypeFlagStr = "type"
@@ -86,6 +87,7 @@ func init() {
operatorCmd.Flags().StringP(saveFlagStr, "s", "", "save file to ...")
operatorCmd.Flags().StringP(outputFlagStr, "o", "file", "output format (file, stdout)")
operatorCmd.Flags().StringSliceP(permissionsFlagStr, "P", []string{}, "grant permissions to the operator profile (all, builder, crackstation)")
operatorCmd.Flags().Bool(disableWGFlagStr, false, "omit WireGuard tunnel settings from the generated operator config")
rootCmd.AddCommand(operatorCmd)
// Certs
@@ -102,6 +104,7 @@ func init() {
daemonCmd.Flags().Uint16P(lportFlagStr, "p", daemon.BlankPort, "multiplayer listener port")
daemonCmd.Flags().BoolP(forceFlagStr, "f", false, "force unpack and overwrite static assets")
daemonCmd.Flags().BoolP(tailscaleFlagStr, "t", false, "enable tailscale")
daemonCmd.Flags().Bool(disableWGFlagStr, false, "expose multiplayer directly instead of wrapping it in WireGuard")
rootCmd.AddCommand(daemonCmd)
// Builder
@@ -149,7 +152,7 @@ var rootCmd = &cobra.Command{
fmt.Printf("[!] %s\n", err)
}
if serverConfig.DaemonMode {
daemon.Start(daemon.BlankHost, daemon.BlankPort, serverConfig.DaemonConfig.Tailscale)
daemon.Start(daemon.BlankHost, daemon.BlankPort, serverConfig.DaemonConfig.Tailscale, serverConfig.DaemonConfig.DisableWG)
} else {
rcScript, err := clientcli.ReadRCScript(cmd)
if err != nil {
+14 -2
View File
@@ -47,6 +47,14 @@ var daemonCmd = &cobra.Command{
fmt.Printf("Failed to parse --%s flag %s\n", tailscaleFlagStr, err)
return
}
disableWG, err := cmd.Flags().GetBool(disableWGFlagStr)
if err != nil {
fmt.Printf("Failed to parse --%s flag %s\n", disableWGFlagStr, err)
return
}
if !cmd.Flags().Changed(disableWGFlagStr) {
disableWG = configs.GetServerConfig().DaemonConfig.DisableWG
}
appDir := assets.GetRootAppDir()
logFile := initConsoleLogging(appDir)
@@ -78,7 +86,7 @@ var daemonCmd = &cobra.Command{
fmt.Printf("[!] %s\n", err)
}
daemon.Start(lhost, uint16(lport), tailscale)
daemon.Start(lhost, uint16(lport), tailscale, disableWG)
},
}
@@ -212,7 +220,11 @@ func persistentJobStartupContext(jobType string, savedJobID uint32, listenerJob
if listenerJob.MultiConf == nil {
return prefix
}
return fmt.Sprintf("%s [%s]", prefix, listenerBind(listenerJob.MultiConf.Host, listenerJob.MultiConf.Port))
mode := "direct"
if listenerJob.MultiConf.WireGuard {
mode = "wireguard"
}
return fmt.Sprintf("%s [%s mode=%s]", prefix, listenerBind(listenerJob.MultiConf.Host, listenerJob.MultiConf.Port), mode)
case constants.TCPListenerStr:
if listenerJob.TCPConf == nil {
return prefix
+11 -1
View File
@@ -99,7 +99,17 @@ var operatorCmd = &cobra.Command{
}
certs.SetupCAs()
configJSON, err := console.NewOperatorConfig(name, lhost, lport, permissions)
includeWG := console.DefaultOperatorWireGuardEnabled()
if cmd.Flags().Changed(disableWGFlagStr) {
disableWG, err := cmd.Flags().GetBool(disableWGFlagStr)
if err != nil {
fmt.Printf("Failed to parse --%s flag %s", disableWGFlagStr, err)
return
}
includeWG = !disableWG
}
configJSON, err := console.NewOperatorConfig(name, lhost, lport, permissions, includeWG)
if err != nil {
fmt.Printf("Failed: %s\n", err)
return
+2
View File
@@ -76,6 +76,7 @@ type DaemonConfig struct {
Host string `json:"host" yaml:"host"`
Port int `json:"port" yaml:"port"`
Tailscale bool `json:"tailscale" yaml:"tailscale"`
DisableWG bool `json:"disable_wg" yaml:"disable_wg"`
}
// JobConfig - Restart Jobs on Load
@@ -92,6 +93,7 @@ type MultiplayerJobConfig struct {
Port uint16 `json:"port" yaml:"port"`
JobID string `json:"job_id" yaml:"job_id"`
Tailscale bool `json:"tailscale" yaml:"tailscale"`
WireGuard bool `json:"wire_guard" yaml:"wire_guard"`
}
// MTLSJobConfig - Per-type job configs
+193 -26
View File
@@ -25,6 +25,7 @@ import (
"errors"
"fmt"
"log"
"net"
"os"
"path/filepath"
"regexp"
@@ -36,10 +37,12 @@ import (
consts "github.com/bishopfox/sliver/client/constants"
"github.com/bishopfox/sliver/protobuf/clientpb"
"github.com/bishopfox/sliver/server/certs"
"github.com/bishopfox/sliver/server/configs"
"github.com/bishopfox/sliver/server/console/forms"
"github.com/bishopfox/sliver/server/core"
"github.com/bishopfox/sliver/server/db"
"github.com/bishopfox/sliver/server/db/models"
"github.com/bishopfox/sliver/server/generate"
"github.com/bishopfox/sliver/server/transport"
)
@@ -72,15 +75,27 @@ const (
var namePattern = regexp.MustCompile("^[a-zA-Z0-9_-]*$") // Only allow alphanumeric chars
const multiplayerWireGuardServerIP = "100.64.0.1"
// ClientWGConfig - Optional WireGuard wrapper configuration.
type ClientWGConfig struct {
ServerPubKey string `json:"server_pub_key"`
ClientPrivateKey string `json:"client_private_key"`
ClientPubKey string `json:"client_pub_key"`
ClientIP string `json:"client_ip"`
ServerIP string `json:"server_ip"`
}
// ClientConfig - Client JSON config
type ClientConfig struct {
Operator string `json:"operator"`
Token string `json:"token"`
LHost string `json:"lhost"`
LPort int `json:"lport"`
CACertificate string `json:"ca_certificate"`
PrivateKey string `json:"private_key"`
Certificate string `json:"certificate"`
Operator string `json:"operator"`
Token string `json:"token"`
LHost string `json:"lhost"`
LPort int `json:"lport"`
CACertificate string `json:"ca_certificate"`
PrivateKey string `json:"private_key"`
Certificate string `json:"certificate"`
WG *ClientWGConfig `json:"wg,omitempty"`
}
func newOperatorCmd(cmd *cobra.Command, _ []string) {
@@ -89,13 +104,18 @@ func newOperatorCmd(cmd *cobra.Command, _ []string) {
lport, _ := cmd.Flags().GetUint16("lport")
save, _ := cmd.Flags().GetString("save")
permissions, _ := cmd.Flags().GetStringSlice("permissions")
includeWG := DefaultOperatorWireGuardEnabled()
if cmd.Flags().Changed("disable-wg") {
disableWG, _ := cmd.Flags().GetBool("disable-wg")
includeWG = !disableWG
}
if save == "" {
save, _ = os.Getwd()
}
fmt.Printf(Info + "Generating new client certificate, please wait ... \n")
configJSON, err := NewOperatorConfig(name, lhost, lport, permissions)
configJSON, err := NewOperatorConfig(name, lhost, lport, permissions, includeWG)
if err != nil {
fmt.Printf(Warn+"%s\n", err)
return
@@ -119,8 +139,36 @@ func newOperatorCmd(cmd *cobra.Command, _ []string) {
fmt.Printf(Info+"Saved new client config to: %s \n", saveTo)
}
// DefaultOperatorWireGuardEnabled returns the default wireguard mode for new
// operator configs based on the current server multiplayer exposure.
func DefaultOperatorWireGuardEnabled() bool {
serverConfig := configs.GetServerConfig()
if serverConfig != nil && serverConfig.DaemonMode && serverConfig.DaemonConfig != nil {
if serverConfig.DaemonConfig.Tailscale {
return false
}
return !serverConfig.DaemonConfig.DisableWG
}
listenerJobs, err := db.ListenerJobs()
if err != nil {
return true
}
for _, listenerJob := range listenerJobs {
if listenerJob == nil || listenerJob.Type != consts.MultiplayerModeStr {
continue
}
config, err := db.ListenerByJobID(listenerJob.JobID)
if err != nil || config == nil || config.MultiConf == nil {
continue
}
return config.MultiConf.WireGuard
}
return true
}
// NewOperatorConfig - Generate a new player/client/operator configuration
func NewOperatorConfig(operatorName string, lhost string, lport uint16, permissions []string) ([]byte, error) {
func NewOperatorConfig(operatorName string, lhost string, lport uint16, permissions []string, includeWG bool) ([]byte, error) {
if !namePattern.MatchString(operatorName) {
return nil, errors.New("invalid operator name (alphanumerics only)")
}
@@ -153,6 +201,34 @@ func NewOperatorConfig(operatorName string, lhost string, lport uint16, permissi
return nil, fmt.Errorf("invalid permission: %s", permission)
}
}
var wgConfig *ClientWGConfig
if includeWG {
certs.SetupWGKeys()
clientIP, err := generateOperatorWGIP()
if err != nil {
return nil, err
}
clientPrivKey, clientPubKey, err := certs.GenerateWGKeyPair()
if err != nil {
return nil, fmt.Errorf("failed to generate wireguard keys: %w", err)
}
_, serverPubKey, err := certs.GetWGServerKeys()
if err != nil {
return nil, fmt.Errorf("failed to get server wireguard keys: %w", err)
}
dbOperator.WGPubKey = clientPubKey
dbOperator.WGTunIP = clientIP
wgConfig = &ClientWGConfig{
ServerPubKey: serverPubKey,
ClientPrivateKey: clientPrivKey,
ClientPubKey: clientPubKey,
ClientIP: clientIP,
ServerIP: multiplayerWireGuardServerIP,
}
}
err := db.Session().Save(dbOperator).Error
if err != nil {
return nil, err
@@ -171,6 +247,10 @@ func NewOperatorConfig(operatorName string, lhost string, lport uint16, permissi
CACertificate: string(caCertPEM),
PrivateKey: string(privateKey),
Certificate: string(publicKey),
WG: wgConfig,
}
if includeWG {
publishOperatorWGPeerAddition(dbOperator)
}
return json.Marshal(config)
}
@@ -218,18 +298,9 @@ func kickOperatorCmd(cmd *cobra.Command, args []string) {
}
fmt.Printf(Info+"Removing auth token(s) for %s, please wait ... \n", operator)
err = db.Session().Where(&models.Operator{
Name: operator,
}).Delete(&models.Operator{}).Error
err = kickOperator(operator)
if err != nil {
fmt.Printf(Warn+"Failed to remove operator %s: %v\n", operator, err)
return
}
transport.ClearTokenCache()
fmt.Printf(Info+"Removing client certificate(s) for %s, please wait ... \n", operator)
err = certs.OperatorClientRemoveCertificate(operator)
if err != nil {
fmt.Printf(Warn+"Failed to remove the operator certificate: %v \n", err)
fmt.Printf(Warn+"Failed to kick operator %s: %v\n", operator, err)
return
}
fmt.Printf(Info+"Operator %s has been kicked out.\n", operator)
@@ -242,6 +313,41 @@ func shouldPromptKickOperator(cmd *cobra.Command, args []string) bool {
return cmd.Flags().NFlag() == 0
}
func removeOperator(operator string) error {
err := db.Session().Where(&models.Operator{
Name: operator,
}).Delete(&models.Operator{}).Error
if err != nil {
return err
}
transport.ClearTokenCache()
return nil
}
func revokeOperatorClientCertificate(operator string) error {
return certs.OperatorClientRemoveCertificate(operator)
}
func closeOperatorStreams(operator string) {
transport.CloseOperatorStreams(operator)
}
func kickOperator(operator string) error {
dbOperators, err := operatorRecordsByName(operator)
if err != nil {
return err
}
if err := removeOperator(operator); err != nil {
return err
}
defer closeOperatorStreams(operator)
for _, dbOperator := range dbOperators {
publishOperatorWGPeerRemoval(dbOperator)
}
return revokeOperatorClientCertificate(operator)
}
func operatorNames() ([]string, error) {
operators, err := db.OperatorAll()
if err != nil {
@@ -284,21 +390,65 @@ func operatorExists(name string) (bool, error) {
return false, err
}
func operatorRecordsByName(name string) ([]*models.Operator, error) {
operators := []*models.Operator{}
err := db.Session().Where(&models.Operator{Name: name}).Find(&operators).Error
return operators, err
}
func publishOperatorWGPeerRemoval(operator *models.Operator) {
if operator == nil || strings.TrimSpace(operator.WGPubKey) == "" {
return
}
core.EventBroker.Publish(core.Event{
EventType: consts.MultiplayerWireGuardRemoved,
Data: []byte(fmt.Sprintf("public_key=%s\nremove=true\n", operator.WGPubKey)),
})
}
func publishOperatorWGPeerAddition(operator *models.Operator) {
if operator == nil || strings.TrimSpace(operator.WGPubKey) == "" || strings.TrimSpace(operator.WGTunIP) == "" {
return
}
core.EventBroker.Publish(core.Event{
EventType: consts.MultiplayerWireGuardNewPeer,
Data: []byte(fmt.Sprintf("public_key=%s\nallowed_ip=%s/32\n", operator.WGPubKey, operator.WGTunIP)),
})
}
func generateOperatorWGIP() (string, error) {
clientIP, err := generate.GenerateUniqueIP()
if err != nil {
return "", fmt.Errorf("failed to allocate wireguard address: %w", err)
}
return clientIP.String(), nil
}
func startMultiplayerModeCmd(cmd *cobra.Command, _ []string) {
lhost, _ := cmd.Flags().GetString("lhost")
lport, _ := cmd.Flags().GetUint16("lport")
tailscale, _ := cmd.Flags().GetBool("tailscale")
disableWG, _ := cmd.Flags().GetBool("disable-wg")
useWireGuard := !disableWG && !tailscale
var err error
var jobID int
if tailscale {
_, err = jobStartTsNetClientListener(lhost, lport)
} else {
jobID, err = JobStartClientListener(&clientpb.MultiplayerListenerReq{Host: lhost, Port: uint32(lport)})
jobID, err = JobStartClientListener(&clientpb.MultiplayerListenerReq{
Host: lhost,
Port: uint32(lport),
WireGuard: useWireGuard,
})
}
if err == nil {
fmt.Printf(Info + "Multiplayer mode enabled!\n")
multiConfig := &clientpb.MultiplayerListenerReq{Host: lhost, Port: uint32(lport)}
multiConfig := &clientpb.MultiplayerListenerReq{
Host: lhost,
Port: uint32(lport),
WireGuard: useWireGuard,
}
listenerJob := &clientpb.ListenerJob{
JobID: uint32(jobID),
Type: "multiplayer",
@@ -315,16 +465,33 @@ func startMultiplayerModeCmd(cmd *cobra.Command, _ []string) {
}
func JobStartClientListener(multiplayerListener *clientpb.MultiplayerListenerReq) (int, error) {
_, ln, err := transport.StartMtlsClientListener(multiplayerListener.Host, uint16(multiplayerListener.Port))
var (
ln net.Listener
err error
)
if multiplayerListener.GetWireGuard() {
_, ln, err = transport.StartWGWrappedMtlsClientListener(multiplayerListener.Host, uint16(multiplayerListener.Port))
} else {
_, ln, err = transport.StartMtlsClientListener(multiplayerListener.Host, uint16(multiplayerListener.Port))
}
if err != nil {
return -1, err // If we fail to bind don't setup the Job
}
name := "grpc/mtls"
description := "client listener"
protocol := "tcp"
if multiplayerListener.GetWireGuard() {
name = "grpc/mtls+wg"
description = "wireguard-wrapped client listener"
protocol = "udp"
}
job := &core.Job{
ID: core.NextJobID(),
Name: "grpc/mtls",
Description: "client listener",
Protocol: "tcp",
Name: name,
Description: description,
Protocol: protocol,
Port: uint16(multiplayerListener.Port),
JobCtrl: make(chan bool),
}
+211
View File
@@ -0,0 +1,211 @@
package console
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"testing"
"time"
clientassets "github.com/bishopfox/sliver/client/assets"
clienttransport "github.com/bishopfox/sliver/client/transport"
"github.com/bishopfox/sliver/protobuf/commonpb"
"github.com/bishopfox/sliver/protobuf/rpcpb"
"github.com/bishopfox/sliver/server/certs"
"github.com/bishopfox/sliver/server/core"
servertransport "github.com/bishopfox/sliver/server/transport"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/status"
"google.golang.org/grpc/test/bufconn"
)
func TestKickOperatorClosesActiveEventStreams(t *testing.T) {
certs.SetupCAs()
listener := bufconn.Listen(2 * 1024 * 1024)
grpcServer, err := servertransport.StartMtlsClientServer(listener)
if err != nil {
t.Fatalf("start mTLS client server: %v", err)
}
defer grpcServer.Stop()
defer listener.Close()
operatorName := uniqueKickOperatorName(t)
t.Cleanup(func() {
_ = removeOperator(operatorName)
_ = revokeOperatorClientCertificate(operatorName)
closeOperatorStreams(operatorName)
})
config := mustNewOperatorAssetsConfig(t, operatorName)
rpcClient, conn, err := mustMTLSBufconnClient(t, listener, config)
if err != nil {
t.Fatalf("connect operator client: %v", err)
}
defer conn.Close()
stream, err := rpcClient.Events(context.Background(), &commonpb.Empty{})
if err != nil {
t.Fatalf("start events stream: %v", err)
}
waitForCondition(t, 3*time.Second, func() bool {
return operatorActive(operatorName)
}, "operator to appear in the active client registry")
recvErr := make(chan error, 1)
go func() {
_, err := stream.Recv()
recvErr <- err
}()
if err := kickOperator(operatorName); err != nil {
t.Fatalf("kick operator: %v", err)
}
select {
case err := <-recvErr:
if err == nil {
t.Fatal("expected kicked operator event stream to close")
}
if errors.Is(err, io.EOF) {
break
}
code := status.Code(err)
if code != codes.Canceled && code != codes.Unavailable && code != codes.Unknown {
t.Fatalf("expected stream cancellation after kick, got %v", err)
}
case <-time.After(3 * time.Second):
t.Fatal("timed out waiting for kicked operator event stream to close")
}
waitForCondition(t, 3*time.Second, func() bool {
return !operatorActive(operatorName)
}, "operator to leave the active client registry")
_, err = rpcClient.GetVersion(context.Background(), &commonpb.Empty{})
if err == nil {
t.Fatal("expected kicked operator to be denied on subsequent RPCs")
}
code := status.Code(err)
if code != codes.Unauthenticated && code != codes.Unavailable {
t.Fatalf("expected unauthenticated or unavailable after kick, got %v", err)
}
exists, err := operatorExists(operatorName)
if err != nil {
t.Fatalf("lookup operator after kick: %v", err)
}
if exists {
t.Fatal("expected operator record to be removed after kick")
}
_, _, err = certs.OperatorClientGetCertificate(operatorName)
if !errors.Is(err, certs.ErrCertDoesNotExist) {
t.Fatalf("expected operator certificate to be removed after kick, got %v", err)
}
}
func mustNewOperatorAssetsConfig(t *testing.T, operatorName string) *clientassets.ClientConfig {
t.Helper()
configJSON, err := NewOperatorConfig(operatorName, "bufnet", 31337, []string{"all"}, false)
if err != nil {
t.Fatalf("generate operator config: %v", err)
}
config := &clientassets.ClientConfig{}
if err := json.Unmarshal(configJSON, config); err != nil {
t.Fatalf("parse operator config: %v", err)
}
return config
}
func mustMTLSBufconnClient(t *testing.T, listener *bufconn.Listener, config *clientassets.ClientConfig) (rpcpb.SliverRPCClient, *grpc.ClientConn, error) {
t.Helper()
dialCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
conn, err := grpc.DialContext(dialCtx, "bufnet",
grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) {
return listener.Dial()
}),
grpc.WithTransportCredentials(credentials.NewTLS(mustClientTLSConfig(t, config))),
grpc.WithPerRPCCredentials(staticTokenAuth(config.Token)),
grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(clienttransport.ClientMaxReceiveMessageSize)),
grpc.WithBlock(),
)
if err != nil {
return nil, nil, err
}
return rpcpb.NewSliverRPCClient(conn), conn, nil
}
func mustClientTLSConfig(t *testing.T, config *clientassets.ClientConfig) *tls.Config {
t.Helper()
clientCert, err := tls.X509KeyPair([]byte(config.Certificate), []byte(config.PrivateKey))
if err != nil {
t.Fatalf("parse client certificate: %v", err)
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM([]byte(config.CACertificate))
return &tls.Config{
Certificates: []tls.Certificate{clientCert},
RootCAs: caCertPool,
InsecureSkipVerify: true,
MinVersion: tls.VersionTLS13,
VerifyPeerCertificate: func(rawCerts [][]byte, _ [][]*x509.Certificate) error {
return clienttransport.RootOnlyVerifyCertificate(config.CACertificate, rawCerts)
},
}
}
func operatorActive(name string) bool {
for _, active := range core.Clients.ActiveOperators() {
if active == name {
return true
}
}
return false
}
func waitForCondition(t *testing.T, timeout time.Duration, predicate func() bool, description string) {
t.Helper()
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
if predicate() {
return
}
time.Sleep(25 * time.Millisecond)
}
t.Fatalf("timed out waiting for %s", description)
}
func uniqueKickOperatorName(t *testing.T) string {
t.Helper()
return fmt.Sprintf("kick-operator-%d", time.Now().UnixNano())
}
type staticTokenAuth string
func (t staticTokenAuth) GetRequestMetadata(context.Context, ...string) (map[string]string, error) {
return map[string]string{
"Authorization": "Bearer " + string(t),
}, nil
}
func (staticTokenAuth) RequireTransportSecurity() bool {
return true
}
+1 -1
View File
@@ -12,7 +12,7 @@ import (
func TestRootOnlyVerifyCertificate(t *testing.T) {
certs.SetupCAs()
data, err := NewOperatorConfig("zerocool", "localhost", uint16(1337), []string{"all"})
data, err := NewOperatorConfig("zerocool", "localhost", uint16(1337), []string{"all"}, false)
if err != nil {
t.Fatalf("failed to generate test player profile %s", err)
}
@@ -0,0 +1,87 @@
package console
import (
"context"
"encoding/json"
"net"
"strings"
"testing"
clientassets "github.com/bishopfox/sliver/client/assets"
clienttransport "github.com/bishopfox/sliver/client/transport"
"github.com/bishopfox/sliver/protobuf/commonpb"
"github.com/bishopfox/sliver/server/certs"
servertransport "github.com/bishopfox/sliver/server/transport"
)
func TestNewOperatorConfigWithWireGuardConnectsToWrappedMultiplayer(t *testing.T) {
certs.SetupCAs()
certs.SetupWGKeys()
clienttransport.SetMultiplayerConnectMode(clienttransport.MultiplayerConnectAuto)
operatorName := uniqueKickOperatorName(t)
t.Cleanup(func() {
_ = removeOperator(operatorName)
_ = revokeOperatorClientCertificate(operatorName)
closeOperatorStreams(operatorName)
})
port := freeUDPPort(t)
configJSON, err := NewOperatorConfig(operatorName, "127.0.0.1", uint16(port), []string{"all"}, true)
if err != nil {
t.Fatalf("generate wireguard operator config: %v", err)
}
config := &clientassets.ClientConfig{}
if err := json.Unmarshal(configJSON, config); err != nil {
t.Fatalf("parse operator config: %v", err)
}
if config.WG == nil {
t.Fatal("expected wireguard config block to be present")
}
operators, err := operatorRecordsByName(operatorName)
if err != nil {
t.Fatalf("load operator row: %v", err)
}
if len(operators) != 1 {
t.Fatalf("expected one operator row, got %d", len(operators))
}
if operators[0].WGPubKey != config.WG.ClientPubKey {
t.Fatalf("expected stored wireguard public key %q, got %q", config.WG.ClientPubKey, operators[0].WGPubKey)
}
if operators[0].WGTunIP != config.WG.ClientIP {
t.Fatalf("expected stored wireguard tunnel IP %q, got %q", config.WG.ClientIP, operators[0].WGTunIP)
}
grpcServer, ln, err := servertransport.StartWGWrappedMtlsClientListener("127.0.0.1", uint16(port))
if err != nil {
if strings.Contains(err.Error(), "operation not permitted") {
t.Skipf("wireguard listener bind not permitted in this environment: %v", err)
}
t.Fatalf("start wrapped multiplayer listener: %v", err)
}
defer grpcServer.Stop()
defer ln.Close()
rpcClient, conn, err := clienttransport.MTLSConnect(config)
if err != nil {
t.Fatalf("connect operator through wireguard wrapper: %v", err)
}
defer clienttransport.CloseGRPCConnection(conn)
if _, err := rpcClient.GetVersion(context.Background(), &commonpb.Empty{}); err != nil {
t.Fatalf("GetVersion over wrapped multiplayer failed: %v", err)
}
}
func freeUDPPort(t *testing.T) int {
t.Helper()
ln, err := net.ListenPacket("udp4", "127.0.0.1:0")
if err != nil {
t.Skipf("udp binds are not permitted in this environment: %v", err)
}
defer ln.Close()
return ln.LocalAddr().(*net.UDPAddr).Port
}
+6
View File
@@ -75,6 +75,7 @@ func serverOnlyCmds() (commands []*cobra.Command) {
f.StringP("lhost", "L", "", "interface to bind server to")
f.Uint16P("lport", "l", 31337, "tcp listen port")
f.BoolP("tailscale", "T", false, "only expose multiplayer interface over Tailscale (requires TS_AUTHKEY)")
f.Bool("disable-wg", false, "expose multiplayer directly instead of wrapping it in WireGuard")
})
commands = append(commands, startMultiplayer)
@@ -92,6 +93,7 @@ func serverOnlyCmds() (commands []*cobra.Command) {
f.StringP("save", "s", "", "directory/file in which to save config")
f.StringP("name", "n", "", "operator name")
f.StringSliceP("permissions", "P", []string{}, "grant permissions to the operator profile (all, builder, crackstation)")
f.Bool("disable-wg", false, "omit WireGuard tunnel settings from the generated operator config")
})
command.BindFlagCompletions(newOperator, func(comp *carapace.ActionMap) {
(*comp)["save"] = carapace.ActionDirectories()
@@ -119,6 +121,10 @@ Create a new operator config file, operator configuration files allow
remote machines to connect to the Sliver server. They are most commonly
used for allowing remote operators to connect in "Multiplayer Mode."
By default, operator configs now include a WireGuard wrapper for the
multiplayer listener; use --disable-wg if the listener is being exposed
directly instead.
To generate a profile for a remote operator, you need to specify the
"all" permission to grant the profile access to all gRPC APIs:
+3 -1
View File
@@ -41,7 +41,7 @@ var (
)
// Start - Start as daemon process
func Start(host string, port uint16, tailscale bool) {
func Start(host string, port uint16, tailscale bool, disableWG bool) {
var (
ln net.Listener
err error
@@ -59,6 +59,8 @@ func Start(host string, port uint16, tailscale bool) {
daemonLog.Infof("Starting Sliver daemon %s:%d ...", host, port)
if tailscale {
_, ln, err = transport.StartTsNetClientListener(host, port)
} else if !disableWG {
_, ln, err = transport.StartWGWrappedMtlsClientListener(host, port)
} else {
_, ln, err = transport.StartMtlsClientListener(host, port)
}
+8
View File
@@ -872,6 +872,14 @@ func WGPeerIPs() ([]string, error) {
for _, peer := range wgPeers {
ips = append(ips, peer.TunIP)
}
operators := []*models.Operator{}
err = Session().Where("wg_tun_ip <> ''").Find(&operators).Error
if err != nil {
return nil, err
}
for _, operator := range operators {
ips = append(ips, operator.WGTunIP)
}
return ips, nil
}
+7 -4
View File
@@ -92,6 +92,7 @@ type MultiplayerListener struct {
ListenerJobID uuid.UUID `gorm:"type:uuid;"`
Host string
Port uint32
WireGuard bool
}
type DnsDomain struct {
@@ -207,8 +208,9 @@ func (j *MtlsListener) ToProtobuf() *clientpb.MTLSListenerReq {
func (j *MultiplayerListener) ToProtobuf() *clientpb.MultiplayerListenerReq {
return &clientpb.MultiplayerListenerReq{
Host: j.Host,
Port: j.Port,
Host: j.Host,
Port: j.Port,
WireGuard: j.WireGuard,
}
}
@@ -277,8 +279,9 @@ func ListenerJobFromProtobuf(pbListenerJob *clientpb.ListenerJob) *ListenerJob {
}
case constants.MultiplayerModeStr:
cfg.MultiplayerListener = MultiplayerListener{
Host: pbListenerJob.MultiConf.Host,
Port: pbListenerJob.MultiConf.Port,
Host: pbListenerJob.MultiConf.Host,
Port: pbListenerJob.MultiConf.Port,
WireGuard: pbListenerJob.MultiConf.WireGuard,
}
}
+2
View File
@@ -37,6 +37,8 @@ type Operator struct {
PermissionAll bool `gorm:"default:false"` // Access all gRPC APIs
PermissionBuilder bool `gorm:"default:false"` // Access Builder gRPC APIs
PermissionCrackstation bool `gorm:"default:false"` // Access Crackstation gRPC APIs
WGPubKey string
WGTunIP string
}
// BeforeCreate - GORM hook
+2
View File
@@ -74,6 +74,7 @@ func initMiddleware(enableAuth bool) []grpc.ServerOption {
grpc.ChainStreamInterceptor(
grpc_auth.StreamServerInterceptor(tokenAuthFunc),
permissionsStreamServerInterceptor(),
trackOperatorStreamInterceptor(),
grpc_tags.StreamServerInterceptor(grpc_tags.WithFieldExtractor(grpc_tags.CodeGenRequestFieldExtractor)),
grpc_logrus.StreamServerInterceptor(logrusEntry, logrusOpts...),
grpc_logrus.PayloadStreamServerInterceptor(logrusEntry, deciderStream),
@@ -90,6 +91,7 @@ func initMiddleware(enableAuth bool) []grpc.ServerOption {
),
grpc.ChainStreamInterceptor(
grpc_auth.StreamServerInterceptor(serverAuthFunc),
trackOperatorStreamInterceptor(),
grpc_tags.StreamServerInterceptor(grpc_tags.WithFieldExtractor(grpc_tags.CodeGenRequestFieldExtractor)),
grpc_logrus.StreamServerInterceptor(logrusEntry, logrusOpts...),
grpc_logrus.PayloadStreamServerInterceptor(logrusEntry, deciderStream),
+28 -5
View File
@@ -21,6 +21,7 @@ package transport
import (
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"net"
"runtime/debug"
@@ -50,15 +51,34 @@ var (
// StartMtlsClientListener - Start a mutual TLS listener
func StartMtlsClientListener(host string, port uint16) (*grpc.Server, net.Listener, error) {
mtlsLog.Infof("Starting gRPC/mtls listener on %s:%d", host, port)
tlsConfig := getOperatorServerTLSConfig("multiplayer")
creds := credentials.NewTLS(tlsConfig)
ln, err := net.Listen("tcp", fmt.Sprintf("%s:%d", host, port))
if err != nil {
mtlsLog.Error(err)
return nil, nil, err
}
grpcServer, err := StartMtlsClientServer(ln)
if err != nil {
ln.Close()
return nil, nil, err
}
return grpcServer, ln, nil
}
// StartMtlsClientServer serves the authenticated multiplayer gRPC server on an
// existing listener. This is primarily useful for tests that need the full mTLS
// + auth stack without opening a real TCP socket.
func StartMtlsClientServer(ln net.Listener) (*grpc.Server, error) {
if ln == nil {
return nil, errors.New("listener is required")
}
tlsConfig := getOperatorServerTLSConfig("multiplayer")
if tlsConfig == nil {
return nil, errors.New("failed to create operator TLS config")
}
creds := credentials.NewTLS(tlsConfig)
options := []grpc.ServerOption{
grpc.Creds(creds),
grpc.MaxRecvMsgSize(ServerMaxMessageSize),
@@ -81,7 +101,7 @@ func StartMtlsClientListener(host string, port uint16) (*grpc.Server, net.Listen
panicked = false
}
}()
return grpcServer, ln, nil
return grpcServer, nil
}
// getOperatorServerTLSConfig - Generate the TLS configuration, we do now allow the end user
@@ -115,6 +135,9 @@ func getOperatorServerTLSConfig(host string) *tls.Config {
ClientCAs: caCertPool,
Certificates: []tls.Certificate{cert},
MinVersion: tls.VersionTLS13,
VerifyConnection: func(state tls.ConnectionState) error {
return certs.ValidateOperatorClientCertificate(state.PeerCertificates)
},
}
return tlsConfig
+153
View File
@@ -0,0 +1,153 @@
package transport
import (
"crypto/tls"
"errors"
"fmt"
"net"
"strings"
"testing"
"time"
"github.com/bishopfox/sliver/server/certs"
)
func TestOperatorClientCertificateValidation(t *testing.T) {
certs.SetupCAs()
t.Run("accepts stored operator certificate", func(t *testing.T) {
operatorName := uniqueOperatorName(t, "stored")
certPEM, keyPEM, err := certs.OperatorClientGenerateCertificate(operatorName)
if err != nil {
t.Fatalf("generate operator certificate: %v", err)
}
err = performMutualTLSHandshake(getOperatorServerTLSConfig("multiplayer"), newClientTLSConfig(t, certPEM, keyPEM))
if err != nil {
t.Fatalf("expected stored operator certificate to be accepted, got %v", err)
}
})
t.Run("rejects deleted operator certificate", func(t *testing.T) {
operatorName := uniqueOperatorName(t, "deleted")
certPEM, keyPEM, err := certs.OperatorClientGenerateCertificate(operatorName)
if err != nil {
t.Fatalf("generate operator certificate: %v", err)
}
if err := certs.OperatorClientRemoveCertificate(operatorName); err != nil {
t.Fatalf("remove operator certificate: %v", err)
}
err = performMutualTLSHandshake(getOperatorServerTLSConfig("multiplayer"), newClientTLSConfig(t, certPEM, keyPEM))
if err == nil {
t.Fatal("expected deleted operator certificate to be rejected")
}
if !strings.Contains(err.Error(), certs.ErrOperatorClientCertificateNotFound.Error()) {
t.Fatalf("expected database rejection error, got %v", err)
}
})
t.Run("rejects rotated-out certificate but accepts current one", func(t *testing.T) {
operatorName := uniqueOperatorName(t, "rotated")
oldCertPEM, oldKeyPEM, err := certs.OperatorClientGenerateCertificate(operatorName)
if err != nil {
t.Fatalf("generate original operator certificate: %v", err)
}
if err := certs.OperatorClientRemoveCertificate(operatorName); err != nil {
t.Fatalf("remove original operator certificate: %v", err)
}
newCertPEM, newKeyPEM, err := certs.OperatorClientGenerateCertificate(operatorName)
if err != nil {
t.Fatalf("generate replacement operator certificate: %v", err)
}
err = performMutualTLSHandshake(getOperatorServerTLSConfig("multiplayer"), newClientTLSConfig(t, oldCertPEM, oldKeyPEM))
if err == nil {
t.Fatal("expected rotated-out operator certificate to be rejected")
}
if !strings.Contains(err.Error(), certs.ErrOperatorClientCertificateNotFound.Error()) {
t.Fatalf("expected rotated certificate to fail database validation, got %v", err)
}
err = performMutualTLSHandshake(getOperatorServerTLSConfig("multiplayer"), newClientTLSConfig(t, newCertPEM, newKeyPEM))
if err != nil {
t.Fatalf("expected replacement operator certificate to be accepted, got %v", err)
}
})
t.Run("rejects certificate from another authority", func(t *testing.T) {
certPEM, keyPEM := certs.GenerateECCCertificate(certs.HTTPSCA, uniqueOperatorName(t, "wrong-ca"), false, true, false)
err := performMutualTLSHandshake(getOperatorServerTLSConfig("multiplayer"), newClientTLSConfig(t, certPEM, keyPEM))
if err == nil {
t.Fatal("expected certificate from another authority to be rejected")
}
if strings.Contains(err.Error(), certs.ErrOperatorClientCertificateNotFound.Error()) {
t.Fatalf("expected trust-chain rejection before database lookup, got %v", err)
}
})
}
func newClientTLSConfig(t *testing.T, certPEM []byte, keyPEM []byte) *tls.Config {
t.Helper()
clientCert, err := tls.X509KeyPair(certPEM, keyPEM)
if err != nil {
t.Fatalf("parse client certificate: %v", err)
}
return &tls.Config{
Certificates: []tls.Certificate{clientCert},
InsecureSkipVerify: true,
MinVersion: tls.VersionTLS13,
}
}
func performMutualTLSHandshake(serverConfig *tls.Config, clientConfig *tls.Config) error {
serverConn, clientConn := net.Pipe()
defer serverConn.Close()
defer clientConn.Close()
deadline := time.Now().Add(2 * time.Second)
_ = serverConn.SetDeadline(deadline)
_ = clientConn.SetDeadline(deadline)
serverTLS := tls.Server(serverConn, serverConfig)
clientTLS := tls.Client(clientConn, clientConfig)
defer serverTLS.Close()
defer clientTLS.Close()
type handshakeResult struct {
side string
err error
}
results := make(chan handshakeResult, 2)
go func() {
results <- handshakeResult{side: "server", err: serverTLS.Handshake()}
}()
go func() {
results <- handshakeResult{side: "client", err: clientTLS.Handshake()}
}()
var errs []string
for i := 0; i < 2; i++ {
result := <-results
if result.err == nil {
continue
}
if errors.Is(result.err, net.ErrClosed) {
continue
}
errs = append(errs, fmt.Sprintf("%s: %v", result.side, result.err))
}
if len(errs) == 0 {
return nil
}
return errors.New(strings.Join(errs, "; "))
}
func uniqueOperatorName(t *testing.T, prefix string) string {
t.Helper()
return fmt.Sprintf("%s-%d", prefix, time.Now().UnixNano())
}
+150
View File
@@ -0,0 +1,150 @@
package transport
import (
"bufio"
"bytes"
"fmt"
"net"
"net/netip"
"strings"
"sync"
consts "github.com/bishopfox/sliver/client/constants"
"github.com/bishopfox/sliver/server/certs"
"github.com/bishopfox/sliver/server/core"
servernetstack "github.com/bishopfox/sliver/server/netstack"
"golang.zx2c4.com/wireguard/conn"
"golang.zx2c4.com/wireguard/device"
"google.golang.org/grpc"
)
const (
multiplayerWireGuardServerIP = "100.64.0.1"
multiplayerWireGuardMTU = 1420
)
type wireGuardWrappedClientListener struct {
net.Listener
dev *device.Device
events chan core.Event
once sync.Once
}
func (l *wireGuardWrappedClientListener) Close() error {
var err error
l.once.Do(func() {
if l.events != nil {
core.EventBroker.Unsubscribe(l.events)
l.events = nil
}
if l.Listener != nil {
err = l.Listener.Close()
}
if l.dev != nil {
l.dev.Close()
}
})
return err
}
func (l *wireGuardWrappedClientListener) processPeerEvents() {
if l == nil || l.events == nil || l.dev == nil {
return
}
for event := range l.events {
switch event.EventType {
case consts.MultiplayerWireGuardNewPeer, consts.MultiplayerWireGuardRemoved:
if len(event.Data) == 0 {
continue
}
if err := l.dev.IpcSetOperation(bufio.NewReader(bytes.NewReader(event.Data))); err != nil {
mtlsLog.Errorf("Failed to update multiplayer wireguard config: %v", err)
}
}
}
}
// StartWGWrappedMtlsClientListener exposes the multiplayer mTLS listener only
// inside a WireGuard tunnel while preserving the existing gRPC/mTLS auth stack.
func StartWGWrappedMtlsClientListener(host string, port uint16) (*grpc.Server, net.Listener, error) {
mtlsLog.Infof("Starting gRPC/mtls listener on %s:%d inside WireGuard", host, port)
tunAddr := netip.MustParseAddr(multiplayerWireGuardServerIP)
tun, tNet, err := servernetstack.CreateNetTUN([]netip.Addr{tunAddr}, nil, multiplayerWireGuardMTU)
if err != nil {
return nil, nil, err
}
if err := tNet.AllowTCPPort(tunAddr, port); err != nil {
_ = tun.Close()
return nil, nil, err
}
privateKey, _, err := certs.GetWGServerKeys()
if err == certs.ErrWGServerKeysDoNotExist {
certs.SetupWGKeys()
privateKey, _, err = certs.GetWGServerKeys()
}
if err != nil {
_ = tun.Close()
return nil, nil, err
}
dev := device.NewDevice(tun, conn.NewDefaultBind(), device.NewLogger(device.LogLevelSilent, "[transport/multiplayer-wg] "))
wgConf := bytes.NewBuffer(nil)
fmt.Fprintf(wgConf, "private_key=%s\n", privateKey)
fmt.Fprintf(wgConf, "listen_port=%d\n", port)
if err := appendOperatorWGPeers(wgConf); err != nil {
dev.Close()
return nil, nil, err
}
if err := dev.IpcSetOperation(bufio.NewReader(wgConf)); err != nil {
dev.Close()
return nil, nil, err
}
if err := dev.Up(); err != nil {
dev.Close()
return nil, nil, err
}
innerListener, err := tNet.ListenTCP(&net.TCPAddr{IP: net.ParseIP(multiplayerWireGuardServerIP), Port: int(port)})
if err != nil {
dev.Close()
return nil, nil, err
}
wrappedListener := &wireGuardWrappedClientListener{
Listener: innerListener,
dev: dev,
events: core.EventBroker.Subscribe(),
}
go wrappedListener.processPeerEvents()
grpcServer, err := StartMtlsClientServer(wrappedListener)
if err != nil {
_ = wrappedListener.Close()
return nil, nil, err
}
return grpcServer, wrappedListener, nil
}
func appendOperatorWGPeers(dst *bytes.Buffer) error {
peers, err := certs.GetOperatorWGPeers()
if err != nil && err != certs.ErrWGPeerDoesNotExist {
return err
}
for publicKey, tunIP := range peers {
if strings.TrimSpace(publicKey) == "" || strings.TrimSpace(tunIP) == "" {
continue
}
if _, err := netip.ParseAddr(tunIP); err != nil {
mtlsLog.Warnf("Skipping multiplayer wireguard peer %q with invalid tunnel IP %q: %v", publicKey, tunIP, err)
continue
}
fmt.Fprintf(dst, "public_key=%s\n", publicKey)
fmt.Fprintf(dst, "allowed_ip=%s/32\n", tunIP)
}
return nil
}
+130
View File
@@ -0,0 +1,130 @@
package transport
import (
"context"
"strings"
"sync"
"sync/atomic"
"github.com/bishopfox/sliver/server/db/models"
"google.golang.org/grpc"
)
type trackedServerStream struct {
grpc.ServerStream
ctx context.Context
}
func (s *trackedServerStream) Context() context.Context {
return s.ctx
}
type operatorStreamRegistry struct {
mu sync.Mutex
nextID atomic.Uint64
operators map[string]map[uint64]context.CancelFunc
}
var activeOperatorStreams = &operatorStreamRegistry{
operators: map[string]map[uint64]context.CancelFunc{},
}
func (r *operatorStreamRegistry) register(operator string, cancel context.CancelFunc) uint64 {
if cancel == nil {
return 0
}
operator = strings.TrimSpace(operator)
if operator == "" {
return 0
}
streamID := r.nextID.Add(1)
r.mu.Lock()
defer r.mu.Unlock()
streams := r.operators[operator]
if streams == nil {
streams = map[uint64]context.CancelFunc{}
r.operators[operator] = streams
}
streams[streamID] = cancel
return streamID
}
func (r *operatorStreamRegistry) unregister(operator string, streamID uint64) {
if streamID == 0 {
return
}
operator = strings.TrimSpace(operator)
if operator == "" {
return
}
r.mu.Lock()
defer r.mu.Unlock()
streams := r.operators[operator]
if streams == nil {
return
}
delete(streams, streamID)
if len(streams) == 0 {
delete(r.operators, operator)
}
}
func (r *operatorStreamRegistry) close(operator string) int {
operator = strings.TrimSpace(operator)
if operator == "" {
return 0
}
r.mu.Lock()
streams := r.operators[operator]
if len(streams) == 0 {
r.mu.Unlock()
return 0
}
cancels := make([]context.CancelFunc, 0, len(streams))
for streamID, cancel := range streams {
cancels = append(cancels, cancel)
delete(streams, streamID)
}
delete(r.operators, operator)
r.mu.Unlock()
for _, cancel := range cancels {
cancel()
}
return len(cancels)
}
// CloseOperatorStreams cancels all tracked operator-owned gRPC streams.
func CloseOperatorStreams(operator string) int {
return activeOperatorStreams.close(operator)
}
func trackOperatorStreamInterceptor() grpc.StreamServerInterceptor {
return func(srv interface{}, ss grpc.ServerStream, _ *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
operator, ok := ss.Context().Value(Operator).(*models.Operator)
if !ok || operator == nil || strings.TrimSpace(operator.Name) == "" {
return handler(srv, ss)
}
ctx, cancel := context.WithCancel(ss.Context())
streamID := activeOperatorStreams.register(operator.Name, cancel)
defer func() {
activeOperatorStreams.unregister(operator.Name, streamID)
cancel()
}()
return handler(srv, &trackedServerStream{
ServerStream: ss,
ctx: ctx,
})
}
}