mirror of
https://github.com/quarkslab/proxyblob
synced 2026-06-08 16:52:59 +00:00
proxyblob v2 release
This commit is contained in:
@@ -8,6 +8,8 @@ config.json
|
||||
*-config.json
|
||||
.DS_Store
|
||||
.cursor/
|
||||
CLAUDE.md
|
||||
AGENTS.md
|
||||
*.txt
|
||||
__blobstorage__/
|
||||
__azurite*.json
|
||||
@@ -1,30 +1,32 @@
|
||||
# ProxyBlob
|
||||
# ProxyBlob v2
|
||||
|
||||
🎉 New release ! _see [CHANGELOG](#changelog)_ 🎉
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/logo.png" alt="ProxyBlob logo" width="300"/>
|
||||
<img src="docs/proxyblob-v2.png" alt="ProxyBlob v2 logo" width="300"/>
|
||||
</p>
|
||||
<p align="center"><i>SOCKS proxy over Azure Storage Blob service</i></p>
|
||||
<p align="center"><i>SOCKS proxy over Azure Storage service</i></p>
|
||||
|
||||
## Overview
|
||||
|
||||
ProxyBlob is a tool designed to create SOCKS proxy tunnels through Azure Blob Storage service. This is particularly useful in environments where direct network connectivity is restricted but `*.blob.core.windows.net` is accessible.
|
||||
ProxyBlob is a tool designed to create SOCKS proxy tunnels through Azure Storage services. This is particularly useful in environments where direct network connectivity is restricted but `*.core.windows.net` is accessible.
|
||||
|
||||
The system consists of two components:
|
||||
|
||||
1. **Proxy Server**: Runs on your local machine and provides a SOCKS interface for your applications
|
||||
2. **Agent**: Runs inside the target network and communicates with the proxy through Azure Blob Storage
|
||||
1. **Proxy Server**: Runs on your local machine or a remote server and provides a SOCKS interface for your applications
|
||||
2. **Agent**: Runs inside the target network and communicates with the proxy through Azure Storage services
|
||||
|
||||
## Features
|
||||
|
||||
- SOCKS5 protocol (CONNECT and UDP ASSOCIATE)
|
||||
- Communication through Azure Blob Storage
|
||||
- Communication through Azure Storage services (thanks to [aznet](https://github.com/atsika/aznet)!)
|
||||
- Interactive CLI with auto-completion
|
||||
- Multiple agent management
|
||||
- Local proxy server
|
||||
- Local or remote proxy server
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Go 1.23 or higher
|
||||
- Go 1.25 or higher
|
||||
- An Azure Storage Account
|
||||
|
||||
### Storage Account
|
||||
@@ -108,7 +110,7 @@ Press on the `[Azurite Blob Service]` and the service should be running.
|
||||
|
||||
For Docker, you will have to pull the image and run it:
|
||||
|
||||
```
|
||||
```bash
|
||||
docker pull mcr.microsoft.com/azure-storage/azurite
|
||||
docker run -p 10000:10000 mcr.microsoft.com/azure-storage/azurite
|
||||
```
|
||||
@@ -137,12 +139,28 @@ Create a `config.json` file based on the [example](example_config.json) with you
|
||||
|
||||
```json
|
||||
{
|
||||
"storage_url": "http://localhost:10000/", // omit if using Azure
|
||||
"storage_account_name": "your-storage-account-name",
|
||||
"storage_account_key": "your-storage-account-key"
|
||||
"listeners": [
|
||||
{
|
||||
"name": "blob-listener",
|
||||
"driver": "azblob",
|
||||
"address": "https://proxyblob.blob.core.windows.net",
|
||||
"storage_account": "proxyblob",
|
||||
"storage_account_key": "your_account_key"
|
||||
},
|
||||
{
|
||||
"name": "queue-listener",
|
||||
"driver": "azqueue",
|
||||
"address": "http://127.0.0.1:10001",
|
||||
"storage_account": "devstoreaccount1",
|
||||
"storage_account_key": "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=="
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
You can configure multiple listeners and manage them in the proxy CLI.
|
||||
|
||||
## Usage
|
||||
|
||||
### Starting the Proxy Server
|
||||
@@ -155,43 +173,46 @@ This will start an interactive CLI with the following commands:
|
||||
|
||||
```
|
||||
Commands:
|
||||
clear clear the screen
|
||||
create, new create a new agent container and generate its connection string
|
||||
delete, rm delete an existing agent container
|
||||
exit exit the shell
|
||||
help use 'help [command]' for command help
|
||||
list, ls list all existing agent containers
|
||||
select, use select an agent for subsequent commands
|
||||
start, proxy start SOCKS proxy server
|
||||
stop stop running proxy for the selected agent
|
||||
agent manage agents
|
||||
clear clear the screen
|
||||
exit exit the shell
|
||||
help use 'help [command]' for command help
|
||||
listener manage listeners
|
||||
new generate a new connection string for an agent
|
||||
```
|
||||
|
||||
Once the proxy server is running, you can create a new agent container by using the `create` command.
|
||||
Once the proxy server is running, start a listener. The started listener automatically becomes the default for subsequent commands.
|
||||
|
||||
```
|
||||
proxyblob » create
|
||||
16:28:37 INF Agent container created successfully container_id=5f5250e9-5518-4682-90ea-f61abf797654
|
||||
16:28:37 INF Connection string generated connection_string=aHR0cDovL2xvY2FsaG9zdDoxMDAwMC9kZXZzdG9yZWFjY291bnQxLzVmNTI1MGU5LTU1MTgtNDY4Mi05MGVhLWY2MWFiZjc5NzY1ND9zZT0yMDI1LTA0LTI0VDE0JTNBMjglM0EzN1omc2lnPXpjNUNVYVZKJTJGS1duY3RtbnlNZ0clMkZZNkNrRzZHYXJzMXRFTXkxR0ZiTVVZJTNEJnNwPXJ3JnNwcj1odHRwcyUyQ2h0dHAmc3I9YyZzdD0yMDI1LTA0LTE3VDE0JTNBMjMlM0EzN1omc3Y9MjAyMC0xMC0wMg
|
||||
proxyblob » listener start blob-local-listener
|
||||
21:23:25 INF Aznet listener started and set as default addr=http://127.0.0.1:10000 driver=azblob listener_id=blob-local-listener
|
||||
```
|
||||
|
||||
Use the connection string generated with the agent (see below [Starting the Agent](#starting-the-agent)). If the agent connects successfully, you should see the “Agent Info” column filled in when you list the agents with the `list` command.
|
||||
Then you can generate a connection string by using the `new` command.
|
||||
|
||||
```
|
||||
proxyblob » list
|
||||
╭──────────────────────────────────────┬────────────┬────────────┬─────────────────────┬─────────────────────╮
|
||||
│ CONTAINER ID │ AGENT INFO │ PROXY PORT │ FIRST SEEN │ LAST SEEN │
|
||||
├──────────────────────────────────────┼────────────┼────────────┼─────────────────────┼─────────────────────┤
|
||||
│ 5f5250e9-5518-4682-90ea-f61abf797654 │ atsika@qb │ │ 2025-04-17 14:28:37 │ 2025-04-17 14:28:37 │
|
||||
╰──────────────────────────────────────┴────────────┴────────────┴─────────────────────┴─────────────────────╯
|
||||
proxyblob » new
|
||||
21:25:10 INF Connection string generated connection_string=YXpibG9ifGh0dHA6Ly8xMjcuMC4wLjE6MTAwMDAvZGV2c3RvcmVhY2NvdW50MT9oYW5kc2hha2U9YzJVOU1qQXlOaTB3TWkweE5sUXlNQ1V6UVRJMUpUTkJNVEJhSm5OcFp6MDRlR3ByVjJFeVNXdHhWWEJxUlU1cGJIVmtOVGhuWXpkNU5raDNXWEZNUmpZNVFrNDVaMUphT1ZNMEpUTkVKbk53UFdGamR5WnpjSEk5YUhSMGNITWxNa05vZEhSd0puTnlQV01tYzNROU1qQXlOaTB3TWkweE5WUXlNQ1V6UVRJd0pUTkJNVEJhSm5OMlBUSXdNalV0TVRFdE1EVSUzRCZ0b2tlbj1jMlU5TWpBeU5pMHdNaTB4TmxReU1DVXpRVEkxSlROQk1UQmFKbk5wWnowM2NXZ3dZVWgxV1dKTE1rcGlOVVYyVG5WTk1XdEpTMFJFVTFkVGNFWlllVXd5ZUV0dWRYaG9jVWc0SlRORUpuTndQWEpzSm5Od2NqMW9kSFJ3Y3lVeVEyaDBkSEFtYzNJOVl5WnpkRDB5TURJMkxUQXlMVEUxVkRJd0pUTkJNakFsTTBFeE1Gb21jM1k5TWpBeU5TMHhNUzB3TlElM0QlM0Q listener_id=blob-local-listener
|
||||
```
|
||||
|
||||
Select the agent using `select <container id>` and start the proxy listener (by default it listens on localhost:1080) by using the `start` command.
|
||||
Use the generated connection string with the agent (see below [Starting the Agent](#starting-the-agent)). If the agent connects successfully, you should see its identity (`user@host`) in the "Info" column when you list agents.
|
||||
|
||||
```
|
||||
atsika@qb » select 5f5250e9-5518-4682-90ea-f61abf797654
|
||||
17:17:51 INF Agent selected agent=atsika@qb
|
||||
atsika@qb » start
|
||||
17:17:58 INF Proxy started successfully agent=atsika@qb port=1080
|
||||
proxyblob » agent ls
|
||||
╭──────────────────────────────────────┬─────────────────┬──────────────────────┬────────────┬─────────────────────┬───────────╮
|
||||
│ AGENT ID │ INFO │ LISTENER │ PROXY PORT │ CONNECTED AT │ LAST SEEN │
|
||||
├──────────────────────────────────────┼─────────────────┼──────────────────────┼────────────┼─────────────────────┼───────────┤
|
||||
│ 7b5af883-7cc7-45a4-8599-da906753005d │ atsika@mac.home │ blob-local-listener │ 1080 │ 2026-02-15 21:50:04 │ 2m ago │
|
||||
╰──────────────────────────────────────┴─────────────────┴──────────────────────┴────────────┴─────────────────────┴───────────╯
|
||||
```
|
||||
|
||||
Select the agent using `agent select <agent-id>` and start the proxy listener (by default it listens on localhost:1080) by using the `agent start` command.
|
||||
|
||||
```
|
||||
proxyblob » agent select 7b5af883-7cc7-45a4-8599-da906753005d
|
||||
22:10:40 INF Agent selected agent_id=7b5af883-7cc7-45a4-8599-da906753005d
|
||||
7b5af883 » agent start
|
||||
22:10:46 INF Proxy started agent_id=7b5af883-7cc7-45a4-8599-da906753005d port=1080
|
||||
```
|
||||
|
||||
You can now use for example [proxychains](https://github.com/rofl0r/proxychains-ng) to tunnel the traffic through the SOCKS proxy.
|
||||
@@ -269,7 +290,7 @@ graph TB
|
||||
TransportA -->|Decode Packets| SocksHandler
|
||||
SocksHandler -->|Process Commands| CommandProcessor
|
||||
|
||||
%% Command Processing - Fixed syntax
|
||||
%% Command Processing
|
||||
subgraph "Command Processing"
|
||||
Connect["CONNECT"]
|
||||
Bind["TODO: BIND"]
|
||||
@@ -341,14 +362,17 @@ sequenceDiagram
|
||||
**Why does my agent immediately stop running ?**
|
||||
|
||||
There might be several reasons why your agent stops immediately after you run it. Check its exit code:
|
||||
|
||||
```sh
|
||||
# Bash
|
||||
echo $?
|
||||
```
|
||||
|
||||
```cmd
|
||||
REM CMD
|
||||
echo %ERRORLEVEL%
|
||||
```
|
||||
|
||||
```pwsh
|
||||
# PowerShell
|
||||
echo $LastExitCode
|
||||
@@ -362,8 +386,6 @@ Each exit code describes why the agent stopped running:
|
||||
| 1 | The context has been canceled |
|
||||
| 2 | The connection string is missing |
|
||||
| 3 | The connection string is invalid or expired |
|
||||
| 4 | The agent failed to write its metadata |
|
||||
| 5 | The agent container is not found |
|
||||
|
||||
If you encounter issues:
|
||||
|
||||
@@ -374,10 +396,41 @@ If you encounter issues:
|
||||
|
||||
## TODO
|
||||
|
||||
- BIND command implementation (currently not working)
|
||||
- Enhanced error handling and recovery
|
||||
- Improve proxy speed
|
||||
- BIND command (not implemented yet)
|
||||
- Improve proxy speed even more
|
||||
|
||||
## CHANGELOG
|
||||
|
||||
**ProxyBlob v2 (aznet boosted) - 16/02/2026:**
|
||||
|
||||
- Complete architecture rewrite using aznet networking layer
|
||||
- Multiple Azure Storage backends (Blob, Queue, Table Storage)
|
||||
- Last seen timestamps for connected agents
|
||||
- Enhanced connection management and lifecycle
|
||||
- Better error handling and recovery mechanisms
|
||||
- Improved polling efficiency with adaptive intervals
|
||||
- Automatic connection cleanup and resource management
|
||||
- Significantly higher throughput and lower latency
|
||||
- Cost optimization through backend selection
|
||||
- Multi-listener configuration support
|
||||
|
||||
**ProxyBlob public release - 29/04/2025:**
|
||||
|
||||
- SOCKS5 protocol (CONNECT and UDP ASSOCIATE)
|
||||
- Reverse client-server architecture
|
||||
- Azure Blob Storage communication
|
||||
- Interactive CLI with auto-completion
|
||||
- Multi-agent management
|
||||
- Local or remote proxy server
|
||||
- Container-based communication
|
||||
- Connection string authentication
|
||||
- Error handling
|
||||
- Azurite support for local development
|
||||
|
||||
## License
|
||||
|
||||
[GNU GPLv3 License](LICENSE)
|
||||
[GNU GPLv3 License](LICENSE)
|
||||
|
||||
---
|
||||
|
||||
Made with ❤️ by [@_atsika](https://x.com/_atsika)
|
||||
|
||||
+37
-145
@@ -2,13 +2,11 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"os/user"
|
||||
@@ -16,13 +14,12 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/Azure/azure-storage-blob-go/azblob"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"proxyblob/pkg/protocol"
|
||||
proxy "proxyblob/pkg/proxy/socks"
|
||||
"proxyblob/pkg/transport"
|
||||
|
||||
"github.com/atsika/aznet"
|
||||
)
|
||||
|
||||
// Exit codes.
|
||||
@@ -31,61 +28,44 @@ const (
|
||||
ErrContextCanceled = 1 // context canceled
|
||||
ErrNoConnectionString = 2 // missing connection string
|
||||
ErrConnectionStringError = 3 // invalid connection string
|
||||
ErrInfoBlobError = 4 // info blob write failed
|
||||
ErrContainerNotFound = 5 // container not found
|
||||
)
|
||||
|
||||
// ConnString holds the Azure connection string.
|
||||
// Can be set at compile time or via command line flag.
|
||||
var ConnString string
|
||||
|
||||
// Blob names for proxy-agent communication.
|
||||
const (
|
||||
InfoBlobName = "info" // agent metadata
|
||||
RequestBlobName = "request" // proxy-to-agent traffic
|
||||
ResponseBlobName = "response" // agent-to-proxy traffic
|
||||
)
|
||||
|
||||
// InfoKey defines the XOR encryption key for agent information
|
||||
// Security Note: Changing this key requires synchronized updates on both proxy and agent
|
||||
var (
|
||||
InfoKey = []byte{0xDE, 0xAD, 0xB1, 0x0B}
|
||||
)
|
||||
|
||||
// Agent manages proxy operations and blob storage communication.
|
||||
// Agent manages proxy operations and aznet communication.
|
||||
type Agent struct {
|
||||
ContainerURL azblob.ContainerURL // Azure container access
|
||||
Handler *proxy.SocksHandler // SOCKS handler
|
||||
Handler *proxy.SocksHandler // SOCKS handler
|
||||
}
|
||||
|
||||
// NewAgent creates an agent from a connection string.
|
||||
func NewAgent(ctx context.Context, connString string) (*Agent, int) {
|
||||
storageURL, containerID, sasToken, errCode := ParseConnectionString(connString)
|
||||
// Decode and parse the connection string
|
||||
driver, address, errCode := ParseConnectionString(connString)
|
||||
if errCode != Success {
|
||||
return nil, errCode
|
||||
}
|
||||
|
||||
pipeline := azblob.NewPipeline(
|
||||
azblob.NewAnonymousCredential(),
|
||||
azblob.PipelineOptions{},
|
||||
)
|
||||
|
||||
fullURL := fmt.Sprintf("%s/%s?%s", storageURL, containerID, sasToken)
|
||||
containerURL, err := url.Parse(fullURL)
|
||||
// Dial to the proxy using aznet with ULTRA-aggressive polling (WARNING: High API costs!)
|
||||
conn, err := aznet.Dial(driver, address, aznet.WithContext(ctx), aznet.WithFastPoll(time.Millisecond))
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to dial proxy")
|
||||
return nil, ErrConnectionStringError
|
||||
}
|
||||
log.Info().Msg("Connected to proxy")
|
||||
|
||||
container := azblob.NewContainerURL(*containerURL, pipeline)
|
||||
blobTransport := transport.NewBlobTransport(
|
||||
container.NewBlockBlobURL(RequestBlobName), // read
|
||||
container.NewBlockBlobURL(ResponseBlobName), // write
|
||||
)
|
||||
handler := proxy.NewSocksHandler(ctx, blobTransport)
|
||||
// Send identity (user@host) as first raw message before protocol starts
|
||||
identity := getIdentity()
|
||||
if _, err := conn.Write([]byte(identity)); err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to send identity")
|
||||
}
|
||||
|
||||
// Create SOCKS handler with direct connection (no transport wrapper)
|
||||
handler := proxy.NewSocksHandler(ctx, conn)
|
||||
|
||||
agent := &Agent{
|
||||
ContainerURL: container,
|
||||
Handler: handler,
|
||||
Handler: handler,
|
||||
}
|
||||
|
||||
return agent, Success
|
||||
@@ -93,15 +73,6 @@ func NewAgent(ctx context.Context, connString string) (*Agent, int) {
|
||||
|
||||
// Start begins processing proxy requests.
|
||||
func (a *Agent) Start(ctx context.Context) int {
|
||||
// Push agent information to blob storage
|
||||
if err := a.WriteInfoBlob(ctx); err != Success {
|
||||
a.Stop()
|
||||
return ErrContainerNotFound
|
||||
}
|
||||
|
||||
// Start the container monitoring goroutine
|
||||
go a.healthCheck(ctx)
|
||||
|
||||
// Start the handler
|
||||
a.Handler.Start("")
|
||||
|
||||
@@ -121,120 +92,41 @@ func (a *Agent) Stop() {
|
||||
a.Handler.Stop()
|
||||
}
|
||||
|
||||
// healthCheck verifies container existence every 30s and stops the agent if it's unavailable.
|
||||
func (a *Agent) healthCheck(ctx context.Context) {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
blobURL := a.ContainerURL.NewBlockBlobURL(InfoBlobName)
|
||||
_, err := blobURL.GetProperties(ctx, azblob.BlobAccessConditions{}, azblob.ClientProvidedKeyOptions{})
|
||||
if err != nil {
|
||||
if storageErr, ok := err.(azblob.StorageError); ok {
|
||||
if storageErr.ServiceCode() == azblob.ServiceCodeContainerNotFound ||
|
||||
storageErr.ServiceCode() == azblob.ServiceCodeContainerBeingDeleted {
|
||||
a.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WriteInfoBlob updates agent metadata.
|
||||
func (a *Agent) WriteInfoBlob(ctx context.Context) int {
|
||||
info := GetCurrentInfo()
|
||||
encryptedInfo := protocol.Xor([]byte(info), InfoKey)
|
||||
|
||||
blobURL := a.ContainerURL.NewBlockBlobURL(InfoBlobName)
|
||||
_, err := blobURL.Upload(
|
||||
ctx,
|
||||
bytes.NewReader(encryptedInfo),
|
||||
azblob.BlobHTTPHeaders{ContentType: "text/plain"},
|
||||
azblob.Metadata{},
|
||||
azblob.BlobAccessConditions{},
|
||||
azblob.DefaultAccessTier,
|
||||
nil,
|
||||
azblob.ClientProvidedKeyOptions{},
|
||||
azblob.ImmutabilityPolicyOptions{},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
// Check for context cancellation first
|
||||
if errors.Is(ctx.Err(), context.Canceled) {
|
||||
return ErrContextCanceled
|
||||
}
|
||||
|
||||
// Check for container deletion
|
||||
if storageErr, ok := err.(azblob.StorageError); ok {
|
||||
if storageErr.ServiceCode() == azblob.ServiceCodeContainerNotFound ||
|
||||
storageErr.ServiceCode() == azblob.ServiceCodeContainerBeingDeleted {
|
||||
return ErrContainerNotFound
|
||||
}
|
||||
// Other storage errors
|
||||
return ErrInfoBlobError
|
||||
}
|
||||
|
||||
return ErrInfoBlobError
|
||||
}
|
||||
|
||||
return Success
|
||||
}
|
||||
|
||||
// ParseConnectionString extracts storage URL, container ID and SAS token from a connection string.
|
||||
func ParseConnectionString(connString string) (string, string, string, int) {
|
||||
// ParseConnectionString extracts the driver and aznet address from a connection string.
|
||||
func ParseConnectionString(connString string) (string, string, int) {
|
||||
// Check for empty string first
|
||||
if connString == "" {
|
||||
return "", "", "", ErrNoConnectionString
|
||||
return "", "", ErrNoConnectionString
|
||||
}
|
||||
|
||||
// Try to decode the base64 encoded string
|
||||
decoded, err := base64.RawStdEncoding.DecodeString(connString)
|
||||
if err != nil {
|
||||
return "", "", "", ErrConnectionStringError
|
||||
return "", "", ErrConnectionStringError
|
||||
}
|
||||
|
||||
// Parse the URL and extract components
|
||||
u, err := url.Parse(string(decoded))
|
||||
if err != nil {
|
||||
return "", "", "", ErrConnectionStringError
|
||||
// Parse the string - it might be in the new format: driver|address
|
||||
// or the old format: address (where driver is extracted from scheme)
|
||||
str := string(decoded)
|
||||
if strings.Contains(str, "|") {
|
||||
parts := strings.SplitN(str, "|", 2)
|
||||
return parts[0], parts[1], Success
|
||||
}
|
||||
|
||||
// Extract path components and query string
|
||||
path := strings.TrimPrefix(u.Path, "/")
|
||||
if path == "" {
|
||||
return "", "", "", ErrConnectionStringError
|
||||
}
|
||||
|
||||
if u.RawQuery == "" {
|
||||
return "", "", "", ErrConnectionStringError
|
||||
}
|
||||
|
||||
// Return the storage URL, container ID and SAS token
|
||||
storageURL := fmt.Sprintf("%s://%s", u.Scheme, u.Host)
|
||||
return storageURL, path, u.RawQuery, Success
|
||||
return "", "", ErrConnectionStringError
|
||||
}
|
||||
|
||||
// GetCurrentInfo returns username@hostname.
|
||||
func GetCurrentInfo() string {
|
||||
// getIdentity returns "user@hostname" for agent identification.
|
||||
func getIdentity() string {
|
||||
username := "unknown"
|
||||
if u, err := user.Current(); err == nil {
|
||||
username = u.Username
|
||||
}
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
hostname = "unknown"
|
||||
}
|
||||
|
||||
currentUser, err := user.Current()
|
||||
if err != nil {
|
||||
currentUser = &user.User{
|
||||
Username: "unknown",
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s@%s", currentUser.Username, hostname)
|
||||
return fmt.Sprintf("%s@%s", username, hostname)
|
||||
}
|
||||
|
||||
// init configures logging with zerolog
|
||||
|
||||
+823
-501
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 2.0 MiB |
+17
-3
@@ -1,4 +1,18 @@
|
||||
{
|
||||
"storage_account_name": "storage-name",
|
||||
"storage_account_key": "storage-access-key"
|
||||
}
|
||||
"listeners": [
|
||||
{
|
||||
"name": "blob-listener",
|
||||
"driver": "azblob",
|
||||
"address": "https://proxyblob.blob.core.windows.net",
|
||||
"storage_account": "proxyblob",
|
||||
"storage_account_key": "your_account_key"
|
||||
},
|
||||
{
|
||||
"name": "queue-listener",
|
||||
"driver": "azqueue",
|
||||
"address": "http://127.0.0.1:10001",
|
||||
"storage_account": "devstoreaccount1",
|
||||
"storage_account_key": "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=="
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,40 +1,42 @@
|
||||
module proxyblob
|
||||
|
||||
go 1.23.4
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/desertbit/grumble v1.1.3
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/data/aztables v1.4.1 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azqueue v1.0.1 // indirect
|
||||
github.com/desertbit/grumble v1.2.0
|
||||
github.com/google/uuid v1.6.0
|
||||
golang.org/x/crypto v0.33.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/azure-pipeline-go v0.2.3 // indirect
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.24 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||
github.com/go-openapi/errors v0.22.0 // indirect
|
||||
github.com/go-openapi/strfmt v0.23.0 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.1 // indirect
|
||||
github.com/mattn/go-ieproxy v0.0.12 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
go.mongodb.org/mongo-driver v1.17.3 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/azure-storage-blob-go v0.15.0
|
||||
github.com/clipperhouse/uax29/v2 v2.2.0 // indirect
|
||||
github.com/desertbit/closer/v3 v3.7.5 // indirect
|
||||
github.com/desertbit/columnize v2.1.0+incompatible // indirect
|
||||
github.com/desertbit/go-shlex v0.1.1 // indirect
|
||||
github.com/desertbit/readline v1.5.1 // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/jedib0t/go-pretty v4.3.0+incompatible
|
||||
github.com/flynn/noise v1.1.0 // indirect
|
||||
github.com/go-openapi/errors v0.22.3 // indirect
|
||||
github.com/go-openapi/strfmt v0.24.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/rs/zerolog v1.33.0
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.19 // indirect
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
go.mongodb.org/mongo-driver v1.17.4 // indirect
|
||||
golang.org/x/crypto v0.48.0 // indirect
|
||||
golang.org/x/net v0.50.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/atsika/aznet v0.0.0-20260216144450-1395eae946cd
|
||||
github.com/jedib0t/go-pretty v4.3.0+incompatible
|
||||
github.com/rs/zerolog v1.34.0
|
||||
)
|
||||
|
||||
@@ -1,190 +1,205 @@
|
||||
github.com/Azure/azure-pipeline-go v0.2.3 h1:7U9HBg1JFK3jHl5qmo4CTZKFTVgMwdFHMVtCdfBE21U=
|
||||
github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k=
|
||||
github.com/Azure/azure-storage-blob-go v0.15.0 h1:rXtgp8tN1p29GvpGgfJetavIG0V7OgcSXPpwp3tx6qk=
|
||||
github.com/Azure/azure-storage-blob-go v0.15.0/go.mod h1:vbjsVbX0dlxnRc4FFMPsS9BsJWPcne7GB7onqlPvz58=
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs=
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M=
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.24 h1:BHZfgGsGwdkHDyZdtQRQk1WeUdW0m2WPAwuHZwUi5i4=
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.24/go.mod h1:7T1+g0PYFmACYW5LlG2fcoPiPlFHjClyRGL7dRlP5c8=
|
||||
github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw=
|
||||
github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74=
|
||||
github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=
|
||||
github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg=
|
||||
github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
|
||||
github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo=
|
||||
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
|
||||
github.com/AlecAivazis/survey/v2 v2.0.5/go.mod h1:WYBhg6f0y/fNYUuesWQc0PKbJcEliGcYHB9sNT3Bg74=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/data/aztables v1.4.1 h1:j0hhYS006eJ54vusoap0f2NVZ1YY3QnaAEnLM68f0SQ=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/data/aztables v1.4.1/go.mod h1:AdtInaXmK8eYmbjezRWgLz+Qs46nc9Up9GWGwteWNfw=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1 h1:/Zt+cDPnpC3OVDm/JKLOs7M2DKmLRIIp3XIx9pHHiig=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1/go.mod h1:Ng3urmn6dYe8gnbCMoHHVl5APYz2txho3koEkV2o2HA=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4 h1:jWQK1GI+LeGGUKBADtcH2rRqPxYB1Ljwms5gFA2LqrM=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4/go.mod h1:8mwH4klAm9DUgR2EEHyEEAQlRDvLPyg5fQry3y+cDew=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azqueue v1.0.1 h1:qvrrnQ2mIjwY7IVlQuNB0ma43Nr74+9ZTZJ60KlmlV4=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azqueue v1.0.1/go.mod h1:FkF/Az07vR3S4sBdjCuisznWfFWOD8u6Ibm/g/oyDAk=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
|
||||
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc=
|
||||
github.com/Netflix/go-expect v0.0.0-20190729225929-0e00d9168667/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc=
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||
github.com/atsika/aznet v0.0.0-20260216144450-1395eae946cd h1:tvxn6W9ky7ROcX5M3SbIHCDdypnQRPiBC/3K/+3+sBo=
|
||||
github.com/atsika/aznet v0.0.0-20260216144450-1395eae946cd/go.mod h1:0+F6U1Bi8CrghJ51eAY1J7B9pCG5FCJc930Dm1Jqh1I=
|
||||
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY=
|
||||
github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
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/desertbit/closer/v3 v3.1.2/go.mod h1:AAC4KRd8DC40nwvV967J/kDFhujMEiuwIKQfN0IDxXw=
|
||||
github.com/desertbit/closer/v3 v3.1.3/go.mod h1:AAC4KRd8DC40nwvV967J/kDFhujMEiuwIKQfN0IDxXw=
|
||||
github.com/desertbit/closer/v3 v3.7.5 h1:tJ3BXDeflcWjGSacIQFiFOZf3ep7kit9HMxM87qXVLc=
|
||||
github.com/desertbit/closer/v3 v3.7.5/go.mod h1:wxbB5mDxqhQC8CjI8ApBhj9aHHSLjdok5WFkJj4Bq7M=
|
||||
github.com/desertbit/columnize v2.1.0+incompatible h1:h55rYmdrWoTj7w9aAnCkxzM3C2Eb8zuFa2W41t0o5j0=
|
||||
github.com/desertbit/columnize v2.1.0+incompatible/go.mod h1:5kPrzQwKbQ8E5D28nvTVPqIBJyj+8jvJzwt6HXZvXgI=
|
||||
github.com/desertbit/go-shlex v0.1.1 h1:c65HnbgX1QyC6kPL1dMzUpZ4puNUE6ai/eVucWNLNsk=
|
||||
github.com/desertbit/go-shlex v0.1.1/go.mod h1:Qbb+mJNud5AypgHZ81EL8syOGaWlwvAOTqS7XmWI4pQ=
|
||||
github.com/desertbit/grumble v1.1.3 h1:gbdgVGWsHmNraJ7Gn6Q4TiUEIHU/UHfbc1KUSbBlgYU=
|
||||
github.com/desertbit/grumble v1.1.3/go.mod h1:r7j3ShNy5EmOsegRD2DzTutIaGiLiA3M5yBTXXeLwcs=
|
||||
github.com/desertbit/grumble v1.2.0 h1:STmiLglcNWLjh1N9Qm+l1NIUjxNk3FH9sS7yPSlUI3I=
|
||||
github.com/desertbit/grumble v1.2.0/go.mod h1:XfdVDEGzjcFO7GFBq5xrdm3dhZTId0XC4RMG+KEsB68=
|
||||
github.com/desertbit/readline v1.5.1 h1:/wOIZkWYl1s+IvJm/5bOknfUgs6MhS9svRNZpFM53Os=
|
||||
github.com/desertbit/readline v1.5.1/go.mod h1:pHQgTsCFs9Cpfh5mlSUFi9Xa5kkL4d8L1Jo4UVWzPw0=
|
||||
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
|
||||
github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
|
||||
github.com/go-openapi/errors v0.22.0 h1:c4xY/OLxUBSTiepAg3j/MHuAv5mJhnf53LLMWFB+u/w=
|
||||
github.com/go-openapi/errors v0.22.0/go.mod h1:J3DmZScxCDufmIMsdOuDHxJbdOGC0xtUynjIx092vXE=
|
||||
github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c=
|
||||
github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4=
|
||||
github.com/flynn/noise v1.1.0 h1:KjPQoQCEFdZDiP03phOvGi11+SVVhBG2wOWAorLsstg=
|
||||
github.com/flynn/noise v1.1.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag=
|
||||
github.com/go-openapi/errors v0.22.3 h1:k6Hxa5Jg1TUyZnOwV2Lh81j8ayNw5VVYLvKrp4zFKFs=
|
||||
github.com/go-openapi/errors v0.22.3/go.mod h1:+WvbaBBULWCOna//9B9TbLNGSFOfF8lY9dw4hGiEiKQ=
|
||||
github.com/go-openapi/strfmt v0.24.0 h1:dDsopqbI3wrrlIzeXRbqMihRNnjzGC+ez4NQaAAJLuc=
|
||||
github.com/go-openapi/strfmt v0.24.0/go.mod h1:Lnn1Bk9rZjXxU9VMADbEEOo7D7CDyKGLsSKekhFr7s4=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=
|
||||
github.com/hinshun/vt10x v0.0.0-20180809195222-d55458df857c/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=
|
||||
github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo=
|
||||
github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-ieproxy v0.0.1/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/gNWuh88E=
|
||||
github.com/mattn/go-ieproxy v0.0.12 h1:OZkUFJC3ESNZPQ+6LzC3VJIFSnreeFLQyqvBWtvfL2M=
|
||||
github.com/mattn/go-ieproxy v0.0.12/go.mod h1:Vn+N61199DAnVeTgaF8eoB9PvLO8P3OBnG95ENh7B7c=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
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.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
||||
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||
github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU=
|
||||
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
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/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
|
||||
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
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/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.mongodb.org/mongo-driver v1.17.3 h1:TQyXhnsWfWtgAhMtOgtYHMTkZIfBTpMTsMnd9ZBeHxQ=
|
||||
go.mongodb.org/mongo-driver v1.17.3/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
|
||||
golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw=
|
||||
go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
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/net v0.0.0-20191112182307-2180aed22343/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180606202747-9527bec2660b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/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-20191112214154-59a1497f0cea/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/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.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/AlecAivazis/survey.v1 v1.8.5/go.mod h1:iBNOmqKz/NUbZx3bA+4hAGLRC7fSK7tgtVDT4tB22XA=
|
||||
gopkg.in/AlecAivazis/survey.v1 v1.8.8/go.mod h1:CaHjv79TCgAvXMSFJSVgonHXYWxnhzI3eoHtnX5UgUo=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
|
||||
+84
-114
@@ -2,11 +2,11 @@ package protocol
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"proxyblob/pkg/transport"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
@@ -38,8 +38,8 @@ type PacketHandler interface {
|
||||
// BaseHandler implements common protocol functionality for proxy and agent.
|
||||
// It provides connection management, packet routing, and error handling.
|
||||
type BaseHandler struct {
|
||||
// transport handles underlying packet transmission
|
||||
transport transport.Transport
|
||||
// conn handles underlying packet transmission (direct net.Conn)
|
||||
conn net.Conn
|
||||
|
||||
// Connections maps UUIDs to active Connection objects
|
||||
Connections sync.Map
|
||||
@@ -50,70 +50,95 @@ type BaseHandler struct {
|
||||
// Cancel terminates handler context
|
||||
Cancel context.CancelFunc
|
||||
|
||||
// OnReceive is called on every successful packet read (optional)
|
||||
OnReceive func()
|
||||
|
||||
// PacketHandler routes packets to specific handlers
|
||||
PacketHandler
|
||||
}
|
||||
|
||||
// NewBaseHandler creates a handler with specified context and transport.
|
||||
// NewBaseHandler creates a handler with specified context and connection.
|
||||
// Uses background context if parent context is nil.
|
||||
func NewBaseHandler(parentCtx context.Context, transport transport.Transport) *BaseHandler {
|
||||
func NewBaseHandler(parentCtx context.Context, conn net.Conn) *BaseHandler {
|
||||
if parentCtx == nil {
|
||||
parentCtx = context.Background()
|
||||
}
|
||||
ctx, cancel := context.WithCancel(parentCtx)
|
||||
return &BaseHandler{
|
||||
transport: transport,
|
||||
Ctx: ctx,
|
||||
Cancel: cancel,
|
||||
conn: conn,
|
||||
Ctx: ctx,
|
||||
Cancel: cancel,
|
||||
}
|
||||
}
|
||||
|
||||
// ReceiveLoop processes incoming packets until context cancellation.
|
||||
// Implements exponential backoff for consecutive errors.
|
||||
// ReceiveLoop processes incoming packets until EOF or context cancellation.
|
||||
// Uses exponential backoff on transient errors but never exits silently.
|
||||
func (h *BaseHandler) ReceiveLoop() {
|
||||
consecutiveErrors := 0
|
||||
maxConsecutiveErrors := 5
|
||||
const maxBackoff = 5 * time.Second
|
||||
|
||||
// NOTE: aznet already handles message framing and returns complete messages
|
||||
// We don't need stream buffering - each Read() returns ONE complete message
|
||||
|
||||
// Allocate buffer once and reuse it (16MB is the max frame size in aznet)
|
||||
buffer := make([]byte, 16*1024*1024)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-h.Ctx.Done():
|
||||
return
|
||||
default:
|
||||
data, errCode := h.transport.Receive(h.Ctx)
|
||||
if errCode != ErrNone {
|
||||
if h.transport.IsClosed(errCode) {
|
||||
h.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if h.Ctx.Err() == nil && errCode != ErrTransportError {
|
||||
consecutiveErrors++
|
||||
if consecutiveErrors == maxConsecutiveErrors {
|
||||
return // Too many errors, just exit
|
||||
}
|
||||
time.Sleep(time.Duration(consecutiveErrors*50) * time.Millisecond)
|
||||
}
|
||||
// Read directly from connection (aznet handles framing)
|
||||
n, err := h.conn.Read(buffer)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
h.Stop()
|
||||
return
|
||||
}
|
||||
|
||||
if h.Ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Transient error: exponential backoff (100ms, 200ms, 400ms, ... capped at 5s)
|
||||
consecutiveErrors++
|
||||
backoff := time.Duration(100<<uint(consecutiveErrors-1)) * time.Millisecond
|
||||
if backoff > maxBackoff {
|
||||
backoff = maxBackoff
|
||||
}
|
||||
// Interruptible sleep
|
||||
select {
|
||||
case <-h.Ctx.Done():
|
||||
return
|
||||
case <-time.After(backoff):
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
consecutiveErrors = 0
|
||||
if h.OnReceive != nil {
|
||||
h.OnReceive()
|
||||
}
|
||||
data := buffer[:n]
|
||||
|
||||
if len(data) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
packet := Decode(data)
|
||||
if packet == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
errCode := h.handlePacket(packet)
|
||||
if errCode != ErrNone {
|
||||
if h.Ctx.Err() != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
consecutiveErrors = 0
|
||||
|
||||
if len(data) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
packet := Decode(data)
|
||||
if packet == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
errCode = h.handlePacket(packet)
|
||||
if errCode != ErrNone {
|
||||
if h.Ctx.Err() != nil && errCode == ErrConnectionClosed {
|
||||
continue
|
||||
}
|
||||
h.SendClose(packet.ConnectionID, errCode)
|
||||
}
|
||||
// Async close dispatch to avoid blocking ReceiveLoop on aznet writes
|
||||
go h.SendClose(packet.ConnectionID, errCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -135,73 +160,25 @@ func (h *BaseHandler) handlePacket(packet *Packet) byte {
|
||||
}
|
||||
}
|
||||
|
||||
// SendNewConnection initiates key exchange for new connection.
|
||||
// SendNewConnection initiates a new connection.
|
||||
// Returns error code indicating success or specific failure.
|
||||
func (h *BaseHandler) SendNewConnection(connectionID uuid.UUID) byte {
|
||||
privateKey, publicKey := GenerateKeyPair()
|
||||
nonce := GenerateNonce()
|
||||
|
||||
connObj, exists := h.Connections.Load(connectionID)
|
||||
if !exists {
|
||||
return ErrConnectionNotFound
|
||||
}
|
||||
conn := connObj.(*Connection)
|
||||
|
||||
// Store nonce and private key for key derivation during ACK
|
||||
tempData := make([]byte, len(nonce)+len(privateKey))
|
||||
copy(tempData[:len(nonce)], nonce)
|
||||
copy(tempData[len(nonce):], privateKey)
|
||||
conn.SecretKey = tempData
|
||||
|
||||
// Send nonce and public key to peer
|
||||
data := make([]byte, len(nonce)+len(publicKey))
|
||||
copy(data[:len(nonce)], nonce)
|
||||
copy(data[len(nonce):], publicKey)
|
||||
|
||||
return h.sendPacket(CmdNew, connectionID, data)
|
||||
return h.sendPacket(CmdNew, connectionID, nil)
|
||||
}
|
||||
|
||||
// SendConnAck completes key exchange by deriving shared key.
|
||||
// SendConnAck acknowledges connection.
|
||||
// Returns error code indicating success or specific failure.
|
||||
func (h *BaseHandler) SendConnAck(connectionID uuid.UUID) byte {
|
||||
connObj, exists := h.Connections.Load(connectionID)
|
||||
if !exists {
|
||||
return ErrConnectionNotFound
|
||||
}
|
||||
conn := connObj.(*Connection)
|
||||
|
||||
privateKey, publicKey := GenerateKeyPair()
|
||||
|
||||
// The first 24 bytes of SecretKey should be the nonce,
|
||||
// and the server's public key should be in the data field from OnNew
|
||||
serverData := conn.SecretKey
|
||||
nonce := serverData[:24]
|
||||
serverPublicKey := serverData[24:]
|
||||
|
||||
symmetricKey, errCode := DeriveKey(privateKey, serverPublicKey, nonce)
|
||||
if errCode != ErrNone {
|
||||
return errCode
|
||||
}
|
||||
|
||||
conn.SecretKey = symmetricKey
|
||||
|
||||
// Send public key in CmdAck
|
||||
return h.sendPacket(CmdAck, connectionID, publicKey)
|
||||
return h.sendPacket(CmdAck, connectionID, nil)
|
||||
}
|
||||
|
||||
// SendData sends data.
|
||||
// Returns error code indicating success or specific failure.
|
||||
func (h *BaseHandler) SendData(connectionID uuid.UUID, data []byte) byte {
|
||||
// Get connection
|
||||
connObj, exists := h.Connections.Load(connectionID)
|
||||
if !exists {
|
||||
// Verify connection exists
|
||||
if _, exists := h.Connections.Load(connectionID); !exists {
|
||||
return ErrConnectionNotFound
|
||||
}
|
||||
conn := connObj.(*Connection)
|
||||
|
||||
encrypted, errCode := Encrypt(conn.SecretKey, data)
|
||||
if errCode != ErrNone {
|
||||
return errCode
|
||||
}
|
||||
data = encrypted
|
||||
|
||||
return h.sendPacket(CmdData, connectionID, data)
|
||||
}
|
||||
@@ -215,6 +192,7 @@ func (h *BaseHandler) SendClose(connectionID uuid.UUID, errCode byte) byte {
|
||||
conn := connObj.(*Connection)
|
||||
|
||||
conn.Close()
|
||||
h.Connections.Delete(connectionID)
|
||||
return h.sendPacket(CmdClose, connectionID, []byte{errCode})
|
||||
}
|
||||
|
||||
@@ -235,13 +213,12 @@ func (h *BaseHandler) sendPacket(cmd byte, connectionID uuid.UUID, data []byte)
|
||||
return ErrInvalidPacket
|
||||
}
|
||||
|
||||
errCode := h.transport.Send(h.Ctx, encoded)
|
||||
if errCode != ErrNone {
|
||||
// Check if this is a transport closure
|
||||
if h.transport.IsClosed(errCode) {
|
||||
_, err := h.conn.Write(encoded)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return ErrTransportClosed
|
||||
}
|
||||
return ErrPacketSendFailed
|
||||
return ErrTransportError
|
||||
}
|
||||
|
||||
return ErrNone
|
||||
@@ -250,15 +227,8 @@ func (h *BaseHandler) sendPacket(cmd byte, connectionID uuid.UUID, data []byte)
|
||||
func (h *BaseHandler) CloseAllConnections() {
|
||||
h.Connections.Range(func(key, value interface{}) bool {
|
||||
conn := value.(*Connection)
|
||||
|
||||
// Only notify if not already closed
|
||||
select {
|
||||
case <-conn.Closed:
|
||||
// Already closed
|
||||
default:
|
||||
conn.Close()
|
||||
}
|
||||
|
||||
conn.Close()
|
||||
h.Connections.Delete(key)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
+56
-44
@@ -2,51 +2,44 @@ package protocol
|
||||
|
||||
import (
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// ConnectionState tracks the lifecycle of a proxy connection
|
||||
type ConnectionState int
|
||||
|
||||
const (
|
||||
// StateNew indicates a pending connection awaiting establishment
|
||||
StateNew ConnectionState = iota
|
||||
|
||||
// StateConnected indicates an active connection with data flow
|
||||
StateConnected
|
||||
|
||||
// StateClosed indicates a terminated connection
|
||||
StateClosed
|
||||
)
|
||||
|
||||
// Connection manages a proxy connection between client and target.
|
||||
// It is safe for concurrent use by multiple goroutines.
|
||||
//
|
||||
// Connection state is determined by:
|
||||
// - ProtocolConn == nil: connection is pending acknowledgment
|
||||
// - ProtocolConn != nil && Closed not signaled: connection is active
|
||||
// - Closed signaled: connection is terminated
|
||||
type Connection struct {
|
||||
// ID uniquely identifies the connection
|
||||
ID uuid.UUID
|
||||
|
||||
// State indicates current connection lifecycle phase
|
||||
State ConnectionState
|
||||
|
||||
// Conn holds the network connection (optional)
|
||||
// Conn holds the network connection (could be real or virtual ProtocolConn)
|
||||
Conn net.Conn
|
||||
|
||||
// ReadBuffer receives data from the remote endpoint
|
||||
ReadBuffer chan []byte
|
||||
// ProtocolConn is the virtual connection for protocol-based data transfer
|
||||
ProtocolConn *ProtocolConn
|
||||
|
||||
// Closed signals connection termination
|
||||
Closed chan struct{}
|
||||
|
||||
// deliverCh is the dispatch buffer between ReceiveLoop and the delivery goroutine.
|
||||
// ReceiveLoop writes here non-blocking; the delivery goroutine drains to readBuffer.
|
||||
deliverCh chan []byte
|
||||
|
||||
// closeOnce guards Close() against double-close panics
|
||||
closeOnce sync.Once
|
||||
|
||||
// CreatedAt records connection creation time
|
||||
CreatedAt time.Time
|
||||
|
||||
// LastActivity tracks most recent data transfer
|
||||
LastActivity time.Time
|
||||
|
||||
// SecretKey holds encryption key for secure communication
|
||||
SecretKey []byte
|
||||
}
|
||||
|
||||
// NewConnection creates a connection with specified ID.
|
||||
@@ -54,36 +47,55 @@ type Connection struct {
|
||||
func NewConnection(id uuid.UUID) *Connection {
|
||||
return &Connection{
|
||||
ID: id,
|
||||
State: StateNew,
|
||||
ReadBuffer: make(chan []byte),
|
||||
Closed: make(chan struct{}),
|
||||
deliverCh: make(chan []byte, 256),
|
||||
CreatedAt: time.Now(),
|
||||
LastActivity: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// Close terminates the connection and its resources.
|
||||
// Safe to call multiple times. Returns ErrNone on success.
|
||||
// Safe to call multiple times (guarded by closeOnce). Returns ErrNone.
|
||||
func (c *Connection) Close() byte {
|
||||
if c.State == StateClosed {
|
||||
return ErrNone
|
||||
}
|
||||
|
||||
c.State = StateClosed
|
||||
|
||||
select {
|
||||
case <-c.Closed:
|
||||
// Already closed
|
||||
default:
|
||||
c.closeOnce.Do(func() {
|
||||
close(c.Closed)
|
||||
}
|
||||
|
||||
if c.Conn != nil {
|
||||
err := c.Conn.Close()
|
||||
if err != nil {
|
||||
return ErrConnectionClosed
|
||||
if c.ProtocolConn != nil {
|
||||
c.ProtocolConn.Shutdown()
|
||||
}
|
||||
}
|
||||
|
||||
if c.Conn != nil {
|
||||
c.Conn.Close()
|
||||
}
|
||||
})
|
||||
return ErrNone
|
||||
}
|
||||
|
||||
// Deliver sends data to the connection's delivery goroutine without blocking.
|
||||
// Returns true if data was accepted, false if the buffer is full or connection is closed.
|
||||
func (c *Connection) Deliver(data []byte) bool {
|
||||
select {
|
||||
case c.deliverCh <- data:
|
||||
return true
|
||||
case <-c.Closed:
|
||||
return false
|
||||
default:
|
||||
return false // dispatch buffer full
|
||||
}
|
||||
}
|
||||
|
||||
// StartDelivery launches a goroutine that drains deliverCh into ProtocolConn.DeliverData.
|
||||
// Must be called after ProtocolConn is set.
|
||||
func (c *Connection) StartDelivery() {
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case data, ok := <-c.deliverCh:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
c.ProtocolConn.DeliverData(data)
|
||||
case <-c.Closed:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"io"
|
||||
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
"golang.org/x/crypto/curve25519"
|
||||
"golang.org/x/crypto/hkdf"
|
||||
"golang.org/x/crypto/sha3"
|
||||
)
|
||||
|
||||
// GenerateKeyPair creates a new X25519 key pair for key exchange.
|
||||
// Returns a properly clamped private key and its corresponding public key.
|
||||
func GenerateKeyPair() (privateKey, publicKey []byte) {
|
||||
privateKey = make([]byte, curve25519.ScalarSize)
|
||||
io.ReadFull(rand.Reader, privateKey)
|
||||
|
||||
// Clamp private key according to X25519 spec
|
||||
privateKey[0] &= 248
|
||||
privateKey[31] &= 127
|
||||
privateKey[31] |= 64
|
||||
|
||||
publicKey, _ = curve25519.X25519(privateKey, curve25519.Basepoint)
|
||||
return privateKey, publicKey
|
||||
}
|
||||
|
||||
// GenerateNonce creates a random nonce for ChaCha20-Poly1305.
|
||||
// Returns a cryptographically secure random nonce.
|
||||
func GenerateNonce() []byte {
|
||||
nonce := make([]byte, chacha20poly1305.NonceSizeX)
|
||||
io.ReadFull(rand.Reader, nonce)
|
||||
return nonce
|
||||
}
|
||||
|
||||
// DeriveKey performs X25519 key exchange and HKDF key derivation.
|
||||
// Returns a symmetric key and status code. Key is nil on error.
|
||||
func DeriveKey(privateKey, peerPublicKey, nonce []byte) ([]byte, byte) {
|
||||
// Derive shared secret using X25519
|
||||
sharedSecret, err := curve25519.X25519(privateKey, peerPublicKey)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidCrypto
|
||||
}
|
||||
|
||||
// Derive symmetric key using HKDF-SHA3
|
||||
kdf := hkdf.New(sha3.New256, sharedSecret, nonce, nil)
|
||||
symmetricKey := make([]byte, chacha20poly1305.KeySize)
|
||||
if _, err := io.ReadFull(kdf, symmetricKey); err != nil {
|
||||
return nil, ErrInvalidCrypto
|
||||
}
|
||||
|
||||
return symmetricKey, ErrNone
|
||||
}
|
||||
|
||||
// Encrypt performs authenticated encryption using ChaCha20-Poly1305.
|
||||
// Returns (nonce || ciphertext || tag) or nil on error.
|
||||
func Encrypt(key, plaintext []byte) ([]byte, byte) {
|
||||
aead, err := chacha20poly1305.NewX(key)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidCrypto
|
||||
}
|
||||
|
||||
nonce := GenerateNonce()
|
||||
ciphertext := aead.Seal(nonce, nonce, plaintext, nil)
|
||||
return ciphertext, ErrNone
|
||||
}
|
||||
|
||||
// Decrypt performs authenticated decryption using ChaCha20-Poly1305.
|
||||
// Returns decrypted plaintext or nil if authentication fails.
|
||||
func Decrypt(key, ciphertext []byte) ([]byte, byte) {
|
||||
aead, err := chacha20poly1305.NewX(key)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidCrypto
|
||||
}
|
||||
|
||||
if len(ciphertext) < chacha20poly1305.NonceSizeX {
|
||||
return nil, ErrInvalidCrypto
|
||||
}
|
||||
|
||||
nonce := ciphertext[:chacha20poly1305.NonceSizeX]
|
||||
ciphertextBody := ciphertext[chacha20poly1305.NonceSizeX:]
|
||||
|
||||
plaintext, err := aead.Open(nil, nonce, ciphertextBody, nil)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidCrypto
|
||||
}
|
||||
|
||||
return plaintext, ErrNone
|
||||
}
|
||||
|
||||
// Xor performs byte-wise XOR of data with a repeating key.
|
||||
// Warning: This is NOT cryptographically secure, use only for basic obfuscation.
|
||||
func Xor(data []byte, key []byte) []byte {
|
||||
for i := range data {
|
||||
data[i] ^= key[i%len(key)]
|
||||
}
|
||||
return data
|
||||
}
|
||||
+47
-5
@@ -2,7 +2,8 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"proxyblob/pkg/transport"
|
||||
"errors"
|
||||
"net"
|
||||
)
|
||||
|
||||
// Protocol error codes for agent-server communication.
|
||||
@@ -21,11 +22,12 @@ const (
|
||||
ErrPacketSendFailed byte = 14 // Packet transmission failed
|
||||
ErrHandlerStopped byte = 15 // Protocol handler is not running
|
||||
ErrUnexpectedPacket byte = 16 // Received unexpected packet type
|
||||
ErrBufferFull byte = 17 // Per-connection delivery buffer is full
|
||||
|
||||
// Transport errors (20-29)
|
||||
ErrTransportClosed byte = transport.ErrTransportClosed // Transport layer terminated
|
||||
ErrTransportTimeout byte = transport.ErrTransportTimeout // Transport operation timed out
|
||||
ErrTransportError byte = transport.ErrTransportError // Transport operation failed
|
||||
ErrTransportClosed byte = 20 // Transport layer terminated
|
||||
ErrTransportTimeout byte = 21 // Transport operation timed out
|
||||
ErrTransportError byte = 22 // Transport operation failed
|
||||
|
||||
// SOCKS errors (30-39)
|
||||
ErrInvalidSocksVersion byte = 30 // Unsupported SOCKS protocol version
|
||||
@@ -40,5 +42,45 @@ const (
|
||||
|
||||
// Packet errors (40-49)
|
||||
ErrInvalidPacket byte = 40 // Malformed packet structure
|
||||
ErrInvalidCrypto byte = 41 // Cryptographic operation failed
|
||||
)
|
||||
|
||||
// MapNetError converts a network error to an appropriate protocol error code.
|
||||
// This consolidates error mapping logic used throughout the codebase.
|
||||
func MapNetError(err error) byte {
|
||||
if err == nil {
|
||||
return ErrNone
|
||||
}
|
||||
|
||||
// Check for timeout
|
||||
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
||||
return ErrTTLExpired
|
||||
}
|
||||
|
||||
// Check for net.OpError (most common network error type)
|
||||
if opErr, ok := err.(*net.OpError); ok {
|
||||
if errors.Is(opErr, net.ErrClosed) {
|
||||
return ErrTransportClosed
|
||||
}
|
||||
switch opErr.Op {
|
||||
case "dial":
|
||||
return ErrNetworkUnreachable
|
||||
case "read", "write":
|
||||
return ErrHostUnreachable
|
||||
case "listen":
|
||||
return ErrNetworkUnreachable
|
||||
}
|
||||
}
|
||||
|
||||
// Check for DNS errors
|
||||
if _, ok := err.(*net.DNSError); ok {
|
||||
return ErrHostUnreachable
|
||||
}
|
||||
|
||||
// Check for closed connection
|
||||
if errors.Is(err, net.ErrClosed) {
|
||||
return ErrTransportClosed
|
||||
}
|
||||
|
||||
// Default to connection refused
|
||||
return ErrConnectionRefused
|
||||
}
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// ProtocolConn implements net.Conn by wrapping the protocol layer.
|
||||
// This allows using standard io.Copy for data transfer, just like armon/go-socks5.
|
||||
type ProtocolConn struct {
|
||||
id uuid.UUID
|
||||
handler *BaseHandler
|
||||
readBuffer chan []byte // internal buffer for received data
|
||||
readData []byte // partial read buffer
|
||||
readOffset int
|
||||
closed chan struct{}
|
||||
closeOnce sync.Once // guards SendClose (protocol-level close)
|
||||
shutdownOnce sync.Once // guards close(closed) (local shutdown)
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
// NewProtocolConn creates a virtual connection that uses the protocol layer.
|
||||
func NewProtocolConn(ctx context.Context, id uuid.UUID, handler *BaseHandler) *ProtocolConn {
|
||||
return &ProtocolConn{
|
||||
id: id,
|
||||
handler: handler,
|
||||
readBuffer: make(chan []byte, 100), // Buffered channel for received data
|
||||
closed: make(chan struct{}),
|
||||
ctx: ctx,
|
||||
}
|
||||
}
|
||||
|
||||
// Read implements net.Conn.Read - reads data received via protocol.
|
||||
func (c *ProtocolConn) Read(b []byte) (n int, err error) {
|
||||
// If we have buffered data, return it first
|
||||
if c.readOffset < len(c.readData) {
|
||||
n = copy(b, c.readData[c.readOffset:])
|
||||
c.readOffset += n
|
||||
if c.readOffset >= len(c.readData) {
|
||||
c.readData = nil
|
||||
c.readOffset = 0
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// Wait for new data from protocol layer
|
||||
select {
|
||||
case <-c.closed:
|
||||
return 0, io.EOF
|
||||
case <-c.ctx.Done():
|
||||
return 0, io.EOF
|
||||
case data, ok := <-c.readBuffer:
|
||||
if !ok {
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
// Copy as much as possible to b
|
||||
n = copy(b, data)
|
||||
// If there's leftover data, store it
|
||||
if n < len(data) {
|
||||
c.readData = data
|
||||
c.readOffset = n
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Write implements net.Conn.Write - sends data via protocol.
|
||||
func (c *ProtocolConn) Write(b []byte) (n int, err error) {
|
||||
select {
|
||||
case <-c.closed:
|
||||
return 0, io.ErrClosedPipe
|
||||
case <-c.ctx.Done():
|
||||
return 0, io.ErrClosedPipe
|
||||
default:
|
||||
}
|
||||
|
||||
errCode := c.handler.SendData(c.id, b)
|
||||
if errCode != ErrNone {
|
||||
return 0, io.ErrClosedPipe
|
||||
}
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
// Shutdown closes the ProtocolConn's closed channel without sending a CmdClose packet.
|
||||
// Called by Connection.Close() to unblock delivery goroutines and ProtocolConn.Read().
|
||||
// Safe to call multiple times.
|
||||
func (c *ProtocolConn) Shutdown() {
|
||||
c.shutdownOnce.Do(func() {
|
||||
close(c.closed)
|
||||
})
|
||||
}
|
||||
|
||||
// Close implements net.Conn.Close.
|
||||
// Sends a CmdClose packet and then shuts down locally.
|
||||
func (c *ProtocolConn) Close() error {
|
||||
c.closeOnce.Do(func() {
|
||||
c.handler.SendClose(c.id, ErrNone)
|
||||
})
|
||||
c.Shutdown()
|
||||
return nil
|
||||
}
|
||||
|
||||
// LocalAddr implements net.Conn.LocalAddr (returns dummy address).
|
||||
func (c *ProtocolConn) LocalAddr() net.Addr {
|
||||
return &protocolAddr{network: "protocol", address: c.id.String()}
|
||||
}
|
||||
|
||||
// RemoteAddr implements net.Conn.RemoteAddr (returns dummy address).
|
||||
func (c *ProtocolConn) RemoteAddr() net.Addr {
|
||||
return &protocolAddr{network: "protocol", address: "remote"}
|
||||
}
|
||||
|
||||
// SetDeadline implements net.Conn.SetDeadline (not implemented).
|
||||
func (c *ProtocolConn) SetDeadline(t time.Time) error {
|
||||
return nil // Not implemented for protocol connections
|
||||
}
|
||||
|
||||
// SetReadDeadline implements net.Conn.SetReadDeadline (not implemented).
|
||||
func (c *ProtocolConn) SetReadDeadline(t time.Time) error {
|
||||
return nil // Not implemented for protocol connections
|
||||
}
|
||||
|
||||
// SetWriteDeadline implements net.Conn.SetWriteDeadline (not implemented).
|
||||
func (c *ProtocolConn) SetWriteDeadline(t time.Time) error {
|
||||
return nil // Not implemented for protocol connections
|
||||
}
|
||||
|
||||
// DeliverData is called by the protocol handler when data arrives for this connection.
|
||||
func (c *ProtocolConn) DeliverData(data []byte) {
|
||||
select {
|
||||
case <-c.closed:
|
||||
return
|
||||
case c.readBuffer <- data:
|
||||
}
|
||||
}
|
||||
|
||||
// protocolAddr implements net.Addr for protocol connections.
|
||||
type protocolAddr struct {
|
||||
network string
|
||||
address string
|
||||
}
|
||||
|
||||
func (a *protocolAddr) Network() string {
|
||||
return a.network
|
||||
}
|
||||
|
||||
func (a *protocolAddr) String() string {
|
||||
return a.address
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
// Package protocol implements the communication protocol between proxy server and agent.
|
||||
// It provides packet encoding/decoding, connection management, and secure data transfer
|
||||
// using ChaCha20-Poly1305.
|
||||
// It provides packet encoding/decoding and connection management.
|
||||
//
|
||||
// The protocol uses a binary packet format with fixed-size headers and variable-length
|
||||
// payloads. Each packet contains a command type, connection ID, and optional data.
|
||||
@@ -51,6 +50,11 @@ func NewPacket(command byte, connectionID uuid.UUID, data []byte) *Packet {
|
||||
}
|
||||
}
|
||||
|
||||
// EncodedSize returns the total size of the encoded packet in bytes.
|
||||
func (p *Packet) EncodedSize() int {
|
||||
return HeaderSize + len(p.Data)
|
||||
}
|
||||
|
||||
// Encode serializes the packet into a byte slice following the protocol format.
|
||||
// Returns nil if any encoding operation fails.
|
||||
func (p *Packet) Encode() []byte {
|
||||
@@ -78,8 +82,8 @@ func (p *Packet) Encode() []byte {
|
||||
}
|
||||
|
||||
// Decode deserializes a byte slice into a protocol packet.
|
||||
// Returns nil if the data is malformed, incomplete, or contains an invalid command.
|
||||
// The input must contain at least HeaderSize bytes and match the encoded length.
|
||||
// Returns nil if the data is malformed or contains an invalid command.
|
||||
// The input must be EXACTLY one complete packet (aznet ensures this).
|
||||
func Decode(data []byte) *Packet {
|
||||
if len(data) < HeaderSize {
|
||||
return nil
|
||||
@@ -94,6 +98,8 @@ func Decode(data []byte) *Packet {
|
||||
copy(id[:], data[CommandSize:CommandSize+UUIDSize])
|
||||
|
||||
dataLength := binary.BigEndian.Uint32(data[CommandSize+UUIDSize : HeaderSize])
|
||||
|
||||
// Check exact size - aznet returns complete messages, so size must match exactly
|
||||
if uint32(len(data)) != uint32(HeaderSize)+dataLength {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -40,5 +40,4 @@ var ErrToString = map[byte]string{
|
||||
|
||||
// Protocol packet errors
|
||||
protocol.ErrInvalidPacket: "invalid protocol packet structure",
|
||||
protocol.ErrInvalidCrypto: "invalid cryptographic operation",
|
||||
}
|
||||
+93
-143
@@ -1,7 +1,7 @@
|
||||
// Package proxy implements a SOCKS proxy server.
|
||||
// It accepts client connections and forwards traffic through transport channels
|
||||
// to remote agents. The server manages connection lifecycle, encryption, and
|
||||
// bidirectional data transfer.
|
||||
// to remote agents. The server manages connection lifecycle and bidirectional
|
||||
// data transfer.
|
||||
package proxy
|
||||
|
||||
import (
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"io"
|
||||
"net"
|
||||
"proxyblob/pkg/protocol"
|
||||
"proxyblob/pkg/transport"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -28,11 +27,11 @@ type ProxyServer struct {
|
||||
Listener net.Listener
|
||||
}
|
||||
|
||||
// NewProxyServer creates a proxy server instance with the given transport.
|
||||
// The transport is used for communication with remote agents.
|
||||
func NewProxyServer(ctx context.Context, transport transport.Transport) *ProxyServer {
|
||||
// NewProxyServer creates a proxy server instance with the given connection.
|
||||
// The connection is used for communication with remote agents.
|
||||
func NewProxyServer(ctx context.Context, conn net.Conn) *ProxyServer {
|
||||
server := &ProxyServer{}
|
||||
server.BaseHandler = protocol.NewBaseHandler(ctx, transport)
|
||||
server.BaseHandler = protocol.NewBaseHandler(ctx, conn)
|
||||
server.PacketHandler = server
|
||||
return server
|
||||
}
|
||||
@@ -69,8 +68,7 @@ func (s *ProxyServer) OnNew(connectionID uuid.UUID, data []byte) byte {
|
||||
return protocol.ErrUnexpectedPacket
|
||||
}
|
||||
|
||||
// OnAck processes connection acknowledgments from agents. It derives a shared
|
||||
// encryption key using the agent's public key and updates the connection state.
|
||||
// OnAck processes connection acknowledgments from agents.
|
||||
// Returns an error code indicating success or failure.
|
||||
func (s *ProxyServer) OnAck(connectionID uuid.UUID, data []byte) byte {
|
||||
value, ok := s.Connections.Load(connectionID)
|
||||
@@ -79,34 +77,20 @@ func (s *ProxyServer) OnAck(connectionID uuid.UUID, data []byte) byte {
|
||||
}
|
||||
conn := value.(*protocol.Connection)
|
||||
|
||||
if conn.State != protocol.StateNew {
|
||||
// Check if connection already established (ProtocolConn should be nil for new connections)
|
||||
if conn.ProtocolConn != nil {
|
||||
return protocol.ErrInvalidState
|
||||
}
|
||||
|
||||
clientPublicKey := data[:32]
|
||||
|
||||
// At this point,conn.SecretKey contains [24B nonce][32B server private key]
|
||||
nonce := conn.SecretKey[:24]
|
||||
serverPrivateKey := conn.SecretKey[24:]
|
||||
|
||||
// Derive the shared key
|
||||
symmetricKey, errCode := protocol.DeriveKey(serverPrivateKey, clientPublicKey, nonce)
|
||||
if errCode != protocol.ErrNone {
|
||||
return errCode
|
||||
}
|
||||
|
||||
// Store the symmetric key
|
||||
conn.SecretKey = symmetricKey
|
||||
|
||||
// Signal connection acknowledgment (non-blocking)
|
||||
conn.ReadBuffer <- []byte{}
|
||||
conn.State = protocol.StateConnected
|
||||
// Create the virtual protocol connection (marks connection as established)
|
||||
conn.ProtocolConn = protocol.NewProtocolConn(s.Ctx, connectionID, s.BaseHandler)
|
||||
conn.StartDelivery()
|
||||
conn.LastActivity = time.Now()
|
||||
return protocol.ErrNone
|
||||
}
|
||||
|
||||
// OnData processes data received from agents. It decrypts the data and forwards
|
||||
// it to the client. Returns an error code indicating success or failure.
|
||||
// OnData processes data received from agents and forwards it to the client.
|
||||
// Returns an error code indicating success or failure.
|
||||
func (s *ProxyServer) OnData(connectionID uuid.UUID, data []byte) byte {
|
||||
value, ok := s.Connections.Load(connectionID)
|
||||
if !ok {
|
||||
@@ -115,20 +99,13 @@ func (s *ProxyServer) OnData(connectionID uuid.UUID, data []byte) byte {
|
||||
conn := value.(*protocol.Connection)
|
||||
conn.LastActivity = time.Now()
|
||||
|
||||
decrypted, errCode := protocol.Decrypt(conn.SecretKey, data)
|
||||
if errCode != protocol.ErrNone {
|
||||
return errCode
|
||||
}
|
||||
data = decrypted
|
||||
|
||||
// Writing to client is handled by forwardToClient goroutine
|
||||
select {
|
||||
case <-s.Ctx.Done():
|
||||
return protocol.ErrConnectionClosed
|
||||
case conn.ReadBuffer <- data:
|
||||
return protocol.ErrNone
|
||||
|
||||
// Non-blocking delivery to per-connection goroutine
|
||||
if conn.ProtocolConn != nil {
|
||||
if !conn.Deliver(data) {
|
||||
return protocol.ErrBufferFull
|
||||
}
|
||||
}
|
||||
return protocol.ErrNone
|
||||
}
|
||||
|
||||
// OnClose handles connection termination from agents. It cleans up the
|
||||
@@ -141,7 +118,18 @@ func (s *ProxyServer) OnClose(connectionID uuid.UUID, errorCode byte) byte {
|
||||
conn := value.(*protocol.Connection)
|
||||
conn.Close()
|
||||
s.Connections.Delete(connectionID)
|
||||
return errorCode
|
||||
return protocol.ErrNone
|
||||
}
|
||||
|
||||
// cleanupConnection closes all connection resources and removes from connection map.
|
||||
// This helper reduces code duplication in handleConnection.
|
||||
func (s *ProxyServer) cleanupConnection(connID uuid.UUID, clientConn net.Conn, proxyConn *protocol.Connection) {
|
||||
clientConn.Close()
|
||||
if proxyConn.ProtocolConn != nil {
|
||||
proxyConn.ProtocolConn.Close()
|
||||
}
|
||||
proxyConn.Close()
|
||||
s.Connections.Delete(connID)
|
||||
}
|
||||
|
||||
// acceptLoop accepts incoming TCP connections and spawns goroutines to handle
|
||||
@@ -178,6 +166,11 @@ func (s *ProxyServer) acceptLoop() {
|
||||
func (s *ProxyServer) handleConnection(clientConn net.Conn) {
|
||||
defer clientConn.Close()
|
||||
|
||||
// Enable TCP_NODELAY to disable Nagle's algorithm for better TLS performance
|
||||
if tcpConn, ok := clientConn.(*net.TCPConn); ok {
|
||||
tcpConn.SetNoDelay(true)
|
||||
}
|
||||
|
||||
connID := uuid.New()
|
||||
proxyConn := protocol.NewConnection(connID)
|
||||
s.Connections.Store(proxyConn.ID, proxyConn)
|
||||
@@ -189,114 +182,71 @@ func (s *ProxyServer) handleConnection(clientConn net.Conn) {
|
||||
return
|
||||
}
|
||||
|
||||
// 2. Wait for agent acknowledgment with timeout
|
||||
select {
|
||||
case <-s.Ctx.Done():
|
||||
s.SendClose(connID, protocol.ErrHandlerStopped)
|
||||
s.Connections.Delete(connID)
|
||||
return
|
||||
case <-time.After(5 * time.Second):
|
||||
s.SendClose(connID, protocol.ErrTransportTimeout)
|
||||
s.Connections.Delete(connID)
|
||||
return
|
||||
case <-proxyConn.ReadBuffer:
|
||||
// Agent acknowledged connection
|
||||
}
|
||||
|
||||
// 3. Connection established, start bidirectional forwarding
|
||||
proxyConn.State = protocol.StateConnected
|
||||
proxyConn.LastActivity = time.Now()
|
||||
|
||||
errCh := make(chan byte, 2)
|
||||
go s.forwardToAgent(clientConn, proxyConn, errCh)
|
||||
go s.forwardToClient(clientConn, proxyConn, errCh)
|
||||
|
||||
// Wait for error, closed connection, or context cancellation
|
||||
select {
|
||||
case <-s.Ctx.Done():
|
||||
// Context cancelled, closing connection
|
||||
case <-proxyConn.Closed:
|
||||
// Connection closed by agent
|
||||
case errCode := <-errCh:
|
||||
if errCode != protocol.ErrNone && errCode != protocol.ErrConnectionClosed {
|
||||
log.Error().Str("msg", ErrToString[errCode]).Msg("Connection error")
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up the connection
|
||||
s.SendClose(connID, protocol.ErrConnectionClosed)
|
||||
proxyConn.Close()
|
||||
s.Connections.Delete(connID)
|
||||
}
|
||||
|
||||
// forwardToAgent reads data from the client connection and forwards it to
|
||||
// the remote agent. It continues until an error occurs or the connection
|
||||
// is closed.
|
||||
func (s *ProxyServer) forwardToAgent(clientConn net.Conn, proxyConn *protocol.Connection, errCh chan<- byte) {
|
||||
buffer := make([]byte, 64*1024)
|
||||
// 2. Wait for agent acknowledgment with timeout (ProtocolConn will be created in OnAck)
|
||||
timeout := time.After(5 * time.Second)
|
||||
ticker := time.NewTicker(50 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
// Check early termination conditions
|
||||
if proxyConn.State == protocol.StateClosed {
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case <-s.Ctx.Done():
|
||||
s.SendClose(connID, protocol.ErrHandlerStopped)
|
||||
s.Connections.Delete(connID)
|
||||
return
|
||||
case <-timeout:
|
||||
s.SendClose(connID, protocol.ErrTransportTimeout)
|
||||
s.Connections.Delete(connID)
|
||||
return
|
||||
case <-ticker.C:
|
||||
if proxyConn.ProtocolConn != nil {
|
||||
// Agent acknowledged connection
|
||||
goto connected
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
connected:
|
||||
// 3. Connection established, start bidirectional forwarding using io.Copy
|
||||
errCh := make(chan error, 2)
|
||||
|
||||
// Client → Agent
|
||||
go func() {
|
||||
_, err := io.Copy(proxyConn.ProtocolConn, clientConn)
|
||||
errCh <- err
|
||||
}()
|
||||
|
||||
// Agent → Client
|
||||
go func() {
|
||||
_, err := io.Copy(clientConn, proxyConn.ProtocolConn)
|
||||
errCh <- err
|
||||
}()
|
||||
|
||||
// Wait for BOTH directions to complete before cleaning up
|
||||
var err1, err2 error
|
||||
for i := 0; i < 2; i++ {
|
||||
select {
|
||||
case <-s.Ctx.Done():
|
||||
s.cleanupConnection(connID, clientConn, proxyConn)
|
||||
return
|
||||
case <-proxyConn.Closed:
|
||||
s.cleanupConnection(connID, clientConn, proxyConn)
|
||||
return
|
||||
default:
|
||||
// Continue processing
|
||||
}
|
||||
|
||||
n, err := clientConn.Read(buffer)
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
errCh <- protocol.ErrConnectionClosed
|
||||
} else if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
||||
errCh <- protocol.ErrTransportTimeout
|
||||
case err := <-errCh:
|
||||
if err1 == nil {
|
||||
err1 = err
|
||||
} else {
|
||||
// General network error
|
||||
errCh <- protocol.ErrNetworkUnreachable
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
errCode := s.SendData(proxyConn.ID, buffer[:n])
|
||||
if errCode != protocol.ErrNone {
|
||||
errCh <- protocol.ErrPacketSendFailed
|
||||
return
|
||||
}
|
||||
|
||||
proxyConn.LastActivity = time.Now()
|
||||
}
|
||||
}
|
||||
|
||||
// forwardToClient reads data from the connection's read buffer and forwards
|
||||
// it to the client. It continues until an error occurs or the connection
|
||||
// is closed.
|
||||
func (s *ProxyServer) forwardToClient(clientConn net.Conn, proxyConn *protocol.Connection, errCh chan<- byte) {
|
||||
for {
|
||||
select {
|
||||
case <-s.Ctx.Done():
|
||||
return
|
||||
case <-proxyConn.Closed:
|
||||
return
|
||||
case data := <-proxyConn.ReadBuffer:
|
||||
proxyConn.LastActivity = time.Now()
|
||||
_, err := clientConn.Write(data)
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
errCh <- protocol.ErrConnectionClosed
|
||||
} else if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
||||
errCh <- protocol.ErrTransportTimeout
|
||||
} else {
|
||||
// General network error
|
||||
errCh <- protocol.ErrNetworkUnreachable
|
||||
}
|
||||
return
|
||||
err2 = err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Both directions finished - NOW clean up
|
||||
s.cleanupConnection(connID, clientConn, proxyConn)
|
||||
|
||||
// Log errors if any
|
||||
for _, err := range []error{err1, err2} {
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
log.Debug().Err(err).Str("conn_id", connID.String()).Msg("Connection closed with error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+66
-86
@@ -2,6 +2,8 @@ package proxy
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
@@ -36,38 +38,33 @@ func (h *SocksHandler) handleConnect(conn *protocol.Connection, cmdData []byte)
|
||||
return errCode
|
||||
}
|
||||
|
||||
fmt.Printf("[CONNECT] %s\n", target)
|
||||
|
||||
// Establish TCP connection to target
|
||||
targetConn, err := net.DialTimeout("tcp", target, 10*time.Second)
|
||||
if err != nil {
|
||||
// Map network error to appropriate SOCKS5 error code
|
||||
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
||||
errCode = protocol.ErrTTLExpired
|
||||
} else if opErr, ok := err.(*net.OpError); ok {
|
||||
if opErr.Op == "dial" {
|
||||
errCode = protocol.ErrNetworkUnreachable
|
||||
} else if opErr.Op == "read" {
|
||||
errCode = protocol.ErrHostUnreachable
|
||||
}
|
||||
} else if _, ok := err.(*net.DNSError); ok {
|
||||
errCode = protocol.ErrHostUnreachable
|
||||
} else {
|
||||
errCode = protocol.ErrConnectionRefused
|
||||
}
|
||||
|
||||
// Send failure response with appropriate code
|
||||
// Map network error to appropriate protocol error code
|
||||
errCode = protocol.MapNetError(err)
|
||||
h.SendError(conn, errCode)
|
||||
return errCode
|
||||
}
|
||||
|
||||
// Enable TCP_NODELAY to disable Nagle's algorithm for better TLS performance
|
||||
if tcpConn, ok := targetConn.(*net.TCPConn); ok {
|
||||
tcpConn.SetNoDelay(true)
|
||||
}
|
||||
|
||||
// Send success response
|
||||
// Use stack allocation for fixed-size response (10 bytes)
|
||||
localAddr := targetConn.LocalAddr().(*net.TCPAddr)
|
||||
response := make([]byte, 10)
|
||||
response[0] = Version5
|
||||
response[1] = Succeeded
|
||||
response[2] = 0x00
|
||||
response[3] = IPv4
|
||||
copy(response[4:8], localAddr.IP.To4())
|
||||
binary.BigEndian.PutUint16(response[8:], uint16(localAddr.Port))
|
||||
var responseBuf [10]byte
|
||||
responseBuf[0] = Version5
|
||||
responseBuf[1] = Succeeded
|
||||
responseBuf[2] = 0x00
|
||||
responseBuf[3] = IPv4
|
||||
copy(responseBuf[4:8], localAddr.IP.To4())
|
||||
binary.BigEndian.PutUint16(responseBuf[8:], uint16(localAddr.Port))
|
||||
response := responseBuf[:]
|
||||
|
||||
errCode = h.SendData(conn.ID, response)
|
||||
if errCode != protocol.ErrNone {
|
||||
@@ -75,98 +72,81 @@ func (h *SocksHandler) handleConnect(conn *protocol.Connection, cmdData []byte)
|
||||
return protocol.ErrPacketSendFailed
|
||||
}
|
||||
|
||||
// Store connection and set state
|
||||
// Store connection (state is already established via ProtocolConn)
|
||||
conn.Conn = targetConn
|
||||
conn.State = protocol.StateConnected
|
||||
|
||||
// Start data transfer
|
||||
return h.handleTCPDataTransfer(conn, targetConn)
|
||||
}
|
||||
|
||||
// handleTCPDataTransfer manages bidirectional data transfer for TCP connections.
|
||||
// It spawns two goroutines:
|
||||
// - One reads from client and writes to target
|
||||
// - One reads from target and writes to client
|
||||
// Uses io.Copy for efficient data transfer: io.Copy(dst, src)
|
||||
//
|
||||
// The transfer continues until either:
|
||||
// - The connection is closed by either end
|
||||
// - The context is canceled
|
||||
// - An error occurs
|
||||
func (h *SocksHandler) handleTCPDataTransfer(conn *protocol.Connection, tcpConn net.Conn) byte {
|
||||
// Create channels for communication
|
||||
clientToTarget := make(chan []byte)
|
||||
targetToClient := make(chan []byte)
|
||||
errorCh := make(chan byte, 2)
|
||||
errCh := make(chan error, 2)
|
||||
|
||||
// Read from SOCKS client and forward to target
|
||||
// Client → Target
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-conn.Closed:
|
||||
return
|
||||
case <-h.Ctx.Done():
|
||||
return
|
||||
case data, ok := <-conn.ReadBuffer:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
clientToTarget <- data
|
||||
}
|
||||
_, err := io.Copy(tcpConn, conn.ProtocolConn)
|
||||
// Half-close: close write side of target when client→target finishes
|
||||
if tcpCloser, ok := tcpConn.(*net.TCPConn); ok {
|
||||
tcpCloser.CloseWrite()
|
||||
}
|
||||
errCh <- err
|
||||
}()
|
||||
|
||||
// Read from target and forward to SOCKS client
|
||||
// Target → Client
|
||||
go func() {
|
||||
buffer := make([]byte, 128*1024)
|
||||
for {
|
||||
n, err := tcpConn.Read(buffer)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
errorCh <- protocol.ErrConnectionClosed
|
||||
} else if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
||||
errorCh <- protocol.ErrTTLExpired
|
||||
} else {
|
||||
errorCh <- protocol.ErrHostUnreachable
|
||||
}
|
||||
return
|
||||
}
|
||||
// Copy data since buffer will be reused
|
||||
data := make([]byte, n)
|
||||
copy(data, buffer[:n])
|
||||
targetToClient <- data
|
||||
}
|
||||
_, err := io.Copy(conn.ProtocolConn, tcpConn)
|
||||
errCh <- err
|
||||
}()
|
||||
|
||||
// Main data transfer loop
|
||||
for {
|
||||
// Wait for BOTH directions to complete (don't exit early on first error!)
|
||||
// DO NOT listen to conn.Closed here - it will abort the wait loop prematurely!
|
||||
var err1, err2 error
|
||||
for i := 0; i < 2; i++ {
|
||||
select {
|
||||
case <-conn.Closed:
|
||||
tcpConn.Close()
|
||||
return protocol.ErrNone
|
||||
|
||||
case <-h.Ctx.Done():
|
||||
tcpConn.Close()
|
||||
conn.ProtocolConn.Close()
|
||||
return protocol.ErrHandlerStopped
|
||||
|
||||
case errCode := <-errorCh:
|
||||
case <-conn.Closed:
|
||||
tcpConn.Close()
|
||||
return errCode
|
||||
|
||||
case data := <-clientToTarget:
|
||||
_, err := tcpConn.Write(data)
|
||||
if err != nil {
|
||||
tcpConn.Close()
|
||||
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
||||
return protocol.ErrTTLExpired
|
||||
}
|
||||
return protocol.ErrHostUnreachable
|
||||
conn.ProtocolConn.Close()
|
||||
// Drain any remaining errors
|
||||
select {
|
||||
case <-errCh:
|
||||
default:
|
||||
}
|
||||
case data := <-targetToClient:
|
||||
errCode := h.SendData(conn.ID, data)
|
||||
if errCode != protocol.ErrNone {
|
||||
tcpConn.Close()
|
||||
return protocol.ErrPacketSendFailed
|
||||
return protocol.ErrNone
|
||||
|
||||
case err := <-errCh:
|
||||
if err1 == nil {
|
||||
err1 = err
|
||||
} else {
|
||||
err2 = err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Both directions finished - NOW it's safe to close
|
||||
tcpConn.Close()
|
||||
conn.ProtocolConn.Close()
|
||||
|
||||
// Check for errors (ignore normal EOF and closed connections)
|
||||
for _, err := range []error{err1, err2} {
|
||||
if err != nil && !errors.Is(err, io.EOF) && !errors.Is(err, net.ErrClosed) {
|
||||
// Use helper to map network errors to protocol errors
|
||||
if errCode := protocol.MapNetError(err); errCode != protocol.ErrTransportClosed {
|
||||
return errCode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return protocol.ErrNone
|
||||
}
|
||||
|
||||
+137
-91
@@ -5,10 +5,11 @@ package proxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"slices"
|
||||
|
||||
"proxyblob/pkg/protocol"
|
||||
"proxyblob/pkg/transport"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
@@ -20,11 +21,11 @@ type SocksHandler struct {
|
||||
*protocol.BaseHandler
|
||||
}
|
||||
|
||||
// NewSocksHandler creates a SOCKS5 handler with the given transport.
|
||||
// The transport is used for sending and receiving protocol messages.
|
||||
func NewSocksHandler(ctx context.Context, transport transport.Transport) *SocksHandler {
|
||||
// NewSocksHandler creates a SOCKS5 handler with the given connection.
|
||||
// The connection is used for sending and receiving protocol messages.
|
||||
func NewSocksHandler(ctx context.Context, conn net.Conn) *SocksHandler {
|
||||
handler := &SocksHandler{}
|
||||
handler.BaseHandler = protocol.NewBaseHandler(ctx, transport)
|
||||
handler.BaseHandler = protocol.NewBaseHandler(ctx, conn)
|
||||
handler.PacketHandler = handler
|
||||
return handler
|
||||
}
|
||||
@@ -54,27 +55,19 @@ func (h *SocksHandler) OnNew(connectionID uuid.UUID, data []byte) byte {
|
||||
conn := protocol.NewConnection(connectionID)
|
||||
h.Connections.Store(conn.ID, conn)
|
||||
|
||||
// Process the server's nonce and public key from data
|
||||
if len(data) >= 24+32 {
|
||||
// Extract nonce and server public key
|
||||
nonce := data[:24]
|
||||
serverPublicKey := data[24 : 24+32]
|
||||
// Create the virtual protocol connection
|
||||
conn.ProtocolConn = protocol.NewProtocolConn(h.Ctx, connectionID, h.BaseHandler)
|
||||
conn.StartDelivery()
|
||||
|
||||
// Store temporary data for key derivation during ACK
|
||||
tmp := make([]byte, len(nonce)+len(serverPublicKey))
|
||||
copy(tmp[:len(nonce)], nonce)
|
||||
copy(tmp[len(nonce):], serverPublicKey)
|
||||
conn.SecretKey = tmp
|
||||
}
|
||||
|
||||
// Send connection acknowledgment
|
||||
errCode := h.SendConnAck(connectionID)
|
||||
if errCode != protocol.ErrNone {
|
||||
return errCode
|
||||
}
|
||||
|
||||
// Process the connection
|
||||
go h.processConnection(conn)
|
||||
// Send ACK and process in a goroutine so ReceiveLoop never blocks on aznet writes
|
||||
go func() {
|
||||
errCode := h.SendConnAck(connectionID)
|
||||
if errCode != protocol.ErrNone {
|
||||
h.SendClose(connectionID, protocol.ErrConnectionClosed)
|
||||
return
|
||||
}
|
||||
h.processConnection(conn)
|
||||
}()
|
||||
return protocol.ErrNone
|
||||
}
|
||||
|
||||
@@ -85,7 +78,7 @@ func (h *SocksHandler) OnAck(connectionID uuid.UUID, data []byte) byte {
|
||||
}
|
||||
|
||||
// OnData processes incoming data for a connection.
|
||||
// It decrypts the data and forwards it to the connection's read buffer.
|
||||
// Data is forwarded directly (handled by transport layer).
|
||||
func (h *SocksHandler) OnData(connectionID uuid.UUID, data []byte) byte {
|
||||
value, ok := h.Connections.Load(connectionID)
|
||||
if !ok {
|
||||
@@ -93,20 +86,13 @@ func (h *SocksHandler) OnData(connectionID uuid.UUID, data []byte) byte {
|
||||
}
|
||||
conn := value.(*protocol.Connection)
|
||||
|
||||
decrypted, errCode := protocol.Decrypt(conn.SecretKey, data)
|
||||
if errCode != protocol.ErrNone {
|
||||
h.SendClose(connectionID, protocol.ErrInvalidCrypto)
|
||||
return errCode
|
||||
}
|
||||
data = decrypted
|
||||
|
||||
select {
|
||||
case <-h.Ctx.Done():
|
||||
return protocol.ErrConnectionClosed
|
||||
case conn.ReadBuffer <- data:
|
||||
return protocol.ErrNone
|
||||
|
||||
// Non-blocking delivery to per-connection goroutine
|
||||
if conn.ProtocolConn != nil {
|
||||
if !conn.Deliver(data) {
|
||||
return protocol.ErrBufferFull
|
||||
}
|
||||
}
|
||||
return protocol.ErrNone
|
||||
}
|
||||
|
||||
// OnClose cleans up resources associated with a connection.
|
||||
@@ -184,28 +170,52 @@ func (h *SocksHandler) SendError(conn *protocol.Connection, errCode byte) {
|
||||
// handleAuthNegotiation processes the client's authentication method selection.
|
||||
// Currently only the NO AUTHENTICATION REQUIRED (0x00) method is supported.
|
||||
func (h *SocksHandler) handleAuthNegotiation(conn *protocol.Connection) byte {
|
||||
select {
|
||||
case methods := <-conn.ReadBuffer:
|
||||
// Currently we only support NoAuth (0x00)
|
||||
if !slices.Contains(methods, NoAuth) {
|
||||
h.SendError(conn, protocol.ErrAuthFailed)
|
||||
return protocol.ErrAuthFailed
|
||||
}
|
||||
|
||||
// Send response
|
||||
errCode := h.SendData(conn.ID, []byte{Version5, NoAuth})
|
||||
if errCode != protocol.ErrNone {
|
||||
return errCode
|
||||
}
|
||||
|
||||
return protocol.ErrNone
|
||||
|
||||
case <-conn.Closed:
|
||||
// Read SOCKS5 auth packet: [version(1)][nmethods(1)][methods(nmethods)]
|
||||
// Use stack allocation for small fixed-size header
|
||||
var headerBuf [2]byte
|
||||
header := headerBuf[:]
|
||||
if _, err := io.ReadFull(conn.ProtocolConn, header); err != nil {
|
||||
return protocol.ErrConnectionClosed
|
||||
|
||||
case <-h.Ctx.Done():
|
||||
return protocol.ErrHandlerStopped
|
||||
}
|
||||
|
||||
// Check version
|
||||
if header[0] != Version5 {
|
||||
return protocol.ErrInvalidSocksVersion
|
||||
}
|
||||
|
||||
// Read methods
|
||||
nmethods := int(header[1])
|
||||
if nmethods == 0 {
|
||||
return protocol.ErrInvalidPacket
|
||||
}
|
||||
|
||||
// Use stack buffer for typical case (most clients send 1-2 methods)
|
||||
// Max nmethods is 255, but allocate reasonable stack space
|
||||
var methodsBuf [4]byte
|
||||
var methods []byte
|
||||
if nmethods <= 4 {
|
||||
methods = methodsBuf[:nmethods]
|
||||
} else {
|
||||
methods = make([]byte, nmethods)
|
||||
}
|
||||
if _, err := io.ReadFull(conn.ProtocolConn, methods); err != nil {
|
||||
return protocol.ErrConnectionClosed
|
||||
}
|
||||
|
||||
// Currently we only support NoAuth (0x00)
|
||||
if !slices.Contains(methods, NoAuth) {
|
||||
h.SendError(conn, protocol.ErrAuthFailed)
|
||||
return protocol.ErrAuthFailed
|
||||
}
|
||||
|
||||
// Send response
|
||||
response := []byte{Version5, NoAuth}
|
||||
errCode := h.SendData(conn.ID, response)
|
||||
if errCode != protocol.ErrNone {
|
||||
return protocol.ErrConnectionClosed
|
||||
}
|
||||
|
||||
return protocol.ErrNone
|
||||
}
|
||||
|
||||
// handleCommand processes SOCKS5 commands from the client.
|
||||
@@ -218,40 +228,76 @@ func (h *SocksHandler) handleAuthNegotiation(conn *protocol.Connection) byte {
|
||||
//
|
||||
// - BIND (0x02): TCP/IP port binding
|
||||
func (h *SocksHandler) handleCommand(conn *protocol.Connection) byte {
|
||||
select {
|
||||
case cmdData := <-conn.ReadBuffer:
|
||||
// Validate command format
|
||||
if len(cmdData) < 4 {
|
||||
h.SendError(conn, protocol.ErrInvalidPacket)
|
||||
return protocol.ErrInvalidPacket
|
||||
}
|
||||
|
||||
// Check SOCKS version
|
||||
if cmdData[0] != Version5 {
|
||||
h.SendError(conn, protocol.ErrInvalidSocksVersion)
|
||||
return protocol.ErrInvalidSocksVersion
|
||||
}
|
||||
|
||||
var errCode byte
|
||||
switch cmdData[1] {
|
||||
case Connect:
|
||||
errCode = h.handleConnect(conn, cmdData)
|
||||
case Bind:
|
||||
errCode = h.handleBind(conn, cmdData)
|
||||
case UDPAssociate:
|
||||
errCode = h.handleUDPAssociate(conn)
|
||||
default:
|
||||
h.SendError(conn, protocol.ErrUnsupportedCommand)
|
||||
return protocol.ErrUnsupportedCommand
|
||||
}
|
||||
|
||||
return errCode
|
||||
|
||||
case <-conn.Closed:
|
||||
// Read SOCKS5 command header: [version(1)][cmd(1)][rsv(1)][atyp(1)]
|
||||
// Use stack allocation for fixed-size header
|
||||
var headerBuf [4]byte
|
||||
header := headerBuf[:]
|
||||
if _, err := io.ReadFull(conn.ProtocolConn, header); err != nil {
|
||||
return protocol.ErrConnectionClosed
|
||||
case <-h.Ctx.Done():
|
||||
return protocol.ErrHandlerStopped
|
||||
}
|
||||
|
||||
// Check SOCKS version
|
||||
if header[0] != Version5 {
|
||||
h.SendError(conn, protocol.ErrInvalidSocksVersion)
|
||||
return protocol.ErrInvalidSocksVersion
|
||||
}
|
||||
|
||||
cmd := header[1]
|
||||
atyp := header[3]
|
||||
|
||||
// Read address based on address type
|
||||
var addr []byte
|
||||
switch atyp {
|
||||
case IPv4:
|
||||
// Use stack allocation for IPv4 (6 bytes: 4 IP + 2 port)
|
||||
var addrBuf [6]byte
|
||||
addr = addrBuf[:]
|
||||
case IPv6:
|
||||
// Use stack allocation for IPv6 (18 bytes: 16 IP + 2 port)
|
||||
var addrBuf [18]byte
|
||||
addr = addrBuf[:]
|
||||
case Domain:
|
||||
// Read domain length first
|
||||
var lenBuf [1]byte
|
||||
if _, err := io.ReadFull(conn.ProtocolConn, lenBuf[:]); err != nil {
|
||||
return protocol.ErrConnectionClosed
|
||||
}
|
||||
domainLen := int(lenBuf[0])
|
||||
// Domain name can be up to 255 bytes, use heap allocation
|
||||
addr = make([]byte, 1+domainLen+2) // length + domain + port
|
||||
addr[0] = lenBuf[0]
|
||||
if _, err := io.ReadFull(conn.ProtocolConn, addr[1:]); err != nil {
|
||||
return protocol.ErrConnectionClosed
|
||||
}
|
||||
default:
|
||||
h.SendError(conn, protocol.ErrAddressNotSupported)
|
||||
return protocol.ErrAddressNotSupported
|
||||
}
|
||||
|
||||
// Read the address and port
|
||||
if atyp != Domain {
|
||||
if _, err := io.ReadFull(conn.ProtocolConn, addr); err != nil {
|
||||
return protocol.ErrConnectionClosed
|
||||
}
|
||||
}
|
||||
|
||||
// Build complete command data for handlers
|
||||
cmdData := append(header, addr...)
|
||||
|
||||
var errCode byte
|
||||
switch cmd {
|
||||
case Connect:
|
||||
errCode = h.handleConnect(conn, cmdData)
|
||||
case Bind:
|
||||
errCode = h.handleBind(conn, cmdData)
|
||||
case UDPAssociate:
|
||||
errCode = h.handleUDPAssociate(conn)
|
||||
default:
|
||||
h.SendError(conn, protocol.ErrUnsupportedCommand)
|
||||
return protocol.ErrUnsupportedCommand
|
||||
}
|
||||
|
||||
return errCode
|
||||
}
|
||||
|
||||
// handleDataTransfer manages the flow of data between client and target.
|
||||
|
||||
@@ -31,17 +31,8 @@ func (h *SocksHandler) handleUDPAssociate(conn *protocol.Connection) byte {
|
||||
|
||||
udpConn, err := net.ListenUDP("udp", udpAddr)
|
||||
if err != nil {
|
||||
// Determine specific error type
|
||||
errCode := protocol.ErrGeneralSocksFailure
|
||||
if opErr, ok := err.(*net.OpError); ok {
|
||||
if errors.Is(opErr, net.ErrClosed) {
|
||||
errCode = protocol.ErrTransportClosed
|
||||
} else if opErr.Op == "listen" {
|
||||
errCode = protocol.ErrNetworkUnreachable
|
||||
}
|
||||
}
|
||||
|
||||
// Send appropriate error response
|
||||
// Map network error to appropriate protocol error code
|
||||
errCode := protocol.MapNetError(err)
|
||||
h.SendError(conn, errCode)
|
||||
return errCode
|
||||
}
|
||||
@@ -68,9 +59,8 @@ func (h *SocksHandler) handleUDPAssociate(conn *protocol.Connection) byte {
|
||||
return protocol.ErrPacketSendFailed
|
||||
}
|
||||
|
||||
// Store UDP connection and start handling packets
|
||||
// Store UDP connection and start handling packets (state is already established via ProtocolConn)
|
||||
conn.Conn = udpConn
|
||||
conn.State = protocol.StateConnected
|
||||
|
||||
go h.handleUDPPackets(conn)
|
||||
|
||||
@@ -98,7 +88,7 @@ func (h *SocksHandler) handleUDPAssociate(conn *protocol.Connection) byte {
|
||||
// the context is canceled.
|
||||
func (h *SocksHandler) handleUDPPackets(conn *protocol.Connection) {
|
||||
udpConn := conn.Conn.(*net.UDPConn)
|
||||
buffer := make([]byte, 64*1024)
|
||||
buffer := make([]byte, 16*1024) // Reduced from 64KB to 16KB
|
||||
var clientAddr *net.UDPAddr
|
||||
|
||||
// Create a UDP connection for sending to targets
|
||||
@@ -126,7 +116,7 @@ func (h *SocksHandler) handleUDPPackets(conn *protocol.Connection) {
|
||||
|
||||
// Start a goroutine to handle responses
|
||||
go func() {
|
||||
respBuf := make([]byte, 128*1024)
|
||||
respBuf := make([]byte, 16*1024) // Reduced from 128KB to 16KB
|
||||
for {
|
||||
// Check for cancellation
|
||||
select {
|
||||
@@ -138,8 +128,8 @@ func (h *SocksHandler) handleUDPPackets(conn *protocol.Connection) {
|
||||
// Continue processing
|
||||
}
|
||||
|
||||
// Set read deadline
|
||||
if err := targetConn.SetReadDeadline(time.Now().Add(300 * time.Millisecond)); err != nil {
|
||||
// Set read deadline to 100ms for faster response (reduced from 300ms)
|
||||
if err := targetConn.SetReadDeadline(time.Now().Add(100 * time.Millisecond)); err != nil {
|
||||
if errors.Is(err, net.ErrClosed) {
|
||||
return
|
||||
}
|
||||
@@ -252,8 +242,8 @@ func (h *SocksHandler) handleUDPPackets(conn *protocol.Connection) {
|
||||
}
|
||||
|
||||
default:
|
||||
// Set read deadline to prevent blocking forever
|
||||
if err := udpConn.SetReadDeadline(time.Now().Add(300 * time.Millisecond)); err != nil {
|
||||
// Set read deadline to 100ms for faster response (reduced from 300ms)
|
||||
if err := udpConn.SetReadDeadline(time.Now().Add(100 * time.Millisecond)); err != nil {
|
||||
if errors.Is(err, net.ErrClosed) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,266 +0,0 @@
|
||||
// Package transport provides interfaces and implementations for communication
|
||||
// between proxy components. It abstracts the underlying transport mechanism
|
||||
// and ensures reliable data transfer with proper error handling.
|
||||
package transport
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/Azure/azure-storage-blob-go/azblob"
|
||||
)
|
||||
|
||||
// Retry configuration for blob operations.
|
||||
const (
|
||||
InitialRetryDelay = 50 * time.Millisecond // Starting delay between retries
|
||||
MaxRetryDelay = 3 * time.Second // Maximum delay between retries
|
||||
BackoffFactor = 1.5 // Multiplier for exponential backoff
|
||||
)
|
||||
|
||||
// BlobTransport implements the Transport interface using Azure Blob Storage.
|
||||
// It uses separate blobs for reading and writing to provide bidirectional
|
||||
// communication. All operations are retried with exponential backoff.
|
||||
type BlobTransport struct {
|
||||
readBlob azblob.BlockBlobURL // Blob for receiving data
|
||||
writeBlob azblob.BlockBlobURL // Blob for sending data
|
||||
}
|
||||
|
||||
// NewBlobTransport creates a transport that uses the provided blobs for
|
||||
// bidirectional communication. The readBlob is used for receiving data,
|
||||
// and the writeBlob is used for sending data.
|
||||
func NewBlobTransport(readBlob, writeBlob azblob.BlockBlobURL) *BlobTransport {
|
||||
return &BlobTransport{
|
||||
readBlob: readBlob,
|
||||
writeBlob: writeBlob,
|
||||
}
|
||||
}
|
||||
|
||||
// Send writes data to the write blob. It blocks until the data is written
|
||||
// or the context is canceled. Returns an error code indicating success or
|
||||
// specific failure reason.
|
||||
func (t *BlobTransport) Send(ctx context.Context, data []byte) byte {
|
||||
return WriteBlob(ctx, t.writeBlob, data)
|
||||
}
|
||||
|
||||
// Receive reads and clears data from the read blob. It blocks until data
|
||||
// is available or the context is canceled. Returns the read data and an
|
||||
// error code indicating success or failure reason.
|
||||
func (t *BlobTransport) Receive(ctx context.Context) ([]byte, byte) {
|
||||
return WaitForData(ctx, t.readBlob)
|
||||
}
|
||||
|
||||
// IsClosed reports whether the transport is permanently closed.
|
||||
func (t *BlobTransport) IsClosed(errCode byte) bool {
|
||||
return errCode == ErrTransportClosed
|
||||
}
|
||||
|
||||
// WriteBlob attempts to write data to a blob with retry and exponential backoff.
|
||||
// The operation is retried until successful or the context is canceled.
|
||||
// Returns an error code indicating success or specific failure reason.
|
||||
func WriteBlob(ctx context.Context, blobURL azblob.BlockBlobURL, data []byte) byte {
|
||||
retryDelay := InitialRetryDelay
|
||||
|
||||
// Try the operation with unlimited retries
|
||||
for {
|
||||
// Check if blob is empty
|
||||
isEmpty, errCode := IsBlobEmpty(ctx, blobURL)
|
||||
if errCode != ErrNone {
|
||||
return errCode
|
||||
}
|
||||
|
||||
if !isEmpty {
|
||||
// If the blob isn't empty, wait before retrying
|
||||
retryDelay, errCode = WaitDelay(ctx, retryDelay)
|
||||
if errCode != ErrNone {
|
||||
return errCode
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Reset delay when we find empty blob
|
||||
retryDelay = InitialRetryDelay
|
||||
|
||||
// Upload data to the blob
|
||||
_, err := blobURL.Upload(
|
||||
ctx,
|
||||
bytes.NewReader(data),
|
||||
azblob.BlobHTTPHeaders{ContentType: "application/octet-stream"},
|
||||
azblob.Metadata{},
|
||||
azblob.BlobAccessConditions{},
|
||||
azblob.DefaultAccessTier,
|
||||
nil,
|
||||
azblob.ClientProvidedKeyOptions{},
|
||||
azblob.ImmutabilityPolicyOptions{},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
// If upload fails, check context and retry
|
||||
if ctx.Err() != nil {
|
||||
return ErrContextCanceled
|
||||
}
|
||||
|
||||
// Wait before retrying with exponential backoff
|
||||
retryDelay, errCode = WaitDelay(ctx, retryDelay)
|
||||
if errCode != ErrNone {
|
||||
return errCode
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Success
|
||||
return ErrNone
|
||||
}
|
||||
}
|
||||
|
||||
// WaitForData polls a blob until data is available, then reads and clears it.
|
||||
// The operation is retried with exponential backoff until data is found or
|
||||
// the context is canceled. Returns the read data and an error code indicating
|
||||
// success or failure reason.
|
||||
func WaitForData(ctx context.Context, blobURL azblob.BlockBlobURL) ([]byte, byte) {
|
||||
var data []byte
|
||||
retryDelay := InitialRetryDelay
|
||||
|
||||
for {
|
||||
// Check for cancellation
|
||||
if ctx.Err() != nil {
|
||||
return nil, ErrContextCanceled
|
||||
}
|
||||
|
||||
// Poll until the blob contains data
|
||||
isEmpty, errCode := IsBlobEmpty(ctx, blobURL)
|
||||
if errCode != ErrNone {
|
||||
return nil, errCode
|
||||
}
|
||||
|
||||
if isEmpty {
|
||||
// If empty, wait before checking again with exponential backoff
|
||||
retryDelay, errCode = WaitDelay(ctx, retryDelay)
|
||||
if errCode != ErrNone {
|
||||
return nil, errCode
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Reset delay when we find data
|
||||
retryDelay = InitialRetryDelay
|
||||
|
||||
// Found data, download the blob content
|
||||
response, err := blobURL.Download(ctx, 0, azblob.CountToEnd, azblob.BlobAccessConditions{}, false, azblob.ClientProvidedKeyOptions{})
|
||||
if err != nil {
|
||||
return nil, BlobError(err)
|
||||
}
|
||||
|
||||
bodyReader := response.Body(azblob.RetryReaderOptions{MaxRetryRequests: 3})
|
||||
defer bodyReader.Close()
|
||||
|
||||
// Read the data
|
||||
data, err = io.ReadAll(bodyReader)
|
||||
if err != nil {
|
||||
return nil, ErrTransportError
|
||||
}
|
||||
|
||||
// Clear the blob - no retries here
|
||||
errCode = ClearBlob(ctx, blobURL)
|
||||
if errCode != ErrNone {
|
||||
return nil, errCode
|
||||
}
|
||||
|
||||
return data, ErrNone
|
||||
}
|
||||
}
|
||||
|
||||
// IsBlobEmpty checks if a blob is empty by retrieving its properties.
|
||||
// Returns true if the blob has zero content length, and an error code
|
||||
// indicating success or failure reason.
|
||||
func IsBlobEmpty(ctx context.Context, blobURL azblob.BlockBlobURL) (bool, byte) {
|
||||
// Check for cancelation
|
||||
props, err := blobURL.GetProperties(ctx, azblob.BlobAccessConditions{}, azblob.ClientProvidedKeyOptions{})
|
||||
if err != nil {
|
||||
return false, BlobError(err)
|
||||
}
|
||||
|
||||
return props.ContentLength() == 0, ErrNone
|
||||
}
|
||||
|
||||
// ClearBlob empties a blob's contents by uploading an empty byte slice.
|
||||
// The operation is retried with exponential backoff until successful or
|
||||
// the context is canceled. Returns an error code indicating success or
|
||||
// specific failure reason.
|
||||
func ClearBlob(ctx context.Context, blobURL azblob.BlockBlobURL) byte {
|
||||
var errCode byte
|
||||
retryDelay := InitialRetryDelay
|
||||
|
||||
// Try the operation with unlimited retries
|
||||
for {
|
||||
_, err := blobURL.Upload(
|
||||
ctx,
|
||||
bytes.NewReader([]byte{}),
|
||||
azblob.BlobHTTPHeaders{ContentType: "application/octet-stream"},
|
||||
azblob.Metadata{},
|
||||
azblob.BlobAccessConditions{},
|
||||
azblob.DefaultAccessTier,
|
||||
nil,
|
||||
azblob.ClientProvidedKeyOptions{},
|
||||
azblob.ImmutabilityPolicyOptions{},
|
||||
)
|
||||
|
||||
if err == nil {
|
||||
// Success, no error
|
||||
return ErrNone
|
||||
}
|
||||
|
||||
// Wait before retrying with exponential backoff
|
||||
retryDelay, errCode = WaitDelay(ctx, retryDelay)
|
||||
if errCode != ErrNone {
|
||||
return errCode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BlobError maps Azure Blob Storage errors to transport error codes.
|
||||
// It handles common error cases like container not found, network issues,
|
||||
// and authentication failures.
|
||||
func BlobError(err error) byte {
|
||||
// Quick check for nil
|
||||
if err == nil {
|
||||
return ErrNone
|
||||
}
|
||||
|
||||
// Check for context cancellation
|
||||
if errors.Is(err, context.Canceled) {
|
||||
return ErrContextCanceled
|
||||
}
|
||||
|
||||
// Check for container-related errors (indicating transport is closed)
|
||||
if storageErr, ok := err.(azblob.StorageError); ok {
|
||||
serviceCode := storageErr.ServiceCode()
|
||||
if serviceCode == azblob.ServiceCodeContainerNotFound ||
|
||||
serviceCode == azblob.ServiceCodeContainerBeingDeleted ||
|
||||
serviceCode == azblob.ServiceCodeAccountBeingCreated {
|
||||
return ErrTransportClosed
|
||||
}
|
||||
}
|
||||
|
||||
// All other errors are treated as general transport errors
|
||||
return ErrTransportError
|
||||
}
|
||||
|
||||
// WaitDelay implements exponential backoff for retry operations.
|
||||
// It sleeps for the current delay and returns the next delay duration,
|
||||
// which is the current delay multiplied by BackoffFactor, capped at
|
||||
// MaxRetryDelay. Returns an error code if the context is canceled.
|
||||
func WaitDelay(ctx context.Context, retryDelay time.Duration) (time.Duration, byte) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return 0, ErrContextCanceled
|
||||
case <-time.After(retryDelay):
|
||||
retryDelay = time.Duration(float64(retryDelay) * BackoffFactor)
|
||||
if retryDelay > MaxRetryDelay {
|
||||
retryDelay = MaxRetryDelay
|
||||
}
|
||||
return retryDelay, ErrNone
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
// Package transport provides interfaces and implementations for communication
|
||||
// between proxy components. It abstracts the underlying transport mechanism
|
||||
// and ensures reliable data transfer with proper error handling.
|
||||
package transport
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
// Error codes for transport operations.
|
||||
const (
|
||||
ErrNone byte = 0 // Operation completed successfully
|
||||
ErrContextCanceled byte = 2 // Context was canceled during operation
|
||||
|
||||
// Transport errors (20-29)
|
||||
ErrTransportClosed byte = 20 // Transport is permanently closed
|
||||
ErrTransportTimeout byte = 21 // Operation exceeded time limit
|
||||
ErrTransportError byte = 22 // Generic transport error
|
||||
)
|
||||
|
||||
// Transport defines an interface for bidirectional packet communication.
|
||||
// Implementations must ensure reliable delivery and proper error handling.
|
||||
// All methods are safe for concurrent use.
|
||||
type Transport interface {
|
||||
// Send transmits data to the recipient. It blocks until the data is sent
|
||||
// or the context is canceled. Returns an error code indicating success
|
||||
// or specific failure reason.
|
||||
Send(ctx context.Context, data []byte) byte
|
||||
|
||||
// Receive waits for and returns available data. It blocks until data
|
||||
// is available or the context is canceled. Returns the received data
|
||||
// and an error code indicating success or failure reason.
|
||||
Receive(ctx context.Context) ([]byte, byte)
|
||||
|
||||
// IsClosed reports whether the transport is permanently closed.
|
||||
// The error code parameter helps determine the closure reason.
|
||||
IsClosed(byte) bool
|
||||
}
|
||||
Reference in New Issue
Block a user