Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 38 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,11 @@ ts-ssh -scp source dest
### Core Features
- **SSH Connection**: Just like `ssh`, connect to any host on your Tailnet
- **SCP Transfer**: Simple file transfer with `-scp` flag
- **SOCKS5 Proxy**: `-D` flag for dynamic port forwarding (VSCode Remote SSH compatible)
- **Port Specification**: Use `-p` flag or `host:port` syntax
- **PTY Control**: `-T` flag to disable pseudo-terminal allocation
- **Verbose Mode**: `-v` for debugging and authentication URLs
- **Flexible Usernames**: Supports dots in usernames (e.g., `first.last`)

### No Subcommands
- No complex subcommand structure
Expand Down Expand Up @@ -94,11 +97,16 @@ CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ts-ssh-linux-amd64 .
./ts-ssh -scp file.txt hostname:/tmp/
./ts-ssh -scp hostname:/tmp/file.txt ./downloads/

# SOCKS5 dynamic port forwarding
./ts-ssh -D 1080 hostname # SOCKS proxy on localhost:1080
./ts-ssh -D 0.0.0.0:1080 hostname # Bind to all interfaces (with warning)

# With options
./ts-ssh -v hostname # Verbose mode
./ts-ssh -p 2222 hostname # Custom port
./ts-ssh -l alice hostname # Specify username
./ts-ssh -l alice hostname # Specify username (supports dots: first.last)
./ts-ssh -i ~/.ssh/custom_key hostname # Custom key
./ts-ssh -T hostname "cat /etc/hostname" # Disable PTY allocation

# Get help
./ts-ssh --help
Expand All @@ -124,9 +132,9 @@ The following were removed to simplify the codebase:

```
ts-ssh/
├── main.go # ~457 lines - main CLI logic
├── main.go # ~700 lines - main CLI logic + SOCKS5
├── constants.go # ~52 lines - constants
├── main_test.go # ~256 lines - tests
├── main_test.go # ~850 lines - tests (including SOCKS5 tests)
└── internal/
├── client/
│ ├── scp/ # SCP client implementation
Expand All @@ -138,7 +146,7 @@ ts-ssh/
└── security/ # Security validation
```

**Total**: ~4,656 lines (down from 15,000 - 69% reduction)
**Total**: ~5,250 lines (including SOCKS5 proxy support and comprehensive tests)

## Code Quality Standards

Expand Down Expand Up @@ -219,6 +227,12 @@ go vet ./...
3. Ensure remote path uses colon notation
4. Test with verbose mode

### SOCKS5 Proxy Issues
1. Verify port is available: `netstat -an | grep 1080`
2. Check bind address security warnings
3. Test with verbose mode: `./ts-ssh -v -D 1080 hostname`
4. Verify proxy is listening: `curl --socks5 localhost:1080 http://example.com`

## Development Workflow

### Before Committing
Expand Down Expand Up @@ -260,10 +274,28 @@ This codebase was simplified from a complex multi-modal CLI with:
- Multiple subcommands (connect, list, multi, exec, copy, pick)
- ~15,000 lines of code

The simplification reduced it to ~4,656 lines while maintaining core functionality:
The simplification reduced it to ~5,250 lines while maintaining and extending core functionality:
- SSH connections
- SCP file transfers
- SOCKS5 dynamic port forwarding (new in recent updates)
- PTY allocation control (new in recent updates)
- Flexible username validation (now supports dots)
- Tailscale integration
- Security features
- Security features with comprehensive testing

The old complex code is preserved in `_old_complex/` for reference.

## Recent Changes

### SOCKS5 Dynamic Port Forwarding (PR #33)
- Added `-D [bind_address:]port` flag for SOCKS5 proxy support
- Full SOCKS5 protocol implementation (handshake, IPv4/IPv6/domain names)
- Security validation for bind addresses
- Context-based lifecycle management for graceful shutdown
- Comprehensive test coverage for SOCKS5 functionality
- Compatible with VSCode Remote SSH and other tools requiring SOCKS proxies

### Username Validation Enhancement (PR #34)
- Updated SSH username validation to allow dots (e.g., `first.last`)
- Common Unix username format now fully supported
- Added test cases for dot-containing usernames
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ A streamlined command-line SSH client and SCP utility that connects to your Tail
- **Standard SSH syntax**: Works just like regular `ssh`
- **SCP file transfers**: Simple `-scp` flag for file operations
- **Interactive SSH sessions** with full PTY support
- **SOCKS5 dynamic port forwarding**: `-D` flag for proxy support (VSCode Remote SSH compatible)
- **Secure host key verification** using `~/.ssh/known_hosts`
- **Multiple authentication methods**: SSH keys, password prompts
- **Flexible username support**: Allows dots in usernames (e.g., `first.last`)

