Refactor and enhance SOCKS5 server with UDP support
Some checks failed
Go / build (push) Has been cancelled
构建image和发布 / build-and-push (push) Has been cancelled

- Updated `go.mod` and `go.sum` to streamline dependencies.
- Modified `main.go` to add a new action for starting a SOCKS5 server.
- Implemented `AuthUser` method in `schema/trojan.go` for user authentication.
- Enhanced `ProtoVLESS` structure in `schema/vless.go` to include user ID and data handling methods.
- Refactored server package to improve organization and clarity.
- Added a new `socks5.go` file to implement the SOCKS5 protocol and server functionality.
- Created a UDP echo server in `testkit/udp_echo_svr.go` for testing purposes.
- Developed a Python script `testkit/udpcheck.py` to validate UDP functionality through the SOCKS5 proxy.
- Updated `testkit/config.json` to configure the V2Ray client for SOCKS5 with VLESS.
- Added README documentation in `testkit/README.md` for testing UDP over SOCKS5 with VLESS.
This commit is contained in:
Eric Zhou
2025-08-27 16:14:02 +08:00
parent b60caf4869
commit 01e83c12d1
32 changed files with 976 additions and 1255 deletions

24
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,24 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Run Client",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/main.go",
"args": ["--action", "client"]
},
{
"name": "Run Proxy",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/main.go",
"args": []
}
]
}

10
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,10 @@
{
"cSpell.words": [
"DGRAM",
"libragen",
"mojotv",
"socksocket",
"unchainese",
"Upgrader"
]
}

270
README.md
View File

