Repository: liamg/siphon
Branch: main
Commit: 6a6e402ef506
Files: 15
Total size: 11.8 KB
Directory structure:
gitextract_i7w6ueu6/
├── .github/
│ └── workflows/
│ ├── release.yml
│ └── test.yml
├── .gitignore
├── .goreleaser.yml
├── LICENSE
├── README.md
├── go.mod
├── go.sum
├── intercept.go
├── main.go
├── registers_386.go
├── registers_amd64.go
├── registers_arm.go
├── registers_arm64.go
└── watch_test.go
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/release.yml
================================================
name: release
on:
push:
tags:
- v*
jobs:
build:
name: releasing
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: actions/setup-go@v3
with:
go-version: "1.19"
- uses: goreleaser/goreleaser-action@v3
with:
version: latest
args: release --rm-dist
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
================================================
FILE: .github/workflows/test.yml
================================================
name: tests
on:
pull_request:
jobs:
test:
name: tests
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ ubuntu-latest ]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: '1.19'
cache: true
- name: Run tests
run: sudo -E $(which go) test ./...
================================================
FILE: .gitignore
================================================
.idea
================================================
FILE: .goreleaser.yml
================================================
builds:
- id: siphon
main: .
binary: siphon
ldflags:
- "-s -w -extldflags '-fno-PIC -static'"
env:
- CGO_ENABLED=0
goos:
- linux
goarch:
- "amd64"
- "arm64"
- "386"
- "arm"
changelog:
sort: asc
filters:
exclude:
- "^docs:"
- "^test:"
archives:
- format: binary
name_template: "{{ .Binary}}-{{ .Os }}-{{ .Arch }}"
release:
prerelease: auto
github:
owner: liamg
name: siphon
================================================
FILE: LICENSE
================================================
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/>
================================================
FILE: README.md
================================================
# Siphon
Intercept input/output (_stdin/stdout/stderr_) for any process, even where said output is sent to _/dev/null_ or elsewhere.

It can also be used to spy on another users shell:

Currently Siphon works on Linux, with `amd64`, `arm64`, `arm`, and `386`. Adding support for more architectures is pretty simple, feel free to raise an issue.
It uses `ptrace` which means you'll likely need to run it as `root` for the ptrace privilege.
## Installation
Grab a binary from the [latest release](https://github.com/liamg/siphon/releases/latest).
================================================
FILE: go.mod
================================================
module github.com/liamg/siphon
go 1.19
require (
github.com/spf13/cobra v1.6.0
github.com/stretchr/testify v1.7.4
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
================================================
FILE: go.sum
================================================
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.6.0 h1:42a0n6jwCot1pUmomAp4T7DeMD+20LFv4Q54pxLf2LI=
github.com/spf13/cobra v1.6.0/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.4 h1:wZRexSlwd7ZXfKINDLsO4r7WBt3gTKONc6K/VesHvHM=
github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
================================================
FILE: intercept.go
================================================
package main
import (
"fmt"
"os"
"runtime"
"syscall"
)
func watchProcess(pid int, stdout, stderr, stdin bool) error {
// ensure tracing all comes from same thread
runtime.LockOSThread()
if _, err := os.FindProcess(pid); err != nil {
return fmt.Errorf("could not find process with pid %d: %w", pid, err)
}
if err := syscall.PtraceAttach(pid); err == syscall.EPERM {
return fmt.Errorf("could not attach to process with pid %d: %w - check your permissions", pid, err)
} else if err != nil {
return err
}
status := syscall.WaitStatus(0)
if _, err := syscall.Wait4(pid, &status, 0, nil); err != nil {
return err
}
defer func() {
_ = syscall.PtraceDetach(pid)
_, _ = syscall.Wait4(pid, &status, 0, nil)
}()
// deliver SIGTRAP|0x80
if err := syscall.PtraceSetOptions(pid, syscall.PTRACE_O_TRACESYSGOOD); err != nil {
return err
}
for {
fd, data, err := interceptReadsAndWrites(pid)
if err != nil {
return err
}
if stdout && fd == uint64(syscall.Stdout) || stderr && fd == uint64(syscall.Stderr) || stdin && fd == uint64(syscall.Stdin) {
if fd == uint64(syscall.Stdin) {
fd = uint64(os.Stdin.Fd())
}
_, _ = fmt.Fprintf(os.NewFile(uintptr(fd), "pipe"), "%s", string(data))
}
}
}
func interceptReadsAndWrites(pid int) (fd uint64, data []byte, err error) {
// intercept syscall
err = syscall.PtraceSyscall(pid, 0)
if err != nil {
return 0, nil, fmt.Errorf("could not intercept syscall: %w", err)
}
// wait for a syscall
status := syscall.WaitStatus(0)
_, err = syscall.Wait4(pid, &status, 0, nil)
if err != nil {
return 0, nil, err
}
// if interrupted, stop tracing
if status.StopSignal().String() == "interrupt" {
_ = syscall.PtraceSyscall(pid, int(status.StopSignal()))
return 0, nil, fmt.Errorf("process interrupted")
}
var exited bool
waitForExit := func() error {
if exited {
return nil
}
exited = true
// continue the syscall we intercepted
err = syscall.PtraceSyscall(pid, 0)
if err != nil {
return fmt.Errorf("could not continue process: %w", err)
}
// and wait for it to finish
status = syscall.WaitStatus(0)
_, err = syscall.Wait4(pid, &status, 0, nil)
if err != nil {
return err
}
return nil
}
defer func() {
err = waitForExit()
if err == nil {
// process exited
if status.Exited() {
err = fmt.Errorf("process exited")
return
}
// if interrupted, stop tracing
if status.StopSignal().String() == "interrupt" {
_ = syscall.PtraceSyscall(pid, int(status.StopSignal()))
err = fmt.Errorf("process interrupted")
return
}
}
}()
// if we have a syscall, examine it...
if status.TrapCause()&int(syscall.SIGTRAP|0x80) > 0 {
// wait for syscall exit
if err := waitForExit(); err != nil {
return 0, nil, err
}
// read registers
regs := &syscall.PtraceRegs{}
if err := syscall.PtraceGetRegs(pid, regs); err != nil {
return 0, nil, err
}
// find the syscall number for the host architecture
syscallNo := grabSyscallNo(regs)
// if it's a read/write syscall, grab the args
switch syscallNo {
case syscall.SYS_READ, syscall.SYS_WRITE:
// grab the args to WRITE for the host architecture
// fd == file descriptor (generally 1 for stdout, 2 for stderr)
// ptr == pointer to the buffer
// lng == length of the buffer
fd, ptr, lng := grabArgsFromRegs(regs)
// if we want to see this output, read it from memory
if lng > 0 {
data := make([]byte, lng)
if _, err := syscall.PtracePeekData(pid, uintptr(ptr), data); err != nil {
return 0, nil, err
}
return fd, data, nil
}
}
}
return 0, nil, err
}
================================================
FILE: main.go
================================================
package main
import (
"fmt"
"strconv"
"github.com/spf13/cobra"
)
var cmd = &cobra.Command{
Use: "siphon [pid]",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
cmd.SilenceUsage = true
cmd.SilenceErrors = true
pid, err := strconv.ParseInt(args[0], 10, 64)
if err != nil {
return fmt.Errorf("invalid pid: %w", err)
}
return watchProcess(int(pid), flagStdOut, flagStdErr, flagStdIn)
},
}
var (
flagStdOut = true
flagStdErr = true
flagStdIn = false
)
func main() {
cmd.Flags().BoolVarP(&flagStdOut, "stdout", "o", flagStdOut, "Show stdout")
cmd.Flags().BoolVarP(&flagStdErr, "stderr", "e", flagStdErr, "Show stderr")
cmd.Flags().BoolVarP(&flagStdIn, "stdin", "i", flagStdIn, "Show stdin")
if err := cmd.Execute(); err != nil {
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Error: %v", err)
}
}
================================================
FILE: registers_386.go
================================================
//go:build 386
package main
import (
"syscall"
)
func grabSyscallNo(regs *syscall.PtraceRegs) uint64 {
return uint64(regs.Orig_eax)
}
func grabArgsFromRegs(regs *syscall.PtraceRegs) (fd, ptr, lng uint64) {
return uint64(regs.Ebx), uint64(regs.Ecx), uint64(regs.Edx)
}
================================================
FILE: registers_amd64.go
================================================
//go:build amd64
package main
import (
"syscall"
)
func grabSyscallNo(regs *syscall.PtraceRegs) uint64 {
return regs.Orig_rax
}
func grabArgsFromRegs(regs *syscall.PtraceRegs) (fd, ptr, lng uint64) {
return regs.Rdi, regs.Rsi, regs.Rdx
}
================================================
FILE: registers_arm.go
================================================
//go:build arm
package main
import (
"syscall"
)
func grabSyscallNo(regs *syscall.PtraceRegs) uint64 {
return uint64(regs.Uregs[7])
}
func grabArgsFromRegs(regs *syscall.PtraceRegs) (fd, ptr, lng uint64) {
return uint64(regs.Uregs[0]), uint64(regs.Uregs[1]), uint64(regs.Uregs[2])
}
================================================
FILE: registers_arm64.go
================================================
//go:build arm64
package main
import (
"syscall"
)
func grabSyscallNo(regs *syscall.PtraceRegs) uint64 {
return regs.Regs[8]
}
func grabArgsFromRegs(regs *syscall.PtraceRegs) (fd, ptr, lng uint64) {
return regs.Regs[0], regs.Regs[1], regs.Regs[2]
}
================================================
FILE: watch_test.go
================================================
package main
import (
"bytes"
"os"
"os/exec"
"strconv"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_Watch(t *testing.T) {
require.Equal(t, 0, os.Getuid(), "test must be run as root")
pingBuffer := bytes.NewBuffer([]byte{})
pingCmd := exec.Command("ping", "127.0.0.1")
pingCmd.Stdout = pingBuffer
require.NoError(t, pingCmd.Start())
time.Sleep(time.Second)
watchBuffer := bytes.NewBuffer([]byte{})
watchCmd := exec.Command("go", "run", ".", strconv.Itoa(pingCmd.Process.Pid))
watchCmd.Stdout = watchBuffer
require.NoError(t, watchCmd.Start())
time.Sleep(5 * time.Second)
require.NoError(t, pingCmd.Process.Kill())
require.NoError(t, pingCmd.Process.Release())
_ = watchCmd.Wait()
watchOutput := watchBuffer.String()
assert.True(t, strings.HasSuffix(pingBuffer.String(), watchOutput))
assert.Greater(t, len(watchOutput), 0)
}
gitextract_i7w6ueu6/ ├── .github/ │ └── workflows/ │ ├── release.yml │ └── test.yml ├── .gitignore ├── .goreleaser.yml ├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── intercept.go ├── main.go ├── registers_386.go ├── registers_amd64.go ├── registers_arm.go ├── registers_arm64.go └── watch_test.go
SYMBOL INDEX (12 symbols across 7 files)
FILE: intercept.go
function watchProcess (line 10) | func watchProcess(pid int, stdout, stderr, stdin bool) error {
function interceptReadsAndWrites (line 54) | func interceptReadsAndWrites(pid int) (fd uint64, data []byte, err error) {
FILE: main.go
function main (line 30) | func main() {
FILE: registers_386.go
function grabSyscallNo (line 9) | func grabSyscallNo(regs *syscall.PtraceRegs) uint64 {
function grabArgsFromRegs (line 13) | func grabArgsFromRegs(regs *syscall.PtraceRegs) (fd, ptr, lng uint64) {
FILE: registers_amd64.go
function grabSyscallNo (line 9) | func grabSyscallNo(regs *syscall.PtraceRegs) uint64 {
function grabArgsFromRegs (line 13) | func grabArgsFromRegs(regs *syscall.PtraceRegs) (fd, ptr, lng uint64) {
FILE: registers_arm.go
function grabSyscallNo (line 9) | func grabSyscallNo(regs *syscall.PtraceRegs) uint64 {
function grabArgsFromRegs (line 13) | func grabArgsFromRegs(regs *syscall.PtraceRegs) (fd, ptr, lng uint64) {
FILE: registers_arm64.go
function grabSyscallNo (line 9) | func grabSyscallNo(regs *syscall.PtraceRegs) uint64 {
function grabArgsFromRegs (line 13) | func grabArgsFromRegs(regs *syscall.PtraceRegs) (fd, ptr, lng uint64) {
FILE: watch_test.go
function Test_Watch (line 17) | func Test_Watch(t *testing.T) {
Condensed preview — 15 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (14K chars).
[
{
"path": ".github/workflows/release.yml",
"chars": 458,
"preview": "name: release\n\non:\n push:\n tags:\n - v*\n\njobs:\n build:\n name: releasing\n runs-on: ubuntu-latest\n\n step"
},
{
"path": ".github/workflows/test.yml",
"chars": 363,
"preview": "name: tests\non:\n pull_request:\njobs:\n test:\n name: tests\n runs-on: ${{ matrix.os }}\n strategy:\n matrix:\n"
},
{
"path": ".gitignore",
"chars": 6,
"preview": ".idea\n"
},
{
"path": ".goreleaser.yml",
"chars": 480,
"preview": "builds:\n - id: siphon\n main: .\n binary: siphon\n ldflags:\n - \"-s -w -extldflags '-fno-PIC -static'\"\n en"
},
{
"path": "LICENSE",
"chars": 1211,
"preview": "This is free and unencumbered software released into the public domain.\n\nAnyone is free to copy, modify, publish, use, c"
},
{
"path": "README.md",
"chars": 598,
"preview": "# Siphon\n\nIntercept input/output (_stdin/stdout/stderr_) for any process, even where said output is sent to _/dev/null_ "
},
{
"path": "go.mod",
"chars": 367,
"preview": "module github.com/liamg/siphon\n\ngo 1.19\n\nrequire (\n\tgithub.com/spf13/cobra v1.6.0\n\tgithub.com/stretchr/testify v1.7.4\n)\n"
},
{
"path": "go.sum",
"chars": 2050,
"preview": "github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=\ngithub.com/davecgh/go-spe"
},
{
"path": "intercept.go",
"chars": 3649,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"runtime\"\n\t\"syscall\"\n)\n\nfunc watchProcess(pid int, stdout, stderr, stdin bool) erro"
},
{
"path": "main.go",
"chars": 856,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\n\t\"github.com/spf13/cobra\"\n)\n\nvar cmd = &cobra.Command{\n\tUse: \"siphon [pid]\",\n"
},
{
"path": "registers_386.go",
"chars": 275,
"preview": "//go:build 386\n\npackage main\n\nimport (\n\t\"syscall\"\n)\n\nfunc grabSyscallNo(regs *syscall.PtraceRegs) uint64 {\n\treturn uint6"
},
{
"path": "registers_amd64.go",
"chars": 245,
"preview": "//go:build amd64\n\npackage main\n\nimport (\n\t\"syscall\"\n)\n\nfunc grabSyscallNo(regs *syscall.PtraceRegs) uint64 {\n\treturn reg"
},
{
"path": "registers_arm.go",
"chars": 290,
"preview": "//go:build arm\n\npackage main\n\nimport (\n\t\"syscall\"\n)\n\nfunc grabSyscallNo(regs *syscall.PtraceRegs) uint64 {\n\treturn uint6"
},
{
"path": "registers_arm64.go",
"chars": 256,
"preview": "//go:build arm64\n\npackage main\n\nimport (\n\t\"syscall\"\n)\n\nfunc grabSyscallNo(regs *syscall.PtraceRegs) uint64 {\n\treturn reg"
},
{
"path": "watch_test.go",
"chars": 931,
"preview": "package main\n\nimport (\n\t\"bytes\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify"
}
]
About this extraction
This page contains the full source code of the liamg/siphon GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 15 files (11.8 KB), approximately 4.4k tokens, and a symbol index with 12 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.