### 🛠️ Technical Features
- **Cross-platform**: Linux, macOS (Intel/ARM), Windows, FreeBSD, OpenBSD
Expand Down Expand Up @@ -81,6 +83,9 @@ Usage: ts-ssh [options] [user@]host[:port] [command...]
SSH over Tailscale without requiring a full Tailscale daemon

Options:
-D string
SOCKS5 dynamic port forwarding on [bind_address:]port
-T Disable pseudo-terminal allocation
-control-url string
Tailscale control server URL
-i string
Expand Down Expand Up @@ -170,6 +175,48 @@ ts-ssh -insecure hostname
ts-ssh -control-url https://controlplane.tailscale.com hostname
```

### SOCKS5 Dynamic Port Forwarding

Use the `-D` flag to set up a SOCKS5 proxy for forwarding connections through the SSH tunnel. This is particularly useful for tools like VSCode Remote SSH.

```bash
# Start SOCKS5 proxy on localhost:1080
ts-ssh -D 1080 hostname

# Bind to specific address and port
ts-ssh -D localhost:1080 hostname
ts-ssh -D 127.0.0.1:8080 hostname

# Bind to all interfaces (WARNING: exposes proxy to network)
ts-ssh -D 0.0.0.0:1080 hostname

# Use with verbose mode to see proxy connections
ts-ssh -v -D 1080 hostname

# Combine with other options
ts-ssh -D 1080 -p 2222 user@hostname
```

**Security Notes:**
- Binding to `localhost`, `127.0.0.1`, or `::1` is safe (proxy only accessible locally)
- Binding to `0.0.0.0` or specific network IPs exposes the proxy to your network
- The tool will warn you when binding to non-localhost addresses

### Disable PTY Allocation

Use the `-T` flag to disable pseudo-terminal allocation. Useful for non-interactive commands and automation:

```bash
# Run command without PTY (like ssh -T)
ts-ssh -T hostname "cat /etc/hostname"

# Useful for piping output
ts-ssh -T hostname "journalctl -f" | grep error

# Combine with other flags
ts-ssh -T -p 2222 hostname uptime
```

## Tailscale Authentication

The first time you run `ts-ssh` on a machine, or if its Tailscale authentication expires, it will need to authenticate to your Tailscale network.
Expand Down
6 changes: 3 additions & 3 deletions internal/security/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,10 +188,10 @@ func (iv *InputValidator) ValidateSSHUser(username string) error {
return fmt.Errorf("SSH username too long: %d characters (max %d)", len(username), MaxSSHUserLength)
}

// SSH usernames should only contain alphanumeric characters, hyphens, and underscores
validUserRegex := regexp.MustCompile(`^[a-zA-Z0-9_\-]+$`)
// SSH usernames should only contain alphanumeric characters, hyphens, underscores, and dots
validUserRegex := regexp.MustCompile(`^[a-zA-Z0-9_\-\.]+$`)
if !validUserRegex.MatchString(username) {
return fmt.Errorf("SSH username contains invalid characters (only alphanumeric, hyphen, underscore allowed)")
return fmt.Errorf("SSH username contains invalid characters (only alphanumeric, hyphen, underscore, dot allowed)")
}

// Cannot start with hyphen or number
Expand Down
3 changes: 2 additions & 1 deletion internal/security/validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,8 @@ func TestValidateSSHUser(t *testing.T) {
{"valid_with_hyphen", "user-name", false, ""},
{"valid_with_numbers", "user123", false, ""},
{"valid_mixed", "my_user-123", false, ""},
{"valid_with_dot", "user.name", false, ""},
{"valid_with_multiple_dots", "first.last.name", false, ""},

// Invalid usernames
{"empty_username", "", true, "cannot be empty"},
Expand All @@ -199,7 +201,6 @@ func TestValidateSSHUser(t *testing.T) {
{"starts_with_number", "1user", true, "cannot start with hyphen or number"},
{"invalid_chars_space", "user name", true, "invalid characters"},
{"invalid_chars_special", "user@host", true, "invalid characters"},
{"invalid_chars_dot", "user.name", true, "invalid characters"},
{"invalid_chars_slash", "user/name", true, "invalid characters"},
}

Expand Down
Loading