mirror of
https://github.com/unchainese/unchain.git
synced 2025-12-24 12:38:02 +08:00
Refactor and enhance SOCKS5 server with UDP support
- 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:
24
.vscode/launch.json
vendored
Normal file
24
.vscode/launch.json
vendored
Normal 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
10
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"DGRAM",
|
||||
"libragen",
|
||||
"mojotv",
|
||||
"socksocket",
|
||||
"unchainese",
|
||||
"Upgrader"
|
||||
]
|
||||
}
|
||||
270
README.md
270
README.md
@@ -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.
|
||||
|
||||
[](https://golang.org/)
|
||||
[](LICENSE)
|
||||
[](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.
276
client/client.go
276
client/client.go
@@ -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")
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package client
|
||||
|
||||
import "io"
|
||||
|
||||
type RelayTcp interface {
|
||||
io.Reader
|
||||
io.Writer
|
||||
io.Closer
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
14
config.toml
Normal 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'
|
||||
|
||||
|
||||
@@ -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
11
go.mod
@@ -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
22
go.sum
@@ -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=
|
||||
|
||||
9
main.go
9
main.go
@@ -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()
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package node
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
488
server/socks5.go
Normal 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
59
testkit/README.md
Normal 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.
|
||||
BIN
testkit/__pycache__/socks5_udp_send.cpython-313.pyc
Normal file
BIN
testkit/__pycache__/socks5_udp_send.cpython-313.pyc
Normal file
Binary file not shown.
38
testkit/config.json
Normal file
38
testkit/config.json
Normal 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
41
testkit/udp_echo_svr.go
Normal 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
40
testkit/udpcheck.py
Normal 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))
|
||||
Reference in New Issue
Block a user