mirror of
https://github.com/BishopFox/sliver.git
synced 2026-11-08 01:38:17 -10:00
Compare commits
22
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a93f312b4 | ||
|
|
b355fe70d6 | ||
|
|
10b1079286 | ||
|
|
9c042e6977 | ||
|
|
62583cac8f | ||
|
|
95764945b8 | ||
|
|
26abda1c15 | ||
|
|
01dd3cac72 | ||
|
|
f8faa1f767 | ||
|
|
a9e48e84cd | ||
|
|
9d6704d925 | ||
|
|
ce5157e2e9 | ||
|
|
7d19051df8 | ||
|
|
a0d3bd579f | ||
|
|
82c309632a | ||
|
|
5c0b8ec460 | ||
|
|
7791a86eb4 | ||
|
|
7ac4db3fa0 | ||
|
|
512bc822ef | ||
|
|
f2bb9c3733 | ||
|
|
0bd4f100fd | ||
|
|
3230322fd6 |
No files matched your search
@@ -1,3 +1,4 @@
|
||||
/.vscode
|
||||
/.vscode/*
|
||||
/.devcontainer
|
||||
/.idea
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)")
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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...)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
@@ -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,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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+2643
-5407
File diff suppressed because it is too large.
Load diff
@@ -456,6 +456,7 @@ message ListenerJob {
|
||||
message MultiplayerListenerReq {
|
||||
string Host = 1;
|
||||
uint32 Port = 2;
|
||||
bool WireGuard = 3;
|
||||
}
|
||||
|
||||
message MTLSListenerReq {
|
||||
|
||||
+570
-547
File diff suppressed because it is too large.
Load diff
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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),
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user