proxyblob v2 release

This commit is contained in:
atsika
2026-02-16 15:47:28 +01:00
parent d148bc3198
commit 77a7eb53af
21 changed files with 1758 additions and 1710 deletions
+2
View File
@@ -8,6 +8,8 @@ config.json
*-config.json
.DS_Store
.cursor/
CLAUDE.md
AGENTS.md
*.txt
__blobstorage__/
__azurite*.json
+97 -44
View File
@@ -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)
---
Made with ❤️ by [@_atsika](https://x.com/_atsika)
+37 -145
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

+16 -2
View File
@@ -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=="
}
]
}
+25 -23
View File
@@ -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
)
+99 -84
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
}
}()
}
-98
View File
@@ -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
View File
@@ -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
}
+155
View File
@@ -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
}
+10 -4
View File
@@ -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
}
-1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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.
+9 -19
View File
@@ -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
}
-266
View File
@@ -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
}
}
-38
View File
@@ -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
}