@@ -1,145 +1,195 @@
# Unchain Proxy Server
# Unchain
Unchain is a lightweight and easy-to-use proxy server designed to bypass network restrictions, censorship, and surveillance effectively.
A lightweight, high-performance proxy server for bypassing network restrictions using VLESS over WebSocket with TLS.
[![Go Version](https://img.shields.io/badge/Go-1.23+-blue.svg)](https://golang.org/)
[![License](https://img.shields.io/badge/License-Apache%202.0-green.svg)](LICENSE)
[![Docker](https://img.shields.io/badge/Docker-Supported-blue.svg)](Dockerfile)
## Features
- **VLESS Protocol Support**: Full VLESS over WebSocket with TLS encryption
- **Client Compatible**: Works with v2rayN, v2rayA, Clash, ShadowRocket, and more
- **Lightweight**: Minimal resource footprint with core logic in ~200 lines
- **Production Ready**: Includes traffic metering, health checks, and graceful shutdown
- **Flexible Deployment**: Standalone or integrated with admin servers
- **Memory Efficient**: Optimized goroutine management and resource cleanup
- **High Performance**: Concurrent connections optimized for Go 1.23
## Key Features
- **Protocol Support**: Seamlessly handles TCP and UDP (VLESS) packets over WebSocket with TLS/Cloudflare support.
- **Build Your Own VPN Business**: Provides a robust platform for starting your own VPN services.
- **Compatibility**: Fully compatible with popular proxy clients like v2rayN or any application supporting the VLESS + WebSocket protocol.
## Quick Start
### Prerequisites
- Go 1.23+ or Docker
- Basic proxy/VPN knowledge
## How It Works
Unchain operates as a proxy/VPN server, compatible with popular proxy clients such as v2rayN or any application that supports the VLESS+WebSocket protocol. It accepts traffic from various client applications, including:
- [v2rayN](https://github.com/2dust/v2rayN)
- [v2rayA](https://github.com/v2rayA/v2rayA)
- [Clash](https://github.com/Dreamacro/clash)
- [v2rayNG](https://github.com/2dust/v2rayNG)
- [iOS app ShadowRocket](https://apps.apple.com/us/app/shadowrocket/id932747118)
Unchain processes incoming traffic and securely forwards it to the destination server, ensuring both security and efficiency in communication.
## Unchain Architecture
Unchain is a dead simple VLESS over websocket proxy server.
The core biz logic is only 200 lines of code. [app_ws_vless.go](/node/app_ws_vless.go).
Unchain server uses a simple architecture that is VLESS over WebSocket (WS) + TLS.
```
V2rayN,V2rayA,Clash or ShadowRocket
+------------------+
| VLESS Client |
| +-----------+ |
| | TLS Layer | |
| +-----------+ |
| | WebSocket | |
| +-----------+ |
+--------|---------+
|
| Encrypted VLESS Traffic (wss://)
|
+--------------------------------------+
| Internet (TLS Secured) |
+--------------------------------------+
|
|
+-----------------------------------+
| Reverse Proxy Server |
| (e.g., Nginx or Cloudflare) |
| |
| +---------------------------+ |
| | HTTPS/TLS Termination | |
| +---------------------------+ |
| | WebSocket Proxy (wss://) | |
| +---------------------------+ |
| Forward to VLESS Server |
+------------------|----------------+
|
+--------------------------------+
| Unchain Server |
| |
| +------------------------+ |
| | WebSocket Handler | |
| +------------------------+ |
| | VLESS Core Processing | |
| +------------------------+ |
| |
| Forward Traffic to Target |
+------------------|-------------+
|
+-----------------+
| Target Server |
| or Destination |
+-----------------+
### Install from Source
```bash
git clone https://github.com/unchainese/unchain.git
cd unchain
go mod download
cp config.example.standalone.toml config.toml
# Edit config.toml
go run main.go
```
### Docker
```bash
docker build -t unchain .
docker run -p 80:80 \
-e SUB_ADDRESSES="your-domain.com:443" \
-e ALLOW_USERS="your-uuid" \
unchain
```
## Configuration
Unchain uses TOML config or environment variables.
### config.toml
```toml
SubAddresses = 'domain.com:443'
AppPort = '80'
AllowUsers = 'uuid1,uuid2'
LogFile = ''
DebugLevel = 'info'
EnableDataUsageMetering = 'true'
```
### Environment Variables
```bash
APP_PORT=80
SUB_ADDRESSES=domain.com:443
ALLOW_USERS=uuid1,uuid2
```
## Usage
### 1. Build from Source
### Endpoints
- `/wsv/{uid}` - VLESS WebSocket endpoint
- `/sub/{uid}` - Subscription URL generator
- `/` - Health check
To build from source, follow these steps:
### Get VLESS URLs
```bash
curl http://localhost:80/sub/your-uuid
```
1. Clone the repository and navigate to the `cmd/node` directory:
```sh
cd cmd/node
```
2. Copy the example configuration file and customize it:
```sh
cp config.example.standalone.toml config.toml
```
3. Run the application:
```sh
go run main.go
```
### Client Setup
Import the generated VLESS URL into your client (v2rayN, Clash, etc.).
### 2. Deploying on Your Own Ubuntu Server Using GitHub Actions
## Architecture
You can deploy the application on an Ubuntu server using GitHub Actions. Here's how:
```
Client --VLESS/WS/TLS--> Reverse Proxy --WS--> Unchain --TCP/UDP--> Target
```
1. **Fork the repository** to your GitHub account.
2. **Create an Environment** named `production` in your repository settings.
3. **Add the following SSH connection details** to the Environment Secrets:
- `EC2_HOST`: The SSH host with port (e.g., `1.1.1.1:20`).
- `EC2_USER`: The SSH user (e.g., `ubuntu`).
- `EC2_KEY`: Your SSH private key.
Unchain runs behind a reverse proxy (Nginx/Cloudflare) handling TLS and WebSocket upgrades.
4. **Add your TOML configuration file content** to the Environment Variables:
- `CONFIG_TOML`: Copy the content of your `config.toml` file, replace all `"` with `'`, and paste it here.
## Project Structure
learn more in [.github/workflows/deploy.sh](/.github/workflows/deploy.sh)
```
├── main.go # Entry point
├── server/ # Core server
│ ├── app.go # HTTP server
│ ├── app_ws_vless.go # VLESS handler
│ ├── app_ping.go # Health check
│ └── app_sub.go # Subscription
├── global/ # Utilities
│ ├── config.go # Config management
│ └── logger.go # Logging
├── schema/ # Protocols
│ ├── vless.go # VLESS parser
│ └── trojan.go # Trojan support
├── client/ # Client utilities
│ ├── client.go # SOCKS5 proxy
│ ├── websocket.go # WS client
│ ├── proxy.go # Coordination
│ ├── relay_*.go # Relays
│ ├── socks5_*.go # SOCKS5
│ └── geo.go # GeoIP
├── config.example.standalone.toml
└── Dockerfile
```
## Technology Stack
[Click to view Chinese deployment tutorial video](https://www.bilibili.com/video/BV1wBrmYmEiN/?share_source=copy_web&vd_source=aec70c249b680fe47ccf03c2051714fe)
- **Go 1.23**
- **gorilla/websocket** v1.5.3
- **BurntSushi/toml** v1.4.0
- **google/uuid** v1.6.0
- **oschwald/geoip2-golang** v1.11.0
- **sirupsen/logrus** v1.9.3
## Troubleshooting
### 3. Running the Application
### Common Issues
Once the application is running, you will see a VLESS connection schema URL in the standard output. Copy and paste this URL into your V2rayN client.
1. **Connection Failed**
- Check server status: `curl http://localhost:80/`
- Verify firewall and DNS
- Ensure reverse proxy is configured
Congratulations! You now have your self-hosted proxy server up and running.
2. **WebSocket Errors**
- Confirm proxy supports WS upgrades
- Check TLS certificates
3. **Auth Failed**
- Verify UUID in `AllowUsers`
- Check admin server if used
4. **High Memory**
- Monitor goroutines via `/`
- Restart if >1000 goroutines
### Debug
Set `DebugLevel = 'debug'` and check logs.
### 4. (Optional) create your own admin app for Auth and Data-traffic
## Client Compatibility
create an RESTful API for [chain proxy server push](https://github.com/unchainese/unchain/blob/5ece8c39814684a8a54e8e009d7c888e5988a017/internal/node/app.go#L161) :
[Register API example code](https://github.com/unchainese/unchainadmin/blob/035b2232d4262c24ef70b8ad7abb9faebaaecc96/functions/api/nodes.ts#L34)
- v2rayN (Windows)
- v2rayA (Cross-platform)
- Clash (Cross-platform)
- v2rayNG (Android)
- ShadowRocket (iOS)
## Business Use
## Build your own VPN business
Integrate with admin server for user management, traffic metering, and billing.
Using [the cloudflare page UnchainAdmin](https://github.com/unchainese/unchainadmin) start your own VPN business.
See [UnchainAdmin](https://github.com/unchainese/unchainadmin) for example.
## Performance
- **RAM**: 512MB min, 1GB+ recommended
- **CPU**: 1 core min, 2+ for production
- **Connections**: Thousands concurrent
- **Memory**: ~20MB base + ~1-2MB/100 connections
## Contributing
1. Fork the repo
2. Create feature branch
3. Commit changes
4. Push and open PR
### Development
```bash
git clone https://github.com/yourusername/unchain.git
cd unchain
go mod download
go test ./...
go build
```
## License
Apache License 2.0 - see [LICENSE](LICENSE)
---
⭐ Star if useful! [Issues](https://github.com/unchainese/unchain/issues)

Binary file not shown.

View File

@@ -1,276 +0,0 @@
package client
import (
"context"
"errors"
"fmt"
"io"
"log"
"log/slog"
"net"
"sync"
"time"
)
type ClientLocalSocks5Server struct {
AddrSocks5 string
Timeout time.Duration
proxy *Proxy
}
func NewClientLocalSocks5Server(addr string) (*ClientLocalSocks5Server, error) {
return &ClientLocalSocks5Server{
AddrSocks5: addr,
Timeout: 5 * time.Minute,
}, nil
}
func (ss *ClientLocalSocks5Server) fetchActiveProxy() {
ss.proxy = &Proxy{}
}
func (ss *ClientLocalSocks5Server) Run(ctx context.Context) {
ss.fetchActiveProxy()
listener, err := net.Listen("tcp", ss.AddrSocks5)
if err != nil {
listener, err = net.Listen("tcp4", "127.0.0.1:0")
}
if err != nil {
log.Fatalf("Failed to listen on %s: %v", ss.AddrSocks5, err)
}
ss.AddrSocks5 = listener.Addr().String()
slog.Info("socks5 server listening on", "addr", ss.AddrSocks5)
defer listener.Close()
log.Println("SOCKS5 server listening on: " + ss.AddrSocks5)
//proxySettingOn(ss.AddrSocks5)
//defer proxySettingOff()
// Channel to receive new connections
connCh := make(chan net.Conn, 1)
// Channel to signal accept goroutine to stop
done := make(chan struct{})
defer close(done)
// Start accept goroutine
go func() {
defer close(connCh)
for {
select {
case <-done:
return
default:
conn, err := listener.Accept()
if err != nil {
// Check if the error is due to listener being closed
select {
case <-done:
return // Expected closure
default:
log.Printf("Failed to accept connection: %v", err)
continue
}
}
select {
case connCh <- conn:
case <-done:
conn.Close() // Close connection if we can't send it
return
}
}
}
}()
for {
select {
case <-ctx.Done():
log.Println("socks5 server exit")
return
case conn, ok := <-connCh:
if !ok {
// Connection channel closed, exit
return
}
go ss.handleConnection(ctx, conn)
}
}
}
func (ss *ClientLocalSocks5Server) socks5HandShake(conn net.Conn) error {
buf := make([]byte, 2)
if _, err := io.ReadFull(conn, buf); err != nil {
return fmt.Errorf("failed to read version and nmethods: %w", err)
}
if buf[0] != socks5Version {
return fmt.Errorf("socks5 only. unsupported SOCKS version: %d", buf[0])
}
// Read the supported authentication methods
nMethods := int(buf[1])
nMethodsData := make([]byte, nMethods)
if _, err := io.ReadFull(conn, nMethodsData); err != nil {
return fmt.Errorf("failed to read methods: %w", err)
}
// Select no authentication (0x00)
if _, err := conn.Write([]byte{socks5Version, 0x00}); err != nil {
return fmt.Errorf("failed to write method selection: %w", err)
}
return nil
}
func (ss *ClientLocalSocks5Server) socks5Request(conn net.Conn) (*Socks5Request, error) {
buf := make([]byte, 8<<10)
n, err := conn.Read(buf)
if err != nil {
return nil, fmt.Errorf("failed to read request: %w", err)
}
data := buf[:n]
if len(data) < 4 {
return nil, fmt.Errorf("request too short")
}
return parseSocks5Request(data)
}
func (ss *ClientLocalSocks5Server) handleConnection(outerCtx context.Context, conn net.Conn) {
defer conn.Close() // the outer for loop is not suitable for defer, so defer close here
ctx, cf := context.WithTimeout(outerCtx, ss.Timeout)
defer cf()
err := ss.socks5HandShake(conn)
if err != nil {
slog.Error("failed to handshake", "err", err.Error())
socks5Response(conn, net.IPv4zero, 0, socks5ReplyFail)
return
}
req, err := ss.socks5Request(conn)
if err != nil {
slog.Error("failed to parse socks5 request", "err", err.Error())
socks5Response(conn, net.IPv4zero, 0, socks5ReplyFail)
return
}
req.Logger().Info("remote target")
if req.socks5Cmd == socks5CmdConnect { //tcp
relayTcpSvr, err := ss.dispatchRelayTcpServer(ctx, req)
if err != nil {
slog.Error("failed to dispatch relay tcp server", "err", err.Error())
socks5Response(conn, net.IPv4zero, 0, socks5ReplyFail)
return
}
socks5Response(conn, net.IPv4zero, 0, socks5ReplyOkay)
defer relayTcpSvr.Close()
ss.pipeTcp(ctx, conn, relayTcpSvr)
return
} else if req.socks5Cmd == socks5CmdUdpAssoc {
udpH, err := NewRelayUdpDirect(conn)
if err != nil {
slog.Error("failed to create udp handler", "err", err.Error())
socks5Response(conn, net.IPv4zero, 0, socks5ReplyFail)
return
}
defer udpH.Close()
udpH.PipeUdp()
return
} else if req.socks5Cmd == socks5CmdBind {
relayBind(conn, req)
return
} else {
err = fmt.Errorf("unknown command: %d", req.socks5Cmd)
slog.Error("unknown command", "err", err.Error())
socks5Response(conn, net.IPv4zero, 0, socks5ReplyFail)
}
}
func (ss *ClientLocalSocks5Server) shouldGoDirect(req *Socks5Request) (goDirect bool) {
if req.CountryCode == "CN" || req.CountryCode == "" {
//empty means geo ip failed or local address
return true
}
return false
}
func (ss *ClientLocalSocks5Server) dispatchRelayTcpServer(ctx context.Context, req *Socks5Request) (io.ReadWriteCloser, error) {
if ss.shouldGoDirect(req) {
req.Logger().Info("go direct")
return NewRelayTcpDirect(req)
}
return NewRelayTcpSocks5e(ctx, ss.proxy, req)
}
func (ss *ClientLocalSocks5Server) pipeTcp(ctx context.Context, s5 net.Conn, relayRw io.ReadWriter) {
// Create cancellable context for proper goroutine cleanup
ctx, cancel := context.WithCancel(ctx)
defer cancel() // Ensure both goroutines exit when function returns
wg := sync.WaitGroup{}
wg.Add(2)
go func() {
span := slog.With("fn", "ws -> s5")
defer func() {
span.Debug("wg1 done")
cancel() // Cancel context if this goroutine exits
wg.Done()
}()
for {
select {
case <-ctx.Done():
span.Info("ctx.Done exit")
return
default:
//ws.SetReadDeadline(time.Now().Add(1 * time.Second))
buf := make([]byte, 8<<10)
n, err := relayRw.Read(buf)
if err != nil {
span.Error("relay read", "err", err.Error())
return
}
_, err = s5.Write(buf[:n])
if err != nil {
span.Error("s5 write", "err", err.Error())
return
}
}
}
}()
go func() { // s5 -> ws
span := slog.With("fn", "s5 -> ws")
defer func() {
span.Debug("wg2 done")
cancel() // Cancel context if this goroutine exits
wg.Done()
}()
for {
select {
case <-ctx.Done():
span.Debug("ctx.Done exit")
return
default:
buf := make([]byte, 8<<10)
//s5.SetReadDeadline(time.Now().Add(20 * time.Millisecond))
n, err := s5.Read(buf)
if errors.Is(err, io.EOF) {
slog.Info("s5 read EOF")
return
}
if err != nil {
et := fmt.Sprintf("%T", err)
span.With("errType", et).Error("s5 read", "err", err.Error())
return
}
//ws.SetWriteDeadline(time.Now().Add(1 * time.Second))
n, err = relayRw.Write(buf[:n])
if err != nil {
span.Error("relay write", "err", err.Error())
return
}
}
}
}()
wg.Wait()
slog.Debug("2 goroutines is Done")
}

View File

@@ -1,42 +0,0 @@
package client
import (
_ "embed"
"log/slog"
"net"
)
const (
socks5Version = 0x05
socks5ReplyOkay = 0x00
socks5ReplyFail = 0x01
socks5ReplyReserved = 0x00
socks5CmdConnect = 0x01
socks5CmdBind = 0x02
socks5CmdUdpAssoc = 0x03
socks5AtypeIPv4 = 0x01
socks5AtypeDomain = 0x03
socks5AtypeIPv6 = 0x04
socks5UdpFragNotSupported = 0x00
socks5UdpFragEnd = 0x80
bufferSize = 64 << 10
)
func socks5Response(conn net.Conn, ipv4 net.IP, port int, socks5OkayOrFail byte) {
if socks5OkayOrFail != socks5ReplyOkay {
ipv4 = net.IPv4zero
port = 0
}
if ipv4 == nil {
ipv4 = net.IPv4zero
}
if port < 0 || port > 65535 {
port = 0
}
response := []byte{socks5Version, socks5OkayOrFail, socks5ReplyReserved, socks5AtypeIPv4, ipv4[0], ipv4[1], ipv4[2], ipv4[3], byte(port >> 8), byte(port & 0xff)}
_, err := conn.Write(response)
if err != nil {
slog.Error("socks5 request rely failed to write", "err", err.Error())
}
}

View File

@@ -1,51 +0,0 @@
package client
import (
_ "embed"
"fmt"
"github.com/oschwald/geoip2-golang"
"log/slog"
"net"
)
//go:embed GeoLite2-Country.mmdb
var geoData []byte
var geoDB *geoip2.Reader
func init() {
db, err := geoip2.FromBytes(geoData)
if err != nil {
slog.Error("failed to load geo database", "err", err)
} else {
geoDB = db
}
}
func GeoDbClose() {
if geoDB != nil {
geoDB.Close()
}
}
func Country(hostOrIp string) (isoCountryCode string, err error) {
if geoDB == nil {
return "", fmt.Errorf("geo databse is nil")
}
ip := net.ParseIP(hostOrIp)
if ip == nil {
ips, err := net.LookupIP(hostOrIp)
if err != nil {
return "", fmt.Errorf("failed to lookup IP: %w", err)
}
if len(ips) == 0 {
return "", fmt.Errorf("no IP found for %s", hostOrIp)
}
ip = ips[0]
}
record, err := geoDB.Country(ip)
if err != nil {
return "", fmt.Errorf("failed to get Country: %w", err)
}
return record.Country.IsoCode, nil
}

View File

@@ -1,42 +0,0 @@
package client
import (
"fmt"
"strings"
)
type Proxy struct {
Name string
Protocol string //ws,wss,http2,tls,http3
Host string
Uri string
Sni string
Version string // one socks5
UserID string
Password string
TrafficKb int64
SpeedMs int64
Status string //active, inactive
}
func (p *Proxy) RelayURL() string {
switch p.Protocol {
case "ws":
return fmt.Sprintf("ws://%s/%s", p.Host, strings.TrimPrefix(p.Uri, "/"))
case "wss":
return fmt.Sprintf("wss://%s/%s", p.Host, strings.TrimPrefix(p.Uri, "/"))
case "tcp+tls":
return fmt.Sprintf("tcp-tls://%s/%s", p.Host, strings.TrimPrefix(p.Uri, "/"))
default:
return ""
}
}
func (p *Proxy) EnableEarlyData() bool {
//https://xtls.github.io/config/transports/websocket.html#websocketobject
if strings.Contains(p.Uri, "?ed=") {
return true
}
return false
}

View File

@@ -1,9 +0,0 @@
package client
import "io"
type RelayTcp interface {
io.Reader
io.Writer
io.Closer
}

View File

@@ -1,29 +0,0 @@
package client
import "net"
var _ RelayTcp = (*RelayTcpDirect)(nil)
type RelayTcpDirect struct {
conn net.Conn
}
func NewRelayTcpDirect(req *Socks5Request) (*RelayTcpDirect, error) {
conn, err := net.Dial("tcp", req.addr())
if err != nil {
return nil, err
}
return &RelayTcpDirect{conn: conn}, nil
}
func (r *RelayTcpDirect) Read(p []byte) (n int, err error) {
return r.conn.Read(p)
}
func (r *RelayTcpDirect) Write(p []byte) (n int, err error) {
return r.conn.Write(p)
}
func (r *RelayTcpDirect) Close() error {
return r.conn.Close()
}

View File

@@ -1,61 +0,0 @@
package client
import (
"context"
"github.com/gorilla/websocket"
"log/slog"
"time"
)
type RelayTcpSocks5e struct {
cfg *Proxy
req *Socks5Request
conn *websocket.Conn
}
func NewRelayTcpSocks5e(ctx context.Context, cfg *Proxy, req *Socks5Request) (*RelayTcpSocks5e, error) {
ws, err := webSocketConn(ctx, cfg, req)
if err != nil {
return nil, err
}
ws.SetCloseHandler(func(code int, text string) error {
slog.Debug("ws has closed", "code", code, "text", text)
return nil
})
return &RelayTcpSocks5e{cfg: cfg, req: req, conn: ws}, nil
}
func (r RelayTcpSocks5e) Read(data []byte) (n int, err error) {
if r.conn != nil {
_, p, err := r.conn.ReadMessage()
if err != nil {
slog.Error("failed to read ws", "err", err.Error())
}
return copy(data, p), err
}
return 0, nil
}
func (r RelayTcpSocks5e) Write(data []byte) (n int, err error) {
if r.conn != nil {
err = r.conn.WriteMessage(websocket.BinaryMessage, data)
if err != nil {
slog.Error("failed to write ws", "err", err.Error())
}
return len(data), err
}
return 0, nil
}
func (r RelayTcpSocks5e) Close() error {
if r.conn != nil {
err := r.conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""), time.Now().Add(time.Millisecond*20))
if err != nil {
slog.Error("failed to close ws", "err", err.Error())
}
return r.conn.Close()
}
return nil
}
var _ RelayTcp = (*RelayTcpSocks5e)(nil)

View File

@@ -1,283 +0,0 @@
package client
import (
"errors"
"fmt"
"io"
"log"
"log/slog"
"net"
"sort"
"sync"
"time"
)
type RelayUdpDirect struct {
s5 net.Conn
relayUdp *net.UDPConn
// Reassembly queue for fragmented UDP packets.
mu sync.Mutex
fragments map[string][]*udpPacket // Map of DstAddr to fragments
highestFrag map[string]byte // Track highest FRAG value for each DstAddr
timers map[string]*time.Timer // Map of DstAddr to reassembly timer
timerDuration time.Duration // Timer duration
}
func (ud *RelayUdpDirect) addFragment(clientAddr *net.UDPAddr, frag *udpPacket) {
ud.mu.Lock()
defer ud.mu.Unlock()
clientDstAddr := ud.clientDstAddrAsID(clientAddr, frag.dstAddr())
// Initialize fragment queue and timer if not already present.
if _, exists := ud.fragments[clientDstAddr]; !exists {
ud.fragments[clientDstAddr] = []*udpPacket{}
ud.highestFrag[clientDstAddr] = socks5UdpFragNotSupported
ud.startTimer(clientDstAddr)
}
// Update highest FRAG value.
if frag.Frag > ud.highestFrag[clientDstAddr] {
ud.highestFrag[clientDstAddr] = frag.Frag
}
// Add fragment to the queue.
ud.fragments[clientDstAddr] = append(ud.fragments[clientDstAddr], frag)
// Check if this is the final fragment (end-of-fragment sequence).
if frag.Frag == socks5UdpFragEnd || frag.Frag == socks5UdpFragNotSupported { // High-order bit indicates end of sequence.
ud.assembleThenPipeUdp(clientAddr, frag.dstAddr())
}
}
func (ud *RelayUdpDirect) startTimer(ClientDstAddr string) {
if timer, exists := ud.timers[ClientDstAddr]; exists {
timer.Stop()
}
ud.timers[ClientDstAddr] = time.AfterFunc(ud.timerDuration, func() {
ud.mu.Lock()
defer ud.mu.Unlock()
delete(ud.fragments, ClientDstAddr)
delete(ud.highestFrag, ClientDstAddr)
delete(ud.timers, ClientDstAddr)
})
}
func (ud *RelayUdpDirect) clientDstAddrAsID(clientAddr *net.UDPAddr, dstAddr string) string {
return fmt.Sprintf("%s/%s", clientAddr, dstAddr)
}
func (ud *RelayUdpDirect) assembleThenPipeUdp(clientAddr *net.UDPAddr, dstAddr string) {
var data []byte
clientDstAddr := ud.clientDstAddrAsID(clientAddr, dstAddr)
fragments := ud.fragments[clientDstAddr]
if len(fragments) == 0 {
return // No fragments to process
}
// Sort fragments by FRAG value.
sort.Slice(fragments, func(i, j int) bool {
return fragments[i].Frag < fragments[j].Frag
})
for _, frag := range fragments {
data = append(data, frag.Data...)
}
comboPacket := fragments[0]
comboPacket.Data = data
// Clean up after successful reassembly - ensure timer is stopped
if timer, exists := ud.timers[clientDstAddr]; exists {
timer.Stop()
delete(ud.timers, clientDstAddr)
}
delete(ud.fragments, clientDstAddr)
delete(ud.highestFrag, clientDstAddr)
ud.segmentPipe(comboPacket, clientAddr)
}
func (ud *RelayUdpDirect) PipeUdp() {
buf := make([]byte, bufferSize)
for {
n, clientAddr, err := ud.relayUdp.ReadFromUDP(buf)
if errors.Is(err, io.EOF) {
return
}
//I will not verify the `clientAddr` because this SOCKS5 proxy is intended for local relay to bypass the GFW.
if err != nil {
slog.Error("Error reading UDP data", "err", err.Error())
continue
}
packet, err := parseUDPData(buf[:n])
if err != nil {
log.Println("Error parsing UDP data", err)
continue
}
ud.addFragment(clientAddr, packet)
}
}
func (ud *RelayUdpDirect) segmentPipe(comboPacket *udpPacket, clientAddr *net.UDPAddr) {
resp, err := forwardUDPData(comboPacket)
if err != nil {
slog.Error("Error forwarding UDP data", "err", err.Error())
return
}
header := comboPacket.ResponseData(resp)
_, err = ud.relayUdp.WriteToUDP(header, clientAddr)
if err != nil {
slog.Error("Error sending UDP response", "err", err.Error())
}
}
func NewRelayUdpDirect(s5 net.Conn) (*RelayUdpDirect, error) {
udpAddr := &net.UDPAddr{IP: net.IPv4zero, Port: 0}
udpConn, err := net.ListenUDP("udp", udpAddr)
if err != nil {
return nil, fmt.Errorf("failed to bind UDP socket: %w", err)
}
ud := &RelayUdpDirect{
s5: s5,
relayUdp: udpConn,
mu: sync.Mutex{},
fragments: make(map[string][]*udpPacket),
highestFrag: make(map[string]byte),
timers: make(map[string]*time.Timer),
timerDuration: time.Second * 60,
}
boundAddr := udpConn.LocalAddr().(*net.UDPAddr)
response := []byte{
socks5Version, socks5ReplyOkay, socks5ReplyReserved, socks5AtypeIPv4,
boundAddr.IP[0], boundAddr.IP[1], boundAddr.IP[2], boundAddr.IP[3],
byte(boundAddr.Port >> 8), byte(boundAddr.Port & 0xFF),
}
_, err = ud.s5.Write(response)
if err != nil {
return nil, fmt.Errorf("failed to send response to client: %w", err)
}
return ud, nil
}
func forwardUDPData(udpPacket *udpPacket) ([]byte, error) {
conn, err := net.DialUDP("udp", nil, udpPacket.addr())
if err != nil {
return nil, err
}
defer conn.Close()
_, err = conn.Write(udpPacket.Data)
if err != nil {
return nil, err
}
buf := make([]byte, bufferSize)
n, _, err := conn.ReadFromUDP(buf)
if err != nil {
return nil, err
}
return buf[:n], nil
}
type udpPacket struct {
RSV [2]byte // reserved
Frag byte // fragment
AType byte // dst address type
Addr []byte // dst address
Port []byte // dst port
Data []byte // payload
}
func (p udpPacket) ResponseData(payload []byte) []byte {
header := []byte{p.RSV[0], p.RSV[1], 0, p.AType}
header = append(header, p.Addr...)
header = append(header, p.Port...)
return append(header, payload...)
}
func parseUDPData(data []byte) (*udpPacket, error) {
if len(data) < 4 {
return nil, fmt.Errorf("invalid UDP packet")
}
// parse header
var packet = udpPacket{
RSV: [2]byte{data[0], data[1]},
Frag: data[2],
AType: data[3],
}
switch packet.AType {
case socks5AtypeIPv4:
if len(data) < 10 {
return nil, fmt.Errorf("invalid IPv4 UDP packet")
}
packet.Addr = data[4 : 4+net.IPv4len]
packet.Port = data[4+net.IPv4len : 4+net.IPv4len+2]
packet.Data = data[4+net.IPv4len+2:]
case socks5AtypeIPv6:
if len(data) < 22 {
return nil, fmt.Errorf("invalid IPv6 UDP packet")
}
packet.Addr = data[4 : 4+net.IPv6len]
packet.Port = data[4+net.IPv6len : 4+net.IPv6len+2]
packet.Data = data[4+net.IPv6len+2:]
case socks5AtypeDomain:
if len(data) < 7 {
return nil, fmt.Errorf("invalid domain UDP packet")
}
addrLen := int(data[4])
packet.Addr = data[5 : 5+addrLen]
packet.Port = data[5+addrLen : 5+addrLen+2]
packet.Data = data[5+addrLen+2:]
default:
return nil, fmt.Errorf("unsupported address type: %d", packet.AType)
}
return &packet, nil
}
func (p udpPacket) ip() net.IP {
switch p.AType {
case socks5AtypeIPv4, socks5AtypeIPv6:
return p.Addr
case socks5AtypeDomain:
ips, err := net.LookupIP(string(p.Addr))
if err != nil {
slog.Error("failed to resolve domain", "err", err.Error())
return net.IPv4zero
}
if len(ips) == 0 {
return net.IPv4zero
}
return ips[0]
default:
return net.IPv4zero
}
}
func (p udpPacket) port() int {
return int(p.Port[0])<<8 + int(p.Port[1])
}
func (p udpPacket) addr() *net.UDPAddr {
return &net.UDPAddr{IP: p.ip(), Port: p.port()}
}
func (p udpPacket) dstAddr() string {
return fmt.Sprintf("%s:%d", p.ip(), p.port())
}
func (ud *RelayUdpDirect) Close() {
// Clean up all timers before closing
ud.mu.Lock()
for clientDstAddr, timer := range ud.timers {
timer.Stop()
delete(ud.timers, clientDstAddr)
}
// Clear all maps to prevent memory leaks
clear(ud.fragments)
clear(ud.highestFrag)
ud.mu.Unlock()
//s5 has already been closed in outside
if ud.relayUdp != nil {
err := ud.relayUdp.Close()
if err != nil {
log.Println("close udp conn failed: ", err)
}
}
}

View File

@@ -1,66 +0,0 @@
package client
import (
"io"
"log/slog"
"net"
"sync"
)
func relayBind(s5 net.Conn, _ *Socks5Request) {
bindListener, err := net.Listen("tcp4", ":0")
if err != nil {
slog.Error("bind tcp failed", "err", err)
socks5Response(s5, net.IPv4zero, 0, socks5ReplyFail)
return
}
defer bindListener.Close()
//first reply
localAddr := bindListener.Addr().(*net.TCPAddr)
socks5Response(s5, localAddr.IP, localAddr.Port, socks5ReplyOkay)
targetConn, err := bindListener.Accept()
if err != nil {
slog.Error("bind tcp failed", "err", err)
return
}
defer targetConn.Close()
//sec reply
targetAddr := targetConn.RemoteAddr().(*net.TCPAddr)
socks5Response(s5, targetAddr.IP, targetAddr.Port, socks5ReplyOkay)
var wg sync.WaitGroup
// Use a done channel to ensure both goroutines exit when one finishes
done := make(chan struct{})
wg.Add(2)
go func() {
defer wg.Done()
defer func() {
close(done) // Signal the other goroutine to exit
targetConn.Close() // Force close to interrupt the other goroutine
s5.Close()
}()
_, err := io.Copy(targetConn, s5)
if err != nil {
slog.Error("bind tcp failed", "err", err)
}
}()
go func() {
defer wg.Done()
defer func() {
targetConn.Close() // Force close to interrupt the other goroutine
s5.Close()
}()
select {
case <-done:
return // Other goroutine finished, exit
default:
}
_, err := io.Copy(s5, targetConn)
if err != nil {
slog.Error("bind tcp failed", "err", err)
}
}()
wg.Wait()
}

View File

@@ -1,134 +0,0 @@
package client
import (
"fmt"
"github.com/google/uuid"
"log/slog"
"net"
)
type Socks5Request struct {
id string
socks5Cmd byte
socks5Atyp byte
dstAddr []byte
dstPort []byte
CountryCode string //iso country code
}
func parseSocks5Request(data []byte) (*Socks5Request, error) {
id := uuid.NewString()
info := &Socks5Request{id: id}
if data[0] != socks5Version {
return nil, fmt.Errorf("unsupported SOCKS version: %d", data[0])
}
if data[1] == socks5CmdConnect {
info.socks5Cmd = socks5CmdConnect
} else if data[1] == socks5CmdUdpAssoc {
info.socks5Cmd = socks5CmdUdpAssoc
} else {
//BIND is not supported
return nil, fmt.Errorf("unsupported command: %d", data[1])
}
if data[2] != socks5ReplyReserved {
return nil, fmt.Errorf("RSV must be 0x00")
}
if data[3] == socks5AtypeIPv4 {
if len(data) < 10 {
return nil, fmt.Errorf("request too short for atyp IPv4")
}
info.socks5Atyp = socks5AtypeIPv4
info.dstAddr = data[4:8]
info.dstPort = data[8:10]
} else if data[3] == socks5AtypeDomain {
if len(data) < 5 {
return nil, fmt.Errorf("request too short for atyp Domain")
}
addrLen := int(data[4])
info.socks5Atyp = socks5AtypeDomain
info.dstAddr = data[5 : 5+addrLen]
info.dstPort = data[5+addrLen : 5+addrLen+2]
} else if data[3] == socks5AtypeIPv6 {
if len(data) < 22 {
return nil, fmt.Errorf("request too short for atyp IPv6")
}
info.socks5Atyp = socks5AtypeIPv6
info.dstAddr = data[4:20]
info.dstPort = data[20:22]
} else {
return nil, fmt.Errorf("unsupported address type: %d", data[3])
}
//only get country code for connect command
if info.socks5Cmd == socks5CmdConnect {
code, err := Country(info.host())
if err != nil {
info.Logger().Error("failed to get country code", "err", err.Error())
} else {
info.CountryCode = code
}
}
return info, nil
}
func (s Socks5Request) host() string {
addr := ""
if s.socks5Atyp == socks5AtypeIPv4 || s.socks5Atyp == socks5AtypeIPv6 {
addr = net.IP(s.dstAddr).String()
} else if s.socks5Atyp == socks5AtypeDomain {
addr = string(s.dstAddr)
} else {
addr = string(s.dstAddr)
}
return addr
}
func (s Socks5Request) addr() string {
return fmt.Sprintf("%s:%s", s.host(), s.port())
}
func (s Socks5Request) cmd() string {
cmd := "unknown"
if s.socks5Cmd == socks5CmdConnect {
cmd = "connect"
} else if s.socks5Cmd == socks5CmdUdpAssoc {
cmd = "udp"
} else if s.socks5Cmd == socks5CmdBind {
cmd = "bind"
}
return cmd
}
func (s Socks5Request) Network() string {
cmd := "unknown"
if s.socks5Cmd == socks5CmdConnect {
cmd = "tcp"
} else if s.socks5Cmd == socks5CmdUdpAssoc {
cmd = "udp"
} else if s.socks5Cmd == socks5CmdBind {
cmd = "bind"
}
return cmd
}
func (s Socks5Request) aType() string {
return fmt.Sprintf("%v", s.socks5Atyp)
}
func (s Socks5Request) port() string {
port := int(s.dstPort[0])<<8 + int(s.dstPort[1])
return fmt.Sprintf("%v", port)
}
func (s Socks5Request) Logger() *slog.Logger {
return slog.With("reqId", s.id, "cmd", s.cmd(), "atyp", s.aType(), "ip", s.host(), "port", s.port(), "country", s.CountryCode)
}
func (s Socks5Request) String() string {
return fmt.Sprintf("socks5Cmd: %v, socks5Atyp: %v, dstAddr: %v, dstPort: %v, country: %s", s.cmd(), s.aType(), s.host(), s.port(), s.CountryCode)
}
func (s Socks5Request) addressBytes() []byte {
if s.socks5Atyp == socks5AtypeDomain {
return append([]byte{byte(len(s.dstAddr))}, s.dstAddr...)
}
return s.dstAddr
}

View File

@@ -1,40 +0,0 @@
package client
import (
"context"
"crypto/tls"
"fmt"
"github.com/gorilla/websocket"
"log/slog"
"net/http"
)
const (
browserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
)
func webSocketConn(ctx context.Context, proxy *Proxy, req *Socks5Request) (*websocket.Conn, error) {
wsDialer := websocket.DefaultDialer
headers := http.Header{}
headers.Set("Authorization", proxy.UserID)
headers.Set("User-Agent", browserAgent)
if proxy.Sni != "" {
headers.Set("Host", proxy.Sni)
wsDialer.TLSClientConfig = &tls.Config{
ServerName: proxy.Sni, // Set the SNI to the hostname of the server
}
}
headers.Set("x-req-id", req.id)
url := proxy.RelayURL()
slog.Debug("connecting to remote proxy server", "url", url)
ws, resp, err := websocket.DefaultDialer.DialContext(ctx, url, headers)
if err != nil {
return nil, fmt.Errorf("failed to connect to remote proxy server: %s ,error:%v", proxy.RelayURL(), err)
}
if resp.StatusCode != http.StatusSwitchingProtocols {
return nil, fmt.Errorf("failed to connect to remote proxy server: %s ,error:%v", proxy.RelayURL(), err)
}
return ws, nil
}

14
config.toml Normal file
View File

@@ -0,0 +1,14 @@
#toml 格式的文件,建议字符串使用单引号,大小敏感
#
SubAddresses = '127.0.0.1:8013'# 可以被广域网访问的域名端口,可以是域名也可以是ip,多个地址用逗号分隔
AppPort = '8013' # 服务的端口,可以是80,443,在大陆其他的端口不能被访问
RegisterUrl = '' #主控服务器地址,主要作用是控制用户授权和流量计费,可以为空则为个人模式
RegisterToken = 'unchain.people.from.censorship.and.surveillance'# 主控服务器的token
AllowUsers = '6fe57e3f-e618-4873-ba96-a76adec22ccd' # UUID 可以访问的用户UUID,多个则用逗号分隔.个人模式这里不能为空 在线UUID生成器 https://1024tools.com/uuid
LogFile = '' # 日志文件名,可以为空则不记录日志
DebugLevel = 'debug' # 日志基本debug, info, warn, error
IntervalSecond = '7200' #主控服务器推送流量数据的间隔,个人模式不关心
EnableDataUsageMetering = 'false'

View File

@@ -1,51 +0,0 @@
# Unchain 翻墙服务器搭建和使用教程
## 服务器要求
建议使用linux ubuntu 服务器. 服务器配配置没有内存和CPU限制,任意配置都可以. 最便宜的服务器都可以.
服务求区域最好选择日本或者美国(访问OpenAI,Claude,Google Gemini友好).
## 安装部署
从 [https://github.com/unchainese/unchain/releases/tag/v0.0.4](https://github.com/unchainese/unchain/releases/tag/v0.0.4)
现在对应服务器架构的二进制文件,解压到任意目录,然后运行即可.
```bash
wget https://github.com/unchainese/unchain/releases/download/v0.0.4/unchain-linux-amd64.unchain.tar.gz
tar -zxvf unchain-linux-amd64.unchain.tar.gz
```
在上一步解压之后的可执行文件相同的目录创建 `config.toml`配置文件.
创建文件命令 `vim config.toml`.
文件内容详见 [https://github.com/unchainese/unchain/blob/v0.0.4/config.example.standalone.toml](https://github.com/unchainese/unchain/blob/v0.0.4/config.example.standalone.toml)
使用下面命令,来测试配置文件是否正确.
```bash
# cd 到 unchain 和 config.toml 所在目录
chmod +x unchain
./unchain
```
如果没有报错,说明配置文件正确.
## 使用systemctl 管理服务
`/etc/systemd/system/` 目录下创建 `unchain.service` 文件,
使用命令 `vim /etc/systemd/system/unchain.service` 创建文件.
文件内容如下:
[https://github.com/unchainese/unchain/blob/v0.0.4/unchain.service](https://github.com/unchainese/unchain/blob/v0.0.4/unchain.service)
- `systemctl daemon-reload` 重新加载服务
- `systemctl start unchain` 启动服务
- `systemctl stop unchain` 停止服务
- `systemctl restart unchain` 重启服务
- `systemctl status unchain` 查看服务状态
- `journalctl -u unchain` 查看服务日志
执行 `systemctl daemon-reload` 加载刚才的配置
执行 `systemctl start unchain` 启动服务,如果没有报错,说明服务启动成功.
## 使用V2ray/Clash/ShadowRocket 客户端连接
复制日志中的 VLESS链接,在V2ray/Clash/ShadowRocket 客户端中添加VLESS链接.

11
go.mod
View File

@@ -7,13 +7,4 @@ require (
github.com/gorilla/websocket v1.5.3
)
require (
github.com/BurntSushi/toml v1.4.0
github.com/oschwald/geoip2-golang v1.11.0
)
require (
github.com/oschwald/maxminddb-golang v1.13.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
golang.org/x/sys v0.20.0 // indirect
)
require github.com/BurntSushi/toml v1.4.0

22
go.sum
View File

@@ -1,28 +1,6 @@
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
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/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/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/oschwald/geoip2-golang v1.11.0 h1:hNENhCn1Uyzhf9PTmquXENiWS6AlxAEnBII6r8krA3w=
github.com/oschwald/geoip2-golang v1.11.0/go.mod h1:P9zG+54KPEFOliZ29i7SeYZ/GM6tfEL+rgSn03hYuUo=
github.com/oschwald/maxminddb-golang v1.13.0 h1:R8xBorY71s84yO06NgTmQvqvTvlS/bnYZrrWX1MElnU=
github.com/oschwald/maxminddb-golang v1.13.0/go.mod h1:BU0z8BfFVhi1LQaonTwwGQlsHUEu9pWNdMfmq4ztm0o=
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/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -13,12 +13,17 @@ import (
var configFilePath, installMode, action string
func main() {
flag.StringVar(&action, "action", "run", "动作参数,可选值: run, install,uninstall,info,run")
flag.StringVar(&action, "action", "run", "动作参数,可选值: run, install,uninstall,info,client")
flag.StringVar(&configFilePath, "config", "config.toml", "配置文件路径")
flag.StringVar(&installMode, "mode", "single", "安装命令的模式参数")
flag.Parse()
//todo:: switch case command
if action == "client" {
server.StartSocks5Server()
return
}
c := global.Cfg(configFilePath) //using default config.toml file
fd := global.SetupLogger(c)
defer fd.Close()
@@ -27,7 +32,7 @@ func main() {
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt)
app := node.NewApp(c, stop)
app := server.NewApp(c, stop)
app.PushNode() //register node info to the manager server
app.PrintVLESSConnectionURLS() //for standalone node
go app.Run()

View File

@@ -2,6 +2,7 @@ package schema
import (
"bytes"
"crypto/sha256"
"encoding/binary"
"errors"
"fmt"
@@ -25,6 +26,13 @@ const (
byteLF = '\n'
)
func (p ProtoTrojan) AuthUser(password string) (isOk bool) {
sha224Hash := sha256.New224()
sha224Hash.Write([]byte(password))
sha224Sum := sha224Hash.Sum(nil) //28 bytes
hexSha224Bytes := []byte(fmt.Sprintf("%x", sha224Sum))
return bytes.Equal(p.sha224password, hexSha224Bytes)
}
func parseTrojanHeader(buffer []byte) (*ProtoTrojan, error) {
if len(buffer) < 56 {
return nil, errors.New("invalid data")

View File

@@ -1,8 +1,6 @@
package schema
import (
"bytes"
"crypto/sha256"
"encoding/binary"
"errors"
"fmt"
@@ -13,7 +11,7 @@ import (
)
type ProtoVLESS struct {
userID uuid.UUID
UserID uuid.UUID
DstProtocol string //tcp or udp
dstHost string
dstHostType string //ipv6 or ipv4,domain
@@ -22,16 +20,55 @@ type ProtoVLESS struct {
payload []byte
}
func (p ProtoTrojan) AuthUser(password string) (isOk bool) {
sha224Hash := sha256.New224()
sha224Hash.Write([]byte(password))
sha224Sum := sha224Hash.Sum(nil) //28 bytes
hexSha224Bytes := []byte(fmt.Sprintf("%x", sha224Sum))
return bytes.Equal(p.sha224password, hexSha224Bytes)
const VLESS_VERSION = 0
func MakeVless(userID string, dstHost string, dstPort uint16, tcpOrUdp string, payload []byte) *ProtoVLESS {
if tcpOrUdp != "tcp" && tcpOrUdp != "udp" {
panic("tcpOrUdp must be tcp or udp")
}
return &ProtoVLESS{
UserID: uuid.MustParse(userID),
DstProtocol: tcpOrUdp,
dstHost: dstHost,
dstPort: dstPort,
Version: VLESS_VERSION,
payload: payload,
}
}
func (h ProtoVLESS) UUID() string {
return h.userID.String()
return h.UserID.String()
}
func (h ProtoVLESS) DataHeader() []byte {
header := make([]byte, 0)
header = append(header, h.Version)
header = append(header, h.UserID[:]...)
header = append(header, 0) // no extra info length 0
switch h.DstProtocol {
case "tcp":
header = append(header, 1)
case "udp":
header = append(header, 2)
default:
panic("unsupported protocol")
}
//two bytes of port
header = append(header, byte(h.dstPort>>8), byte(h.dstPort&0xff))
//address type
thisIP := net.ParseIP(h.dstHost)
if thisIP != nil && thisIP.To4() != nil {
header = append(header, 1) // IPv4
header = append(header, thisIP.To4()...)
} else if thisIP != nil && thisIP.To16() != nil {
header = append(header, 3) // IPv6
header = append(header, thisIP.To16()...)
} else {
header = append(header, 2) // domain
header = append(header, byte(len(h.dstHost)))
header = append(header, []byte(h.dstHost)...)
}
header = append(header, h.payload...)
return header
}
func (h ProtoVLESS) DataUdp() []byte {
@@ -54,6 +91,27 @@ func (h ProtoVLESS) DataUdp() []byte {
}
return allData
}
func (h ProtoVLESS) DataUdpWrong() []byte {
allData := make([]byte, 0)
chunk := h.payload
for index := 0; index < len(chunk); {
if index+2 > len(chunk) {
fmt.Println("Incomplete length buffer")
return nil
}
lengthBuffer := chunk[index : index+2]
udpPacketLength := binary.BigEndian.Uint16(lengthBuffer)
if index+2+int(udpPacketLength) > len(chunk) {
fmt.Println("Incomplete UDP packet")
return nil
}
udpData := chunk[index+2 : index+2+int(udpPacketLength)]
index = index + 2 + int(udpPacketLength)
allData = append(allData, udpData...)
}
return allData
}
func (h ProtoVLESS) DataTcp() []byte {
return h.payload
}
@@ -81,13 +139,13 @@ func (h ProtoVLESS) HostPort() string {
return net.JoinHostPort(h.dstHost, fmt.Sprintf("%d", h.dstPort))
}
func (h ProtoVLESS) Logger() *slog.Logger {
return slog.With("userID", h.userID.String(), "network", h.DstProtocol, "addr", h.HostPort())
return slog.With("userID", h.UserID.String(), "network", h.DstProtocol, "addr", h.HostPort())
}
// VLESSParse https://xtls.github.io/development/protocols/vless.html
func VLESSParse(buf []byte) (*ProtoVLESS, error) {
payload := &ProtoVLESS{
userID: uuid.Nil,
UserID: uuid.Nil,
DstProtocol: "",
dstHost: "",
dstPort: 0,
@@ -100,7 +158,7 @@ func VLESSParse(buf []byte) (*ProtoVLESS, error) {
}
payload.Version = buf[0]
payload.userID = uuid.Must(uuid.FromBytes(buf[1:17]))
payload.UserID = uuid.Must(uuid.FromBytes(buf[1:17]))
extraInfoProtoBufLen := buf[17]
command := buf[18+extraInfoProtoBufLen]

View File

@@ -1,4 +1,4 @@
package node
package server
import (
"bytes"
@@ -66,8 +66,8 @@ func (app *App) Run() {
func (app *App) PrintVLESSConnectionURLS() {
listenPort := app.cfg.ListenPort()
fmt.Printf("\n\n\nvist to get VLESS connection info: http://127.0.0.1:%d/sub/<YOUR_CONFIGED_UUID> \n", listenPort)
fmt.Printf("vist to get VLESS connection info: http://<HOST>:%d/sub/<YOUR_UUID>\n", listenPort)
fmt.Printf("\n\n visit to get VLESS connection info: http://127.0.0.1:%d/sub/<YOUR_CONFIGED_UUID> \n", listenPort)
fmt.Printf("visit to get VLESS connection info: http://<HOST>:%d/sub/<YOUR_UUID>\n", listenPort)
app.userUsedTrafficKb.Range(func(id, _ interface{}) bool {
userID := id.(string)

View File

@@ -1,4 +1,4 @@
package node
package server
import (
"fmt"

View File

@@ -1,4 +1,4 @@
package node
package server
import (
"fmt"
@@ -20,7 +20,7 @@ import (
// vless://6fe57e3f-e618-4873-ba96-a76ad1112ccd@aws.xxx.cn:80?encryption=none&security=none&sni=s5cf.xxx.cn&allowInsecure=1
// &type=ws
// &hostSni=aws.xxx.cn&path=%2Fws-vless%3Fed%3D2560#locaol-clone
// &hostSni=aws.xxx.cn&path=%2Fws-vless%3Fed%3D2560#local-clone
type vlessSub struct {
remark string
addrWithPort string //eg node.cloudflare.cn:443 or node.cloudflare.cn:80

View File

@@ -1,4 +1,4 @@
package node
package server
import (
"context"
@@ -46,6 +46,7 @@ func startDstConnection(vd *schema.ProtoVLESS, timeout time.Duration) (net.Conn,
}
func (app *App) WsVLESS(w http.ResponseWriter, r *http.Request) {
uid := r.PathValue("uid")
//check can upgrade websocket
if r.Header.Get(upgradeHeader) != websocketProtocol {
@@ -194,6 +195,7 @@ func vlessTCP(ctx context.Context, sv *schema.ProtoVLESS, ws *websocket.Conn) in
return trafficMeter.Load()
}
// vlessUDP handles UDP traffic over VLESS protocol via WebSocket is tested ok
func vlessUDP(_ context.Context, sv *schema.ProtoVLESS, ws *websocket.Conn) (trafficMeter int64) {
logger := sv.Logger()
conn, headerVLESS, err := startDstConnection(sv, time.Millisecond*1000)
@@ -202,18 +204,17 @@ func vlessUDP(_ context.Context, sv *schema.ProtoVLESS, ws *websocket.Conn) (tra
return
}
defer conn.Close()
trafficMeter += int64(len(sv.DataUdp()))
//write early data
_, err = conn.Write(sv.DataUdp())
udpData := sv.DataUdp()
_, err = conn.Write(udpData)
if err != nil {
logger.Error("Error writing early data to TCP connection:", "err", err)
logger.Error("Error writing early data to UDP connection:", "err", err)
return
}
buf := make([]byte, buffSize)
n, err := conn.Read(buf)
if err != nil {
logger.Error("Error reading from TCP connection:", "err", err)
logger.Error("Error reading from UDP connection:", "err", err)
return
}
udpDataLen1 := (n >> 8) & 0xff
@@ -221,11 +222,11 @@ func vlessUDP(_ context.Context, sv *schema.ProtoVLESS, ws *websocket.Conn) (tra
headerVLESS = append(headerVLESS, byte(udpDataLen1), byte(udpDataLen2))
headerVLESS = append(headerVLESS, buf[:n]...)
trafficMeter += int64(len(headerVLESS))
//send back the first udp packet with vless header
err = ws.WriteMessage(websocket.BinaryMessage, headerVLESS)
if err != nil {
logger.Error("Error writing to websocket:", "err", err)
return
}
return trafficMeter
return int64(len(headerVLESS)) + int64(len(udpData))
}

488
server/socks5.go Normal file
View File

@@ -0,0 +1,488 @@
package server
import (
"encoding/binary"
"fmt"
"io"
"log/slog"
"net"
"os"
"strconv"
"github.com/gorilla/websocket"
"github.com/unchainese/unchain/schema"
)
// SOCKS5 protocol and server constants
const (
socksVersion5 = 0x05
authMethodNoAuth = 0x00
reservedField = 0x00
cmdConnect = 0x01
cmdUDPAssociate = 0x03
addrTypeIPv4 = 0x01
addrTypeDomain = 0x03
addrTypeIPv6 = 0x04
replySucceeded = 0x00
replyHostUnreachable = 0x04
responseFixedLen = 10
udpHeaderLen = 10
maxUDPPacketSize = 65536
networkTCP = "tcp"
networkUDP = "udp"
localhostAnyPort = "127.0.0.1:0"
)
var (
socks5Host string = "127.0.0.1"
socks5Port int = 1088
vlessUUID = "6fe57e3f-e618-4873-ba96-a76adec22ccd"
wsURL = "ws://57.180.17.29/wsv/v1?ed=2560"
)
func StartSocks5Server() {
addr := fmt.Sprintf("%s:%d", socks5Host, socks5Port)
listener, err := net.Listen("tcp", addr)
if err != nil {
slog.Error(fmt.Sprintf("Failed to start SOCKS5 server: %v", err))
os.Exit(1)
}
defer listener.Close()
slog.Info(fmt.Sprintf("SOCKS5 server started on %s", addr))
slog.Info("Press Ctrl+C to stop the server")
for {
conn, err := listener.Accept()
if err != nil {
slog.Error(fmt.Sprintf("Failed to accept connection: %v", err))
continue
}
go handleSocks5Connection(conn)
}
}
func handleSocks5Connection(client net.Conn) {
defer client.Close()
slog.Debug(fmt.Sprintf("New connection from %s", client.RemoteAddr()))
// Step 1: Version and authentication methods
if err := handleHandshake(client); err != nil {
slog.Error(fmt.Sprintf("Handshake failed: %v", err))
return
}
// Step 2: Request details
request, err := handleRequest(client)
if err != nil {
slog.Error(fmt.Sprintf("Request handling failed: %v", err))
return
}
// Step 3: Connect to target and relay data
if err := handleRelay(client, request); err != nil {
slog.Error(fmt.Sprintf("Relay failed: %v", err))
return
}
}
func handleHandshake(client net.Conn) error {
// Read version and number of authentication methods
buf := make([]byte, 2)
if _, err := io.ReadFull(client, buf); err != nil {
return fmt.Errorf("failed to read handshake: %v", err)
}
version := buf[0]
if version != socksVersion5 {
return fmt.Errorf("unsupported SOCKS version: %d", version)
}
numMethods := buf[1]
methods := make([]byte, numMethods)
if _, err := io.ReadFull(client, methods); err != nil {
return fmt.Errorf("failed to read authentication methods: %v", err)
}
// For simplicity, we'll accept any authentication method (including no authentication)
// In production, you might want to implement proper authentication
response := []byte{socksVersion5, authMethodNoAuth} // Version 5, No authentication required
if _, err := client.Write(response); err != nil {
return fmt.Errorf("failed to write handshake response: %v", err)
}
return nil
}
type socks5Request struct {
command byte
address string
port uint16
}
func handleRequest(client net.Conn) (*socks5Request, error) {
// Read request header
header := make([]byte, 4)
if _, err := io.ReadFull(client, header); err != nil {
return nil, fmt.Errorf("failed to read request header: %v", err)
}
version := header[0]
if version != socksVersion5 {
return nil, fmt.Errorf("unsupported SOCKS version: %d", version)
}
command := header[1]
if command != cmdConnect && command != cmdUDPAssociate { // CONNECT (0x01) or UDP ASSOCIATE (0x03)
return nil, fmt.Errorf("unsupported command: %d", command)
}
// Reserved field should be 0
if header[2] != reservedField {
return nil, fmt.Errorf("invalid reserved field: %d", header[2])
}
addressType := header[3]
var address string
var port uint16
switch addressType {
case addrTypeIPv4: // IPv4
addr := make([]byte, 4)
if _, err := io.ReadFull(client, addr); err != nil {
return nil, fmt.Errorf("failed to read IPv4 address: %v", err)
}
address = net.IP(addr).String()
case addrTypeDomain: // Domain name
domainLen := make([]byte, 1)
if _, err := io.ReadFull(client, domainLen); err != nil {
return nil, fmt.Errorf("failed to read domain length: %v", err)
}
domain := make([]byte, domainLen[0])
if _, err := io.ReadFull(client, domain); err != nil {
return nil, fmt.Errorf("failed to read domain: %v", err)
}
address = string(domain)
case addrTypeIPv6: // IPv6
addr := make([]byte, 16)
if _, err := io.ReadFull(client, addr); err != nil {
return nil, fmt.Errorf("failed to read IPv6 address: %v", err)
}
address = net.IP(addr).String()
default:
return nil, fmt.Errorf("unsupported address type: %d", addressType)
}
// Read port
portBytes := make([]byte, 2)
if _, err := io.ReadFull(client, portBytes); err != nil {
return nil, fmt.Errorf("failed to read port: %v", err)
}
port = binary.BigEndian.Uint16(portBytes)
slog.Info(fmt.Sprintf("SOCKS5 request: %s:%d", address, port))
return &socks5Request{
command: command,
address: address,
port: port,
}, nil
}
func handleRelay(client net.Conn, request *socks5Request) error {
switch request.command {
case cmdConnect: // CONNECT command
return handleTCPRelay(client, request)
case cmdUDPAssociate: // UDP ASSOCIATE command
return handleUDPRelay(client, request)
default:
return fmt.Errorf("unsupported command: %d", request.command)
}
}
type targetWs struct {
conn *websocket.Conn
isFirstRead bool
}
func makeTargetWs(addr, uid string, req *socks5Request) (*targetWs, error) {
target, _, err := websocket.DefaultDialer.Dial(addr, nil)
if err != nil {
return nil, fmt.Errorf("failed to connect to target %s: %v", addr, err)
}
udpOrTcp := "tcp"
if req.command == cmdUDPAssociate {
udpOrTcp = "udp"
}
vlessHeadData := schema.MakeVless(uid, req.address, req.port, udpOrTcp, nil).DataHeader()
err = target.WriteMessage(websocket.BinaryMessage, vlessHeadData)
if err != nil {
return nil, fmt.Errorf("failed to send VLESS header: %w", err)
}
return &targetWs{conn: target, isFirstRead: true}, nil
}
func (t *targetWs) Read(p []byte) (n int, err error) {
_, bytesRead, err := t.conn.ReadMessage()
if err != nil {
return 0, err
}
if len(bytesRead) > 2 && t.isFirstRead {
// Handle the first read case
t.isFirstRead = false
bytesRead = bytesRead[2:] // Skip the first two bytes
}
return copy(p, bytesRead), nil
}
func (t *targetWs) Write(p []byte) (n int, err error) {
err = t.conn.WriteMessage(websocket.BinaryMessage, p)
if err != nil {
return 0, err
}
return len(p), nil
}
func (t *targetWs) Close() error {
return t.conn.Close()
}
func handleTCPRelay(client net.Conn, request *socks5Request) error {
// Connect to target
target, err := makeTargetWs(wsURL, vlessUUID, request)
if err != nil {
// Send failure response
response := []byte{socksVersion5, replyHostUnreachable, reservedField, addrTypeIPv4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
client.Write(response)
return fmt.Errorf("failed to connect to target %s:%d %v", request.address, request.port, err)
}
defer target.Close()
// Send success response
// For CONNECT requests, we should return the address and port that was used
// Since we're connecting to the target, we'll return the target's address
response := make([]byte, responseFixedLen)
response[0] = socksVersion5 // Version
response[1] = replySucceeded // Success
response[2] = reservedField // Reserved
response[3] = addrTypeIPv4 // IPv4 address type
// The request.address contains the host, and request.port contains the port
// For domain names, we'll use 0.0.0.0
copy(response[4:8], net.IPv4zero)
slog.Debug(fmt.Sprintf("Using fallback IPv4 address for domain: %s", request.address))
// Set the port
binary.BigEndian.PutUint16(response[8:10], request.port)
slog.Debug(fmt.Sprintf("Response port: %d", request.port))
if _, err := client.Write(response); err != nil {
return fmt.Errorf("failed to write success response: %v", err)
}
slog.Info(fmt.Sprintf("TCP relay established between %s and %s", client.RemoteAddr(), request.address))
// Start bidirectional relay
errChan := make(chan error, 2)
// Client -> Target
go func() {
_, err := io.Copy(target, client)
errChan <- err
}()
// Target -> Client
go func() {
_, err := io.Copy(client, target)
errChan <- err
}()
// Wait for either direction to finish
err = <-errChan
if err != nil && err != io.EOF {
slog.Debug(fmt.Sprintf("TCP relay error: %v", err))
}
return nil
}
func handleUDPRelay(client net.Conn, request *socks5Request) error {
// For UDP ASSOCIATE, we need to create a UDP listener
// The client will send UDP packets to this listener
udpAddr, err := net.ResolveUDPAddr(networkUDP, localhostAnyPort)
if err != nil {
response := []byte{socksVersion5, replyHostUnreachable, reservedField, addrTypeIPv4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
client.Write(response)
return fmt.Errorf("failed to resolve UDP address: %v", err)
}
udpListener, err := net.ListenUDP(networkUDP, udpAddr)
if err != nil {
response := []byte{socksVersion5, replyHostUnreachable, reservedField, addrTypeIPv4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
client.Write(response)
return fmt.Errorf("failed to create UDP listener: %v", err)
}
defer udpListener.Close()
// Get the actual address and port that was bound
actualAddr := udpListener.LocalAddr().(*net.UDPAddr)
// Send success response with the UDP listener address
// For UDP ASSOCIATE, we send back the address where the client should send UDP packets
response := make([]byte, responseFixedLen)
response[0] = socksVersion5 // Version
response[1] = replySucceeded // Success
response[2] = reservedField // Reserved
response[3] = addrTypeIPv4 // IPv4 address type
// Copy the IP address (4 bytes)
copy(response[4:8], actualAddr.IP.To4())
// Copy the port (2 bytes, big endian)
binary.BigEndian.PutUint16(response[8:10], uint16(actualAddr.Port))
if _, err := client.Write(response); err != nil {
return fmt.Errorf("failed to write UDP response: %v", err)
}
slog.Info(fmt.Sprintf("UDP association established on %s for client %s", actualAddr.String(), client.RemoteAddr()))
// Start UDP relay
return handleUDPDataRelay(udpListener, client)
}
func handleUDPDataRelay(udpListener *net.UDPConn, client net.Conn) error {
buffer := make([]byte, maxUDPPacketSize) // Max UDP packet size
for {
// Read UDP packet from client
n, clientAddr, err := udpListener.ReadFromUDP(buffer)
if err != nil {
if err == io.EOF {
return nil
}
slog.Debug(fmt.Sprintf("UDP read error: %v", err))
continue
}
// Parse SOCKS5 UDP request header
if n < udpHeaderLen { // Minimum header size
continue
}
// Debug: log the first few bytes of the UDP packet
if n > 20 {
slog.Debug(fmt.Sprintf("UDP packet bytes: %v", buffer[:20]))
} else {
slog.Debug(fmt.Sprintf("UDP packet bytes: %v", buffer[:n]))
}
// Extract target address from UDP packet
targetAddr, err := parseUDPRequestHeader(buffer[:n])
if err != nil {
slog.Debug(fmt.Sprintf("Failed to parse UDP header: %v", err))
continue
}
slog.Debug(fmt.Sprintf("Parsed UDP target address: %s", targetAddr))
// Forward the UDP packet to the target
go func(data []byte, target string) {
if err := forwardUDPPacket(udpListener, clientAddr, target, data); err != nil {
slog.Debug(fmt.Sprintf("UDP forward error: %v", err))
}
}(buffer[:n], targetAddr)
}
}
func parseUDPRequestHeader(data []byte) (string, error) {
if len(data) < udpHeaderLen {
return "", fmt.Errorf("insufficient data for UDP header")
}
// Skip RSV and FRAG fields
addrType := data[3]
var address string
var port uint16
switch addrType {
case addrTypeIPv4: // IPv4
if len(data) < udpHeaderLen {
return "", fmt.Errorf("insufficient data for IPv4 address")
}
address = net.IP(data[4:8]).String()
port = binary.BigEndian.Uint16(data[8:10])
slog.Debug(fmt.Sprintf("IPv4 parsing: data[3:7]=%v, data[7:9]=%v, address=%s, port=%d", data[3:7], data[7:9], address, port))
case addrTypeDomain: // Domain name
domainLen := int(data[4])
if len(data) < 7+domainLen {
return "", fmt.Errorf("insufficient data for domain name")
}
address = string(data[5 : 5+domainLen])
port = binary.BigEndian.Uint16(data[5+domainLen : 7+domainLen])
case addrTypeIPv6: // IPv6
if len(data) < 22 {
return "", fmt.Errorf("insufficient data for IPv6 address")
}
address = net.IP(data[4:20]).String()
port = binary.BigEndian.Uint16(data[20:22])
default:
return "", fmt.Errorf("unsupported address type: %d", addrType)
}
return net.JoinHostPort(address, strconv.Itoa(int(port))), nil
}
func forwardUDPPacket(udpListener *net.UDPConn, clientAddr *net.UDPAddr, targetAddr string, data []byte) error {
slog.Debug(fmt.Sprintf("Forwarding UDP packet to target %s", targetAddr))
// Resolve target address
addr, err := net.ResolveUDPAddr(networkUDP, targetAddr)
if err != nil {
return fmt.Errorf("failed to resolve target address: %v", err)
}
req := &socks5Request{command: cmdUDPAssociate, address: addr.AddrPort().Addr().String(), port: addr.AddrPort().Port()}
ws, err := makeTargetWs(wsURL, vlessUUID, req)
if err != nil {
return fmt.Errorf("failed to connect to target %v websocket: %w", addr, err)
}
defer ws.Close()
if _, err := ws.Write(data[udpHeaderLen:]); err != nil {
return fmt.Errorf("failed to write to target websocket: %w", err)
}
// Read response from target
responseBuffer := make([]byte, maxUDPPacketSize)
n, err := ws.Read(responseBuffer)
if err != nil {
return fmt.Errorf("failed to read from target: %v", err)
}
// Create response packet with SOCKS5 header
responsePacket := createUDPResponsePacket(data[:udpHeaderLen], responseBuffer[:n])
// Send response back to client
_, err = udpListener.WriteToUDP(responsePacket, clientAddr)
if err != nil {
return fmt.Errorf("failed to send response to client: %v", err)
}
return nil
}
func createUDPResponsePacket(header []byte, data []byte) []byte {
response := make([]byte, len(header)+len(data))
copy(response, header)
copy(response[len(header):], data)
return response
}

59
testkit/README.md Normal file
View File

@@ -0,0 +1,59 @@
# Testing UDP over SOCKS5 with VLESS Using V2Ray Client
This guide outlines the steps to test UDP functionality over SOCKS5 with VLESS using the V2Ray client.
## Prerequisites
- Ensure you have the necessary files: `udp_echo_svr.go`, `main.go`, `config.json`, and `udpcheck.py` in the `testkit` directory.
- Access to the server `s3.mojotv.cn` for deploying the UDP echo server.
## Steps
1. **Deploy UDP Echo Server**:
- Copy `testkit/udp_echo_svr.go` to the server `s3.mojotv.cn` using SCP.
- On the server, run the following command to start the UDP echo server in the background:
```
go run udp_echo_svr.go &
```
2. **Start VLESS Proxy Server**:
config.toml
```toml
#toml 格式的文件,建议字符串使用单引号,大小敏感
#
SubAddresses = 'n-us1.libragen.cn:80'# 可以被广域网访问的域名端口,可以是域名也可以是ip,多个地址用逗号分隔
AppPort = '8013' # import same as config.json
RegisterUrl = '' #主控服务器地址,主要作用是控制用户授权和流量计费,可以为空则为个人模式
RegisterToken = 'unchain.people.from.censorship.and.surveillance'# 主控服务器的token
AllowUsers = '6fe57e3f-e618-4873-ba96-a76adec22ccd' # important! same as config.json uuid
LogFile = '' #
DebugLevel = 'debug' # 日志基本debug, info, warn, error
EnableDataUsageMetering = 'false'
```
- In your local environment, navigate to the project root and run:
```
go run main.go
```
This starts the VLESS over WebSocket proxy server.
3. **Run V2Ray Client**:
- Follow the installation guide for V2Ray: https://www.v2fly.org/guide/install.html
- Change to the `testkit` directory and start the V2Ray client using the provided configuration:
```
cd testkit && v2ray run
```
The client will use `testkit/config.json` for configuration.
4. **Perform UDP Check**:
- From the `testkit` directory, run the UDP check script:
```
cd testkit && python3 udpcheck.py
```
This will test UDP connectivity through the SOCKS5 proxy.
## Notes
- Ensure all services are running and accessible before proceeding to the next step.
- Monitor the terminal output for any errors or confirmations during the process.

Binary file not shown.

38
testkit/config.json Normal file
View File

@@ -0,0 +1,38 @@
{
"inbounds": [
{
"port": 1088,
"listen": "127.0.0.1",
"protocol": "socks",
"settings": {
"auth": "noauth",
"udp": true,
"ip": "127.0.0.1"
}
}
],
"outbounds": [
{
"protocol": "vless",
"settings": {
"vnext": [
{
"address": "127.0.0.1",
"port": 8013,
"users": [
{
"id": "6fe57e3f-e618-4873-ba96-a76adec22ccd",
"encryption": "none"
}
]
}
]
},
"streamSettings": {
"network": "ws",
"wsSettings": {
"path": "/wsv/v1?ed=2560"
} }
}
]
}

41
testkit/udp_echo_svr.go Normal file
View File

@@ -0,0 +1,41 @@
package main
import (
"fmt"
"net"
"os"
)
// running in s3.mojotv.cn
func main() {
addr, err := net.ResolveUDPAddr("udp", ":8080")
if err != nil {
fmt.Println("Error resolving address:", err)
os.Exit(1)
}
conn, err := net.ListenUDP("udp", addr)
if err != nil {
fmt.Println("Error listening:", err)
os.Exit(1)
}
defer conn.Close()
fmt.Println("UDP echo server listening on :8080")
buffer := make([]byte, 1024)
for {
n, clientAddr, err := conn.ReadFromUDP(buffer)
if err != nil {
fmt.Println("Error reading:", err)
continue
}
fmt.Printf("Received %d bytes from %s: %s\n", n, clientAddr, string(buffer[:n]))
_, err = conn.WriteToUDP(buffer[:n], clientAddr)
if err != nil {
fmt.Println("Error writing:", err)
}
}
}

40
testkit/udpcheck.py Normal file
View File

@@ -0,0 +1,40 @@
import socket
import socks # pip3 install pysocks
UDP_ECHO_TEST_SERVER_HOST = 's3.mojotv.cn'
UDP_ECHO_TEST_SERVER_PORT = 8080
V2RAY_SOCKS5_HOST = '127.0.0.1'
V2RAY_SOCKS5_PORT = 1080
if __name__ == "__main__":
s = socks.socksocket(
socket.AF_INET, socket.SOCK_DGRAM
) # Same API as socket.socket in the standard lib
try:
s.set_proxy(
socks.SOCKS5, V2RAY_SOCKS5_HOST, V2RAY_SOCKS5_PORT, False, user, pwd
) # SOCKS4 and SOCKS5 use port 1080 by default
# Can be treated identical to a regular socket object
# Raw DNS request
req = 'abcdexxxx'.encode()
s.sendto(req, (UDP_ECHO_TEST_SERVER_HOST, UDP_ECHO_TEST_SERVER_PORT))
(rsp, address) = s.recvfrom(4096)
# check req and rsp equality
if len(rsp) < 2:
print("Invalid response")
if rsp[0] == req[0] and rsp[1] == req[1] and len(rsp) == len(req):
print("UDP check passed")
# print req as hex string
print("req: " + " ".join("{:02x}".format(c) for c in req))
print("res: " + " ".join("{:02x}".format(c) for c in rsp))
else:
print("Invalid response")
except socks.ProxyError as e:
print(e.msg)
except socket.error as e:
print(repr(e))