Remove sudo mechanism from bypass system
The sudo credential broker was never functional and added complexity without value. This removes: - Sudo field from bypass Request structs (broker, client, toolsrv) - Sudo parameter from EvaluateRequest interface and implementations - Sudo/ResetsCounter fields from MetaFile and Variant structs - sudo: true from system_logs.meta variants - All sudo documentation from writing-tools.md, tool-registry.md, core.md, evolution.md, and misc.md Bypass remains fully functional for sandbox escapes. Tools that need elevated privileges should handle that internally or be run manually.
This commit is contained in:
parent
62a7d0d13f
commit
9bb3cd76b8
|
|
@ -177,7 +177,7 @@ func (b *Broker) recordDenial(sessionID string) {
|
|||
// EvaluateRequest evaluates a bypass request from toolsrv and returns the decision.
|
||||
// Execution happens in toolsrv, not here.
|
||||
// Returns (approved, error). If needs user approval, blocks until resolved or timeout.
|
||||
func (b *Broker) EvaluateRequest(sessionID, cmd, cwd string, env map[string]string, sudo bool) (bool, error) {
|
||||
func (b *Broker) EvaluateRequest(sessionID, cmd, cwd string, env map[string]string) (bool, error) {
|
||||
// Validate session
|
||||
if sessionID == "" {
|
||||
return false, fmt.Errorf("no session identity")
|
||||
|
|
@ -206,7 +206,6 @@ func (b *Broker) EvaluateRequest(sessionID, cmd, cwd string, env map[string]stri
|
|||
Cwd: cwd,
|
||||
Env: env,
|
||||
SessionID: sessionID,
|
||||
Sudo: sudo,
|
||||
CreatedAt: time.Now(),
|
||||
resolved: make(chan Resolution, 1),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ func TestBrokerAutoApprove(t *testing.T) {
|
|||
b.GlobalPolicy().AddGlobal(Rule{Cmd: "echo *"})
|
||||
|
||||
// Evaluate a matching command
|
||||
approved, err := b.EvaluateRequest("test-session", "echo hello", os.TempDir(), nil, false)
|
||||
approved, err := b.EvaluateRequest("test-session", "echo hello", os.TempDir(), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
|
@ -44,7 +44,7 @@ func TestBrokerDenyNoSession(t *testing.T) {
|
|||
b := startTestBroker(t)
|
||||
|
||||
// Evaluate with no session ID
|
||||
approved, err := b.EvaluateRequest("", "echo hello", os.TempDir(), nil, false)
|
||||
approved, err := b.EvaluateRequest("", "echo hello", os.TempDir(), nil)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for empty session")
|
||||
}
|
||||
|
|
@ -60,7 +60,7 @@ func TestBrokerDenyInvalidSession(t *testing.T) {
|
|||
b := startTestBroker(t)
|
||||
|
||||
// Evaluate with a session ID that doesn't exist
|
||||
approved, err := b.EvaluateRequest("nonexistent-session", "echo hello", os.TempDir(), nil, false)
|
||||
approved, err := b.EvaluateRequest("nonexistent-session", "echo hello", os.TempDir(), nil)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for invalid session")
|
||||
}
|
||||
|
|
@ -81,7 +81,7 @@ func TestBrokerPendingAndResolve(t *testing.T) {
|
|||
err error
|
||||
})
|
||||
go func() {
|
||||
approved, err := b.EvaluateRequest("test-session", "rm -rf /", os.TempDir(), nil, false)
|
||||
approved, err := b.EvaluateRequest("test-session", "rm -rf /", os.TempDir(), nil)
|
||||
resultCh <- struct {
|
||||
approved bool
|
||||
err error
|
||||
|
|
@ -116,7 +116,7 @@ func TestBrokerApprove(t *testing.T) {
|
|||
err error
|
||||
})
|
||||
go func() {
|
||||
approved, err := b.EvaluateRequest("test-session", "echo approved", os.TempDir(), nil, false)
|
||||
approved, err := b.EvaluateRequest("test-session", "echo approved", os.TempDir(), nil)
|
||||
resultCh <- struct {
|
||||
approved bool
|
||||
err error
|
||||
|
|
@ -149,7 +149,7 @@ func TestBrokerPersist(t *testing.T) {
|
|||
// First request: no policy, goes to pending
|
||||
resultCh := make(chan bool)
|
||||
go func() {
|
||||
approved, _ := b.EvaluateRequest("test-session", "echo persist-me", os.TempDir(), nil, false)
|
||||
approved, _ := b.EvaluateRequest("test-session", "echo persist-me", os.TempDir(), nil)
|
||||
resultCh <- approved
|
||||
}()
|
||||
|
||||
|
|
@ -167,7 +167,7 @@ func TestBrokerPersist(t *testing.T) {
|
|||
}
|
||||
|
||||
// Second request with same command: should auto-approve (no pending)
|
||||
approved, err := b.EvaluateRequest("test-session", "echo persist-me", os.TempDir(), nil, false)
|
||||
approved, err := b.EvaluateRequest("test-session", "echo persist-me", os.TempDir(), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("second request error: %v", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,6 @@ type Request struct {
|
|||
Cwd string
|
||||
Env map[string]string
|
||||
SessionID string
|
||||
Sudo bool
|
||||
CreatedAt time.Time
|
||||
|
||||
// Resolution channel — exactly one value sent when resolved.
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ func (s *Session) StartBypassLoop(broker BypassEvaluator) {
|
|||
|
||||
// BypassEvaluator evaluates bypass requests. Implemented by bypass.Broker.
|
||||
type BypassEvaluator interface {
|
||||
EvaluateRequest(sessionID, cmd, cwd string, env map[string]string, sudo bool) (bool, error)
|
||||
EvaluateRequest(sessionID, cmd, cwd string, env map[string]string) (bool, error)
|
||||
}
|
||||
|
||||
func (s *Session) runBypassLoop(broker BypassEvaluator) {
|
||||
|
|
@ -151,7 +151,7 @@ func (s *Session) runBypassLoop(broker BypassEvaluator) {
|
|||
}
|
||||
|
||||
// Evaluate the request
|
||||
approved, evalErr := broker.EvaluateRequest(s.ID, req.Cmd, req.Cwd, req.Env, req.Sudo)
|
||||
approved, evalErr := broker.EvaluateRequest(s.ID, req.Cmd, req.Cwd, req.Env)
|
||||
|
||||
// Send resolution back to toolsrv
|
||||
resolveConn := s.DialToolServer()
|
||||
|
|
|
|||
|
|
@ -12,11 +12,10 @@ import (
|
|||
|
||||
// Request represents a bypass request.
|
||||
type Request struct {
|
||||
ID string `json:"id"`
|
||||
Cmd string `json:"cmd"`
|
||||
Cwd string `json:"cwd"`
|
||||
Env map[string]string `json:"env,omitempty"`
|
||||
Sudo bool `json:"sudo,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Cmd string `json:"cmd"`
|
||||
Cwd string `json:"cwd"`
|
||||
Env map[string]string `json:"env,omitempty"`
|
||||
|
||||
// Resolution state (not serialized)
|
||||
done chan struct{}
|
||||
|
|
@ -33,13 +32,12 @@ var (
|
|||
// Submit submits a bypass request and blocks until resolved.
|
||||
// Returns (approved, error). If denied, approved is false and error is nil.
|
||||
// If there's an error (e.g., timeout, connection lost), error is non-nil.
|
||||
func Submit(cmd, cwd string, env map[string]string, sudo bool) (bool, error) {
|
||||
func Submit(cmd, cwd string, env map[string]string) (bool, error) {
|
||||
req := &Request{
|
||||
ID: nextID(),
|
||||
Cmd: cmd,
|
||||
Cwd: cwd,
|
||||
Env: env,
|
||||
Sudo: sudo,
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -84,16 +84,6 @@ func ExecuteTool(ctx context.Context, info toolsrv.ToolInfo, args json.RawMessag
|
|||
args, _ = json.Marshal(argMap)
|
||||
}
|
||||
|
||||
// Check if tool requires sudo
|
||||
needsSudo := false
|
||||
if m, err := toolsrv.LoadMetaFile(info.Name); err == nil && m != nil {
|
||||
resolved := m.Resolve()
|
||||
if resolved != nil && resolved.Sudo {
|
||||
needsSudo = true
|
||||
bypassed = true
|
||||
}
|
||||
}
|
||||
|
||||
stdinData := string(args)
|
||||
cwd := cfg.CWD
|
||||
if cwd == "" {
|
||||
|
|
@ -101,12 +91,9 @@ func ExecuteTool(ctx context.Context, info toolsrv.ToolInfo, args json.RawMessag
|
|||
}
|
||||
|
||||
var result string
|
||||
if needsSudo {
|
||||
sudoCode := fmt.Sprintf("cat <<'OLLIE_EOF' | %s\n%s\nOLLIE_EOF", toolPath, stdinData)
|
||||
result, err = executeBypassDirect(ctx, sudoCode, cwd, cfg.Env, timeout, true)
|
||||
} else if bypassed {
|
||||
if bypassed {
|
||||
bypassCode := fmt.Sprintf("cat <<'OLLIE_EOF' | %s\n%s\nOLLIE_EOF", toolPath, stdinData)
|
||||
result, err = executeBypassDirect(ctx, bypassCode, cwd, cfg.Env, timeout, false)
|
||||
result, err = executeBypassDirect(ctx, bypassCode, cwd, cfg.Env, timeout)
|
||||
} else {
|
||||
result, err = executeSandboxed(ctx, toolPath, stdinData, cwd, cfg.Env, timeout, cfg.Yolo, cfg.Output, cfg.Started)
|
||||
}
|
||||
|
|
@ -240,7 +227,7 @@ func executeSandboxed(ctx context.Context, toolPath, stdinData, cwd string, envE
|
|||
|
||||
// executeBypassDirect requests bypass approval and executes the command directly.
|
||||
// The approval comes from olliesrv via the 9P bypass/pending and bypass/resolve files.
|
||||
func executeBypassDirect(ctx context.Context, cmd, cwd string, envExtra map[string]string, timeout int, sudo bool) (string, error) {
|
||||
func executeBypassDirect(ctx context.Context, cmd, cwd string, envExtra map[string]string, timeout int) (string, error) {
|
||||
// Build environment map
|
||||
envMap := make(map[string]string)
|
||||
for _, kv := range os.Environ() {
|
||||
|
|
@ -253,7 +240,7 @@ func executeBypassDirect(ctx context.Context, cmd, cwd string, envExtra map[stri
|
|||
}
|
||||
|
||||
// Submit bypass request and wait for approval
|
||||
approved, err := bypass.Submit(cmd, cwd, envMap, sudo)
|
||||
approved, err := bypass.Submit(cmd, cwd, envMap)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("bypass request failed: %w", err)
|
||||
}
|
||||
|
|
@ -268,17 +255,12 @@ func executeBypassDirect(ctx context.Context, cmd, cwd string, envExtra map[stri
|
|||
defer cancel()
|
||||
}
|
||||
|
||||
return executeDirectUnsandboxed(ctx, cmd, cwd, envMap, sudo)
|
||||
return executeDirectUnsandboxed(ctx, cmd, cwd, envMap)
|
||||
}
|
||||
|
||||
// executeDirectUnsandboxed runs a command without any sandbox.
|
||||
func executeDirectUnsandboxed(ctx context.Context, cmd, cwd string, env map[string]string, sudo bool) (string, error) {
|
||||
var execCmd *osExec.Cmd
|
||||
if sudo {
|
||||
execCmd = osExec.CommandContext(ctx, "sudo", "-E", "bash", "-c", cmd)
|
||||
} else {
|
||||
execCmd = osExec.CommandContext(ctx, "bash", "-c", cmd)
|
||||
}
|
||||
func executeDirectUnsandboxed(ctx context.Context, cmd, cwd string, env map[string]string) (string, error) {
|
||||
execCmd := osExec.CommandContext(ctx, "bash", "-c", cmd)
|
||||
execCmd.Dir = cwd
|
||||
if len(env) > 0 {
|
||||
execCmd.Env = make([]string, 0, len(env))
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ func Spec(srv *Server) virtfs.FsNodeDecl {
|
|||
),
|
||||
virtfs.DirNode("bypass",
|
||||
virtfs.FileNode("pending", 0444,
|
||||
virtfs.Doc("Blocks until bypass request; returns JSON {id, cmd, cwd, env, sudo}"),
|
||||
virtfs.Doc("Blocks until bypass request; returns JSON {id, cmd, cwd, env}"),
|
||||
virtfs.BlockOnce(func(_ context.Context, _ string) ([]byte, string, error) {
|
||||
req := bypass.NextPending()
|
||||
data, err := json.Marshal(req)
|
||||
|
|
|
|||
|
|
@ -16,8 +16,7 @@
|
|||
"grep": {"type": "string", "description": "Filter output by pattern"}
|
||||
}
|
||||
},
|
||||
"cmd": "system_logs_journald",
|
||||
"sudo": true
|
||||
"cmd": "system_logs_journald"
|
||||
},
|
||||
{
|
||||
"match": {"binary": "dmesg"},
|
||||
|
|
@ -31,8 +30,7 @@
|
|||
"grep": {"type": "string", "description": "Filter output by pattern"}
|
||||
}
|
||||
},
|
||||
"cmd": "system_logs_dmesg",
|
||||
"sudo": true
|
||||
"cmd": "system_logs_dmesg"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -426,7 +426,7 @@ Runs a single bash command in a sandbox.
|
|||
|
||||
Before execution, inline code is checked against dangerous patterns:
|
||||
|
||||
- **Universal**: `mkfs`, `dd if=/dev/`, `sudo/su`, `/etc/shadow`
|
||||
- **Universal**: `mkfs`, `dd if=/dev/`, `/etc/shadow`
|
||||
- **Bash**: recursive force-delete of system paths, fork bombs, `/dev/sd` writes, eval injection
|
||||
- **Python**: `shutil.rmtree("/")`, subprocess rm -rf, os.remove of system paths
|
||||
- **Perl**: system/exec rm -rf, backtick rm, unlink system paths
|
||||
|
|
|
|||
|
|
@ -347,13 +347,6 @@ executable. Enables one `.meta` to work across heterogeneous hosts — the
|
|||
contract adapts to capabilities. Critical for `ollie-remote` deployments where
|
||||
the remote host may have different tools, package managers, or init systems.
|
||||
|
||||
### Sudo credential broker
|
||||
Tools declare `"sudo": true` in their `.meta` to require root privileges.
|
||||
Dispatch becomes a two-gate chain: bypass approval → credential prompt → `sudo -S`.
|
||||
Credentials are requested via `kdialog`/`zenity` on the local desktop, or forwarded
|
||||
over SSH for remote execution. The tool itself has no knowledge of sudo — the
|
||||
privilege wrapping is entirely in the dispatch layer.
|
||||
|
||||
## Principles That Emerged
|
||||
1. **Filesystem-as-API** — everything is read/write on synthetic files. No
|
||||
custom protocols needed.
|
||||
|
|
@ -987,7 +980,7 @@ Agent ctl commands: `stop`, `compact`, `clear`, `inject`, `agent`, `model`, `mod
|
|||
|
||||
### elevate → bypass
|
||||
|
||||
The sandbox escape mechanism was renamed from `elevate` to `bypass` throughout (package, namespace, env vars, tool args, all docs). Eliminates confusion with sudo.
|
||||
The sandbox escape mechanism was renamed from `elevate` to `bypass` throughout (package, namespace, env vars, tool args, all docs).
|
||||
|
||||
### Streaming Fix
|
||||
|
||||
|
|
|
|||
|
|
@ -55,7 +55,6 @@ Each tool has a `.meta` JSON sidecar file alongside the executable:
|
|||
| `tier` | Result cacheability: `cold`, `warm`, or `hot` (default: hot) |
|
||||
| `readOnly` | Safe for parallel execution with other read tools |
|
||||
| `cmd` | Executable path or name override |
|
||||
| `sudo` | Requires root privileges; implies bypass |
|
||||
| `variants` | Conditional definitions for heterogeneous hosts |
|
||||
|
||||
### Runtime access
|
||||
|
|
|
|||
|
|
@ -56,27 +56,6 @@ The `.meta` file (`file_glob.meta`):
|
|||
Everything is sandboxed by default via Landlock. Tools that need to escape
|
||||
request bypass explicitly (`"bypass": true`).
|
||||
|
||||
### Privileged tools: `sudo`
|
||||
|
||||
Tools that need root privileges declare `"sudo": true` in their `.meta`. The
|
||||
dispatch chain becomes a two-gate sequence: bypass approval (escape sandbox)
|
||||
→ credential prompt → `sudo -S` execution. The tool itself has no knowledge of
|
||||
sudo — the privilege wrapping is entirely in the dispatch layer.
|
||||
|
||||
```json
|
||||
{
|
||||
"description": "Read system logs (dmesg).",
|
||||
"sudo": true,
|
||||
"cmd": "system_logs_dmesg",
|
||||
"args": {"type":"object","properties":{"lines":{"type":"string"}}},
|
||||
"readOnly": true
|
||||
}
|
||||
```
|
||||
|
||||
Credentials are prompted via `kdialog`/`zenity` on desktop, or forwarded over
|
||||
SSH for remote execution. See [`doc/writing-tools.md`](doc/writing-tools.md) for
|
||||
details.
|
||||
|
||||
### Host-conditional variants
|
||||
|
||||
A single `.meta` file can describe a tool that works differently on different
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ tool, you don't edit config files, you don't restart a server. You write a
|
|||
- **What** the tool does (description, prompt, args schema)
|
||||
- **Where** to find it (cmd, co-located executable, or PATH)
|
||||
- **When** it's available (match conditions per variant)
|
||||
- **What privileges** it needs (sudo)
|
||||
|
||||
The registry reads the `.meta` file, presents the tool to the model, and
|
||||
executes the binary when called. The model sees a typed function. The runtime
|
||||
|
|
@ -60,7 +59,6 @@ reads only `.meta` files — it never inspects the executable itself.
|
|||
| `tier` | `"hot"\|"warm"\|"cold"` | Context retention tier (default: hot) |
|
||||
| `readOnly` | bool | Safe for parallel execution with other read tools |
|
||||
| `cmd` | string | Executable path or name (see Resolution below) |
|
||||
| `sudo` | bool | Requires root privileges; implies bypass |
|
||||
| `variants` | array | Conditional definitions for heterogeneous hosts (see Variants below) |
|
||||
|
||||
## Executable resolution
|
||||
|
|
@ -248,68 +246,6 @@ Landlock but still as the current user.
|
|||
}
|
||||
```
|
||||
|
||||
### Sudo: run as root
|
||||
|
||||
Tools that need root privileges declare `"sudo": true` in their `.meta`.
|
||||
This **implies bypass** — can't sudo inside a sandbox. The dispatch chain
|
||||
becomes two sequential gates:
|
||||
|
||||
```
|
||||
tool call → bypass gate → credential gate → sudo -S tool
|
||||
```
|
||||
|
||||
1. **Elevation gate** — broker asks: "approve escaping the sandbox?" User
|
||||
approves or denies. If denied, the credential gate is never reached.
|
||||
2. **Credential gate** — broker prompts for sudo password (via `kdialog`,
|
||||
`zenity`, or terminal). The password is sent over an encrypted channel
|
||||
(SSH for remote) and piped to `sudo -S`.
|
||||
3. **Execution** — tool runs as root, output streams back to the agent.
|
||||
|
||||
The tool itself has **no knowledge of sudo**. It reads JSON from stdin and
|
||||
writes to stdout as always. The privilege wrapping is entirely in the dispatch
|
||||
layer.
|
||||
|
||||
```json
|
||||
{
|
||||
"description": "Read system logs (dmesg).",
|
||||
"sudo": true,
|
||||
"cmd": "system_logs_dmesg",
|
||||
"args": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"lines": {"type": "string", "description": "Number of lines"},
|
||||
"source": {"type": "string", "description": "Log source"}
|
||||
}
|
||||
},
|
||||
"readOnly": true
|
||||
}
|
||||
```
|
||||
|
||||
### Network transparency for privileges
|
||||
|
||||
The same `.meta` works on any host:
|
||||
- **Local desktop**: kdialog or zenity prompts for password
|
||||
- **Remote (ollie-remote)**: credential request travels back over the
|
||||
encrypted SSH tunnel to the local broker, which prompts the user
|
||||
- **Headless**: terminal-based fallback (or `sudo -n` if NOPASSWD is
|
||||
configured)
|
||||
|
||||
No assumptions about GUI availability, init system, or sudoers configuration.
|
||||
The tool adapts to what's available.
|
||||
|
||||
### Example: full privilege chain
|
||||
|
||||
```bash
|
||||
# system_logs calls dmesg with sudo
|
||||
system_logs(source="dmesg", lines="10")
|
||||
|
||||
# Behind the scenes:
|
||||
# 1. Elevation approved → "escape sandbox"
|
||||
# 2. kdialog prompts for sudo password → "credentials accepted"
|
||||
# 3. sudo -S dmesg --time-format iso | tail -10
|
||||
# 4. Output streams to agent
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Tools receive:
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -0,0 +1,125 @@
|
|||
// Client: 9P client that authenticates via Tauth
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"9fans.net/go/plan9"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
fmt.Println("Usage: client <secret>")
|
||||
fmt.Println(" client new (generate new secret)")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
secret := os.Args[1]
|
||||
if secret == "new" {
|
||||
b := make([]byte, 16)
|
||||
rand.Read(b)
|
||||
secret = hex.EncodeToString(b)
|
||||
fmt.Printf("Generated secret: %s\n", secret)
|
||||
}
|
||||
|
||||
sockPath := "/tmp/auth9p-test.sock"
|
||||
|
||||
conn, err := net.Dial("unix", sockPath)
|
||||
if err != nil {
|
||||
log.Fatalf("dial: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
fmt.Println("Connected to server")
|
||||
|
||||
// Tversion
|
||||
tag := uint16(1)
|
||||
if err := plan9.WriteFcall(conn, &plan9.Fcall{
|
||||
Type: plan9.Tversion,
|
||||
Tag: plan9.NOTAG,
|
||||
Msize: 8192,
|
||||
Version: "9P2000",
|
||||
}); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
resp, _ := plan9.ReadFcall(conn)
|
||||
fmt.Printf("Version: %v\n", resp)
|
||||
|
||||
// Tauth
|
||||
afid := uint32(1)
|
||||
if err := plan9.WriteFcall(conn, &plan9.Fcall{
|
||||
Type: plan9.Tauth,
|
||||
Tag: tag,
|
||||
Afid: afid,
|
||||
Uname: "agent",
|
||||
Aname: "",
|
||||
}); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
tag++
|
||||
resp, _ = plan9.ReadFcall(conn)
|
||||
fmt.Printf("Auth: %v\n", resp)
|
||||
if resp.Type == plan9.Rerror {
|
||||
log.Fatalf("Tauth failed: %s", resp.Ename)
|
||||
}
|
||||
|
||||
// Write secret to auth fid
|
||||
if err := plan9.WriteFcall(conn, &plan9.Fcall{
|
||||
Type: plan9.Twrite,
|
||||
Tag: tag,
|
||||
Fid: afid,
|
||||
Data: []byte(secret),
|
||||
Count: uint32(len(secret)),
|
||||
}); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
tag++
|
||||
resp, _ = plan9.ReadFcall(conn)
|
||||
fmt.Printf("Write secret: %v\n", resp)
|
||||
if resp.Type == plan9.Rerror {
|
||||
log.Fatalf("Auth failed: %s", resp.Ename)
|
||||
}
|
||||
|
||||
// Read token from auth fid
|
||||
if err := plan9.WriteFcall(conn, &plan9.Fcall{
|
||||
Type: plan9.Tread,
|
||||
Tag: tag,
|
||||
Fid: afid,
|
||||
Offset: 0,
|
||||
Count: 1024,
|
||||
}); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
tag++
|
||||
resp, _ = plan9.ReadFcall(conn)
|
||||
fmt.Printf("Read token: %v\n", resp)
|
||||
token := string(resp.Data)
|
||||
fmt.Printf("Session token: %s", token)
|
||||
|
||||
// Tattach
|
||||
fid := uint32(2)
|
||||
if err := plan9.WriteFcall(conn, &plan9.Fcall{
|
||||
Type: plan9.Tattach,
|
||||
Tag: tag,
|
||||
Fid: fid,
|
||||
Afid: afid,
|
||||
Uname: "agent",
|
||||
Aname: "",
|
||||
}); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
tag++
|
||||
resp, _ = plan9.ReadFcall(conn)
|
||||
fmt.Printf("Attach: %v\n", resp)
|
||||
if resp.Type == plan9.Rerror {
|
||||
log.Fatalf("Attach failed: %s", resp.Ename)
|
||||
}
|
||||
|
||||
fmt.Println("\n✓ Successfully authenticated and attached!")
|
||||
fmt.Printf("Use this secret to reconnect: %s\n", secret)
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
module auth9p
|
||||
|
||||
go 1.23
|
||||
|
||||
require 9fans.net/go v0.0.5
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
9fans.net/go v0.0.5 h1:u0H3Et5NowdVm56FDD+aWcDOao9zEo+zolNFsr8qyQk=
|
||||
9fans.net/go v0.0.5/go.mod h1:Rxvbbc1e+1TyGMjAvLthGTyO97t+6JMQ6ly+Lcs9Uf0=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
|
||||
golang.org/x/exp v0.0.0-20210405174845-4513512abef3/go.mod h1:I6l2HNBLBZEcrOoCpyKLdY2lHoRZ8lI4x60KMCQDft4=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20201217150744-e6ae53a27f4f/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4=
|
||||
golang.org/x/mobile v0.0.0-20210220033013-bdb1ca9a1e08/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210415045647-66c3f260301c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
Binary file not shown.
|
|
@ -0,0 +1,216 @@
|
|||
// Server: 9P server with Tauth-based authentication
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"9fans.net/go/plan9"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
mu sync.Mutex
|
||||
secret string // set on first auth, verified on subsequent
|
||||
token string // session token returned after auth
|
||||
}
|
||||
|
||||
func main() {
|
||||
sockPath := "/tmp/auth9p-test.sock"
|
||||
os.Remove(sockPath)
|
||||
|
||||
ln, err := net.Listen("unix", sockPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer ln.Close()
|
||||
defer os.Remove(sockPath)
|
||||
|
||||
fmt.Printf("Server listening on %s\n", sockPath)
|
||||
fmt.Println("Waiting for connections...")
|
||||
|
||||
srv := &Server{}
|
||||
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
log.Printf("accept: %v", err)
|
||||
continue
|
||||
}
|
||||
go srv.handle(conn)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handle(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
fmt.Printf("New connection from %v\n", conn.RemoteAddr())
|
||||
|
||||
// Track fids
|
||||
fids := make(map[uint32]*Fid)
|
||||
authFids := make(map[uint32]*AuthFid)
|
||||
|
||||
for {
|
||||
// Read 9P message
|
||||
fc, err := plan9.ReadFcall(conn)
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
log.Printf("read: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("← %v\n", fc)
|
||||
|
||||
var resp *plan9.Fcall
|
||||
switch fc.Type {
|
||||
case plan9.Tversion:
|
||||
resp = &plan9.Fcall{
|
||||
Type: plan9.Rversion,
|
||||
Tag: fc.Tag,
|
||||
Msize: fc.Msize,
|
||||
Version: "9P2000",
|
||||
}
|
||||
|
||||
case plan9.Tauth:
|
||||
// Create auth fid
|
||||
authFids[fc.Afid] = &AuthFid{
|
||||
uname: fc.Uname,
|
||||
aname: fc.Aname,
|
||||
}
|
||||
resp = &plan9.Fcall{
|
||||
Type: plan9.Rauth,
|
||||
Tag: fc.Tag,
|
||||
Qid: plan9.Qid{Type: plan9.QTAUTH, Vers: 0, Path: uint64(fc.Afid)},
|
||||
}
|
||||
|
||||
case plan9.Tattach:
|
||||
// Check auth
|
||||
af, ok := authFids[fc.Afid]
|
||||
if !ok || !af.authenticated {
|
||||
resp = errorResp(fc.Tag, "not authenticated")
|
||||
break
|
||||
}
|
||||
fids[fc.Fid] = &Fid{path: "/"}
|
||||
resp = &plan9.Fcall{
|
||||
Type: plan9.Rattach,
|
||||
Tag: fc.Tag,
|
||||
Qid: plan9.Qid{Type: plan9.QTDIR, Vers: 0, Path: 0},
|
||||
}
|
||||
fmt.Printf("✓ Client attached successfully (token: %s)\n", af.token)
|
||||
|
||||
case plan9.Tread:
|
||||
// Reading from auth fid returns the token
|
||||
if af, ok := authFids[fc.Fid]; ok {
|
||||
if !af.authenticated {
|
||||
resp = errorResp(fc.Tag, "write secret first")
|
||||
} else {
|
||||
data := []byte(af.token + "\n")
|
||||
resp = &plan9.Fcall{
|
||||
Type: plan9.Rread,
|
||||
Tag: fc.Tag,
|
||||
Data: data,
|
||||
Count: uint32(len(data)),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
resp = &plan9.Fcall{
|
||||
Type: plan9.Rread,
|
||||
Tag: fc.Tag,
|
||||
Data: []byte("hello from server\n"),
|
||||
Count: 18,
|
||||
}
|
||||
}
|
||||
|
||||
case plan9.Twrite:
|
||||
// Writing to auth fid sets/verifies secret
|
||||
if af, ok := authFids[fc.Fid]; ok {
|
||||
clientSecret := string(fc.Data)
|
||||
resp = s.handleAuthWrite(fc.Tag, af, clientSecret)
|
||||
} else {
|
||||
resp = &plan9.Fcall{
|
||||
Type: plan9.Rwrite,
|
||||
Tag: fc.Tag,
|
||||
Count: fc.Count,
|
||||
}
|
||||
}
|
||||
|
||||
case plan9.Tclunk:
|
||||
delete(fids, fc.Fid)
|
||||
delete(authFids, fc.Fid)
|
||||
resp = &plan9.Fcall{Type: plan9.Rclunk, Tag: fc.Tag}
|
||||
|
||||
default:
|
||||
resp = errorResp(fc.Tag, fmt.Sprintf("unhandled message type %d", fc.Type))
|
||||
}
|
||||
|
||||
fmt.Printf("→ %v\n", resp)
|
||||
if err := plan9.WriteFcall(conn, resp); err != nil {
|
||||
log.Printf("write: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handleAuthWrite(tag uint16, af *AuthFid, clientSecret string) *plan9.Fcall {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.secret == "" {
|
||||
// First auth - set the secret
|
||||
s.secret = clientSecret
|
||||
s.token = randomToken()
|
||||
af.authenticated = true
|
||||
af.token = s.token
|
||||
fmt.Printf("✓ Secret established (first connection)\n")
|
||||
return &plan9.Fcall{
|
||||
Type: plan9.Rwrite,
|
||||
Tag: tag,
|
||||
Count: uint32(len(clientSecret)),
|
||||
}
|
||||
}
|
||||
|
||||
// Subsequent auth - verify secret
|
||||
if clientSecret != s.secret {
|
||||
fmt.Printf("✗ Secret mismatch!\n")
|
||||
return errorResp(tag, "authentication failed")
|
||||
}
|
||||
|
||||
af.authenticated = true
|
||||
af.token = s.token
|
||||
fmt.Printf("✓ Secret verified (reconnection)\n")
|
||||
return &plan9.Fcall{
|
||||
Type: plan9.Rwrite,
|
||||
Tag: tag,
|
||||
Count: uint32(len(clientSecret)),
|
||||
}
|
||||
}
|
||||
|
||||
type Fid struct {
|
||||
path string
|
||||
}
|
||||
|
||||
type AuthFid struct {
|
||||
uname string
|
||||
aname string
|
||||
authenticated bool
|
||||
token string
|
||||
}
|
||||
|
||||
func errorResp(tag uint16, msg string) *plan9.Fcall {
|
||||
return &plan9.Fcall{
|
||||
Type: plan9.Rerror,
|
||||
Tag: tag,
|
||||
Ename: msg,
|
||||
}
|
||||
}
|
||||
|
||||
func randomToken() string {
|
||||
b := make([]byte, 16)
|
||||
rand.Read(b)
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
rm -f /tmp/auth9p-test.sock
|
||||
|
||||
echo "Starting server..."
|
||||
./server > /tmp/server.log 2>&1 &
|
||||
SERVER_PID=$!
|
||||
sleep 0.5
|
||||
|
||||
if ! kill -0 $SERVER_PID 2>/dev/null; then
|
||||
echo "Server failed to start:"
|
||||
cat /tmp/server.log
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cleanup() {
|
||||
kill $SERVER_PID 2>/dev/null || true
|
||||
rm -f /tmp/auth9p-test.sock
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
echo ""
|
||||
echo "=== Test 1: First client establishes secret ==="
|
||||
./client new | tee /tmp/client1.log
|
||||
SECRET=$(grep "Use this secret" /tmp/client1.log | awk '{print $NF}')
|
||||
echo "Captured secret: $SECRET"
|
||||
|
||||
echo ""
|
||||
echo "=== Test 2: Reconnect with same secret ==="
|
||||
./client "$SECRET"
|
||||
|
||||
echo ""
|
||||
echo "=== Test 3: Wrong secret (should fail) ==="
|
||||
./client wrongsecret 2>&1 || echo "(expected failure)"
|
||||
|
||||
echo ""
|
||||
echo "=== Server log ==="
|
||||
cat /tmp/server.log
|
||||
|
|
@ -526,11 +526,10 @@ func escapeValue(s string) string {
|
|||
|
||||
// BypassRequest represents a bypass request from toolsrv.
|
||||
type BypassRequest struct {
|
||||
ID string `json:"id"`
|
||||
Cmd string `json:"cmd"`
|
||||
Cwd string `json:"cwd"`
|
||||
Env map[string]string `json:"env,omitempty"`
|
||||
Sudo bool `json:"sudo,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Cmd string `json:"cmd"`
|
||||
Cwd string `json:"cwd"`
|
||||
Env map[string]string `json:"env,omitempty"`
|
||||
}
|
||||
|
||||
// ReadBypassPending blocks until a bypass request is available.
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ type MetaFile struct {
|
|||
Scope string `json:"scope,omitempty"`
|
||||
OutputFormat string `json:"outputFormat,omitempty"`
|
||||
Cmd string `json:"cmd,omitempty"`
|
||||
Sudo bool `json:"sudo,omitempty"`
|
||||
ResetsCounter bool `json:"resetsCounter,omitempty"`
|
||||
Variants []Variant `json:"variants,omitempty"`
|
||||
}
|
||||
|
|
@ -35,7 +34,6 @@ type Variant struct {
|
|||
Scope string `json:"scope,omitempty"`
|
||||
OutputFormat string `json:"outputFormat,omitempty"`
|
||||
Cmd string `json:"cmd,omitempty"`
|
||||
Sudo bool `json:"sudo,omitempty"`
|
||||
ResetsCounter bool `json:"resetsCounter,omitempty"`
|
||||
}
|
||||
|
||||
|
|
@ -87,9 +85,6 @@ func (m *MetaFile) Resolve() *MetaFile {
|
|||
if v.Scope != "" {
|
||||
resolved.Scope = v.Scope
|
||||
}
|
||||
if v.Sudo {
|
||||
resolved.Sudo = true
|
||||
}
|
||||
if v.ResetsCounter {
|
||||
resolved.ResetsCounter = true
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue