mirror of
https://github.com/xiaoqidun/probe.git
synced 2026-01-29 04:58:46 +08:00
feat(正式发布): 以Apache License 2.0协议开源
This commit is contained in:
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
.devcontainer/
|
||||||
|
*.exe
|
||||||
|
*~
|
||||||
@@ -186,7 +186,7 @@
|
|||||||
same "printed page" as the copyright notice for easier
|
same "printed page" as the copyright notice for easier
|
||||||
identification within third-party archives.
|
identification within third-party archives.
|
||||||
|
|
||||||
Copyright [yyyy] [name of copyright owner]
|
Copyright 2026 肖其顿 (XIAO QI DUN)
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
probe
|
||||||
|
Copyright 2026 肖其顿 (XIAO QI DUN)
|
||||||
|
|
||||||
|
This product includes software developed by
|
||||||
|
肖其顿 (XIAO QI DUN) (https://github.com/xiaoqidun/probe).
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
// Copyright 2026 肖其顿 (XIAO QI DUN)
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var (
|
||||||
|
ip string
|
||||||
|
s1 string
|
||||||
|
s2 string
|
||||||
|
s5 string
|
||||||
|
to time.Duration
|
||||||
|
)
|
||||||
|
flag.StringVar(&ip, "ip", "", "协议版本")
|
||||||
|
flag.StringVar(&s1, "s1", "stun.l.google.com:19302", "主服务器")
|
||||||
|
flag.StringVar(&s2, "s2", "", "次服务器")
|
||||||
|
flag.StringVar(&s5, "s5", "", "代理地址")
|
||||||
|
flag.DurationVar(&to, "to", 5*time.Second, "超时时间")
|
||||||
|
flag.Parse()
|
||||||
|
network := "udp"
|
||||||
|
switch ip {
|
||||||
|
case "":
|
||||||
|
network = "udp"
|
||||||
|
case "4":
|
||||||
|
network = "udp4"
|
||||||
|
case "6":
|
||||||
|
network = "udp6"
|
||||||
|
default:
|
||||||
|
flag.PrintDefaults()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var conn net.PacketConn
|
||||||
|
var err error
|
||||||
|
if s5 != "" {
|
||||||
|
fmt.Printf("通过代理探测: %s\n", s5)
|
||||||
|
conn, err = DialSocks5UDP(s5)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("连接代理失败: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Printf("本地直接探测: %s\n", network)
|
||||||
|
conn, err = net.ListenPacket(network, ":0")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("本地监听失败: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
if s5 == "" {
|
||||||
|
fmt.Printf("本地监听地址: %s\n", conn.LocalAddr())
|
||||||
|
}
|
||||||
|
s1p := fmt.Sprintf("探测服务器一: %s (%s)", s1, network)
|
||||||
|
fmt.Println(s1p)
|
||||||
|
maxW := displayWidth(s1p)
|
||||||
|
if s2 != "" {
|
||||||
|
s2p := fmt.Sprintf("探测服务器二: %s (%s)", s2, network)
|
||||||
|
fmt.Println(s2p)
|
||||||
|
if w2 := displayWidth(s2p); w2 > maxW {
|
||||||
|
maxW = w2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Println(strings.Repeat("-", maxW))
|
||||||
|
result := DetectNAT(conn, s1, s2, network, to)
|
||||||
|
fmt.Printf("NAT 类型结果: %s\n", result.Type)
|
||||||
|
if result.MappedIP != "" {
|
||||||
|
fmt.Printf("本地映射地址: %s\n", result.MappedIP)
|
||||||
|
}
|
||||||
|
fmt.Printf("映射行为模式: %s\n", result.Mapping)
|
||||||
|
fmt.Printf("过滤行为模式: %s\n", result.Filtering)
|
||||||
|
}
|
||||||
+228
@@ -0,0 +1,228 @@
|
|||||||
|
// Copyright 2026 肖其顿 (XIAO QI DUN)
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"net"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MappingEndpointIndependent 独立映射模式常量
|
||||||
|
const MappingEndpointIndependent = "Endpoint-Independent"
|
||||||
|
|
||||||
|
// MappingAddressDependent 地址相关映射模式常量
|
||||||
|
const MappingAddressDependent = "Address-Dependent"
|
||||||
|
|
||||||
|
// MappingAddressPortDependent 地址端口相关映射模式常量
|
||||||
|
const MappingAddressPortDependent = "Address and Port-Dependent"
|
||||||
|
|
||||||
|
// MappingUnknown 未知映射模式常量
|
||||||
|
const MappingUnknown = "Unknown"
|
||||||
|
|
||||||
|
// FilteringEndpointIndependent 独立过滤模式常量
|
||||||
|
const FilteringEndpointIndependent = "Endpoint-Independent"
|
||||||
|
|
||||||
|
// FilteringAddressDependent 地址相关过滤模式常量
|
||||||
|
const FilteringAddressDependent = "Address-Dependent"
|
||||||
|
|
||||||
|
// FilteringAddressPortDependent 地址端口相关过滤模式常量
|
||||||
|
const FilteringAddressPortDependent = "Address and Port-Dependent"
|
||||||
|
|
||||||
|
// FilteringUnknown 未知过滤模式常量
|
||||||
|
const FilteringUnknown = "Unknown"
|
||||||
|
|
||||||
|
// NATOpen 公网类型常量
|
||||||
|
const NATOpen = "Open Internet"
|
||||||
|
|
||||||
|
// NATFullCone 全锥型NAT常量
|
||||||
|
const NATFullCone = "Full Cone"
|
||||||
|
|
||||||
|
// NATRestricted 限制锥型NAT常量
|
||||||
|
const NATRestricted = "Restricted Cone"
|
||||||
|
|
||||||
|
// NATPortRestricted 端口限制锥型NAT常量
|
||||||
|
const NATPortRestricted = "Port Restricted Cone"
|
||||||
|
|
||||||
|
// NATSymmetric 对称型NAT常量
|
||||||
|
const NATSymmetric = "Symmetric"
|
||||||
|
|
||||||
|
// NATUDPBlocked UDP阻塞常量
|
||||||
|
const NATUDPBlocked = "UDP Blocked"
|
||||||
|
|
||||||
|
// NATUnknown 未知类型常量
|
||||||
|
const NATUnknown = "Unknown"
|
||||||
|
|
||||||
|
// NATResult NAT探测结果结构
|
||||||
|
type NATResult struct {
|
||||||
|
Type string
|
||||||
|
Mapping string
|
||||||
|
Filtering string
|
||||||
|
MappedIP string
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveAddr 解析探测目标地址
|
||||||
|
// 入参: conn 当前连接, addrStr 目标地址字符串, network 网络协议
|
||||||
|
// 返回: addr 解析后的网络地址, err 解析错误
|
||||||
|
func resolveAddr(conn net.PacketConn, addrStr, network string) (net.Addr, error) {
|
||||||
|
host, portStr, err := net.SplitHostPort(addrStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
port, err := strconv.Atoi(portStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, ok := conn.(*socks5PacketConn); ok {
|
||||||
|
if net.ParseIP(host) == nil && host != "localhost" && host != "127.0.0.1" {
|
||||||
|
return &SocksAddr{Host: host, Port: port}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return net.ResolveUDPAddr(network, addrStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// performTest 执行单次STUN探测
|
||||||
|
// 入参: conn 连接对象, serverAddr STUN服务器地址, network 协议, timeout 超时设定, changeIP 变更IP标志, changePort 变更端口标志
|
||||||
|
// 返回: msg 响应消息, addr 响应源地址, err 探测错误
|
||||||
|
func performTest(conn net.PacketConn, serverAddr string, network string, timeout time.Duration, changeIP, changePort bool) (*stunMessage, *net.UDPAddr, error) {
|
||||||
|
dst, err := resolveAddr(conn, serverAddr, network)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
txID := [12]byte{}
|
||||||
|
rand.Read(txID[:])
|
||||||
|
req := encodeSTUNRequest(txID, changeIP, changePort)
|
||||||
|
if _, err := conn.WriteTo(req, dst); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
conn.SetReadDeadline(time.Now().Add(timeout))
|
||||||
|
defer conn.SetReadDeadline(time.Time{})
|
||||||
|
buf := make([]byte, 2048)
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
n, addr, err := conn.ReadFrom(buf)
|
||||||
|
if err != nil {
|
||||||
|
if nerr, ok := err.(net.Error); ok && nerr.Timeout() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
msg, err := decodeSTUNResponse(buf[:n], txID)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
uAddr, ok := addr.(*net.UDPAddr)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return msg, uAddr, nil
|
||||||
|
}
|
||||||
|
return nil, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DetectNAT 执行NAT类型检测核心逻辑
|
||||||
|
// 入参: conn 连接对象, primarySTUN 主服务器, secondarySTUN 辅服务器, network 协议, timeout 超时设定
|
||||||
|
// 返回: result 检测结果
|
||||||
|
func DetectNAT(conn net.PacketConn, primarySTUN, secondarySTUN, network string, timeout time.Duration) NATResult {
|
||||||
|
res := NATResult{Type: NATUnknown, Mapping: MappingUnknown, Filtering: FilteringUnknown}
|
||||||
|
resp1, _, err := performTest(conn, primarySTUN, network, timeout, false, false)
|
||||||
|
if err != nil || resp1 == nil {
|
||||||
|
res.Type = NATUDPBlocked
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
mappedAddr1 := resp1.GetMappedAddress()
|
||||||
|
if mappedAddr1 == nil {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
res.MappedIP = mappedAddr1.String()
|
||||||
|
var targetSTUN2 string
|
||||||
|
if secondarySTUN != "" {
|
||||||
|
targetSTUN2 = secondarySTUN
|
||||||
|
} else {
|
||||||
|
changedAddr := resp1.GetChangedAddress()
|
||||||
|
if changedAddr != nil {
|
||||||
|
targetSTUN2 = net.JoinHostPort(changedAddr.IP.String(), strconv.Itoa(changedAddr.Port))
|
||||||
|
} else {
|
||||||
|
host, port, _ := net.SplitHostPort(primarySTUN)
|
||||||
|
ips, err := net.LookupIP(host)
|
||||||
|
if err == nil && len(ips) > 1 {
|
||||||
|
primaryIP, _ := net.ResolveIPAddr("ip", host)
|
||||||
|
wantIPv4 := network == "udp4"
|
||||||
|
wantIPv6 := network == "udp6"
|
||||||
|
if network == "udp" {
|
||||||
|
wantIPv4 = primaryIP.IP.To4() != nil
|
||||||
|
wantIPv6 = !wantIPv4
|
||||||
|
}
|
||||||
|
for _, ip := range ips {
|
||||||
|
if ip.Equal(primaryIP.IP) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
isV4 := ip.To4() != nil
|
||||||
|
if wantIPv4 && !isV4 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if wantIPv6 && isV4 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
targetSTUN2 = net.JoinHostPort(ip.String(), port)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var mappedAddr3 *net.UDPAddr
|
||||||
|
if targetSTUN2 != "" {
|
||||||
|
resp3, _, err := performTest(conn, targetSTUN2, network, timeout, false, false)
|
||||||
|
if err == nil && resp3 != nil {
|
||||||
|
mappedAddr3 = resp3.GetMappedAddress()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if mappedAddr3 != nil {
|
||||||
|
if mappedAddr1.String() == mappedAddr3.String() {
|
||||||
|
res.Mapping = MappingEndpointIndependent
|
||||||
|
} else {
|
||||||
|
res.Mapping = MappingAddressPortDependent
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
res.Mapping = MappingUnknown
|
||||||
|
}
|
||||||
|
resp2, _, _ := performTest(conn, primarySTUN, network, timeout, true, true)
|
||||||
|
if resp2 != nil {
|
||||||
|
res.Filtering = FilteringEndpointIndependent
|
||||||
|
} else {
|
||||||
|
resp4, _, _ := performTest(conn, primarySTUN, network, timeout, false, true)
|
||||||
|
if resp4 != nil {
|
||||||
|
res.Filtering = FilteringAddressDependent
|
||||||
|
} else {
|
||||||
|
res.Filtering = FilteringAddressPortDependent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if res.Filtering == FilteringEndpointIndependent {
|
||||||
|
res.Type = NATFullCone
|
||||||
|
if res.Mapping == MappingUnknown {
|
||||||
|
res.Mapping = MappingEndpointIndependent
|
||||||
|
}
|
||||||
|
} else if res.Mapping == MappingEndpointIndependent {
|
||||||
|
switch res.Filtering {
|
||||||
|
case FilteringAddressDependent:
|
||||||
|
res.Type = NATRestricted
|
||||||
|
case FilteringAddressPortDependent:
|
||||||
|
res.Type = NATPortRestricted
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
res.Type = NATSymmetric
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
+275
@@ -0,0 +1,275 @@
|
|||||||
|
// Copyright 2026 肖其顿 (XIAO QI DUN)
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SocksAddr SOCKS5域名地址类型
|
||||||
|
type SocksAddr struct {
|
||||||
|
Host string
|
||||||
|
Port int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Network 返回网络类型
|
||||||
|
// 返回: network 网络类型字符串
|
||||||
|
func (a *SocksAddr) Network() string { return "udp" }
|
||||||
|
|
||||||
|
// String 返回地址字符串表示
|
||||||
|
// 返回: str 地址字符串
|
||||||
|
func (a *SocksAddr) String() string { return net.JoinHostPort(a.Host, strconv.Itoa(a.Port)) }
|
||||||
|
|
||||||
|
// socks5PacketConn SOCKS5数据包连接实现
|
||||||
|
type socks5PacketConn struct {
|
||||||
|
tcpConn net.Conn
|
||||||
|
udpConn *net.UDPConn
|
||||||
|
relayAddr *net.UDPAddr
|
||||||
|
targetAddr net.Addr
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadFrom 从UDP连接读取数据
|
||||||
|
// 入参: p 读取缓冲区
|
||||||
|
// 返回: n 读取字节数, addr 来源地址, err 读取错误
|
||||||
|
func (c *socks5PacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
|
||||||
|
buf := make([]byte, 65535)
|
||||||
|
n, _, err = c.udpConn.ReadFromUDP(buf)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
if n < 10 {
|
||||||
|
return 0, nil, nil
|
||||||
|
}
|
||||||
|
atyp := buf[3]
|
||||||
|
var rAddr *net.UDPAddr
|
||||||
|
var dataOffset int
|
||||||
|
switch atyp {
|
||||||
|
case 0x01:
|
||||||
|
if n < 10 {
|
||||||
|
return 0, nil, errors.New("short packet")
|
||||||
|
}
|
||||||
|
ip := net.IP(buf[4:8])
|
||||||
|
port := binary.BigEndian.Uint16(buf[8:10])
|
||||||
|
rAddr = &net.UDPAddr{IP: ip, Port: int(port)}
|
||||||
|
dataOffset = 10
|
||||||
|
case 0x03:
|
||||||
|
dlen := int(buf[4])
|
||||||
|
if n < 5+dlen+2 {
|
||||||
|
return 0, nil, errors.New("short packet")
|
||||||
|
}
|
||||||
|
domain := string(buf[5 : 5+dlen])
|
||||||
|
port := binary.BigEndian.Uint16(buf[5+dlen : 5+dlen+2])
|
||||||
|
ipAddr, err := net.ResolveIPAddr("ip", domain)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, fmt.Errorf("failed to resolve payload domain: %v", err)
|
||||||
|
}
|
||||||
|
rAddr = &net.UDPAddr{IP: ipAddr.IP, Port: int(port)}
|
||||||
|
dataOffset = 5 + dlen + 2
|
||||||
|
case 0x04:
|
||||||
|
if n < 22 {
|
||||||
|
return 0, nil, errors.New("short packet")
|
||||||
|
}
|
||||||
|
ip := net.IP(buf[4:20])
|
||||||
|
port := binary.BigEndian.Uint16(buf[20:22])
|
||||||
|
rAddr = &net.UDPAddr{IP: ip, Port: int(port)}
|
||||||
|
dataOffset = 22
|
||||||
|
}
|
||||||
|
copy(p, buf[dataOffset:n])
|
||||||
|
return n - dataOffset, rAddr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteTo 写入数据到目标地址
|
||||||
|
// 入参: p 数据内容, addr 目标地址
|
||||||
|
// 返回: n 写入字节数, err 写入错误
|
||||||
|
func (c *socks5PacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
|
||||||
|
header := make([]byte, 0, 24)
|
||||||
|
header = append(header, 0, 0, 0)
|
||||||
|
switch a := addr.(type) {
|
||||||
|
case *net.UDPAddr:
|
||||||
|
ip4 := a.IP.To4()
|
||||||
|
if ip4 != nil {
|
||||||
|
header = append(header, 0x01)
|
||||||
|
header = append(header, ip4...)
|
||||||
|
} else {
|
||||||
|
if len(a.IP) == 16 {
|
||||||
|
header = append(header, 0x04)
|
||||||
|
header = append(header, a.IP...)
|
||||||
|
} else {
|
||||||
|
return 0, errors.New("unknown ip type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
portBuf := make([]byte, 2)
|
||||||
|
binary.BigEndian.PutUint16(portBuf, uint16(a.Port))
|
||||||
|
header = append(header, portBuf...)
|
||||||
|
case *SocksAddr:
|
||||||
|
header = append(header, 0x03)
|
||||||
|
header = append(header, byte(len(a.Host)))
|
||||||
|
header = append(header, a.Host...)
|
||||||
|
portBuf := make([]byte, 2)
|
||||||
|
binary.BigEndian.PutUint16(portBuf, uint16(a.Port))
|
||||||
|
header = append(header, portBuf...)
|
||||||
|
default:
|
||||||
|
return 0, errors.New("unsupported address type")
|
||||||
|
}
|
||||||
|
finalBuf := append(header, p...)
|
||||||
|
_, err = c.udpConn.WriteToUDP(finalBuf, c.relayAddr)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close 关闭连接
|
||||||
|
// 返回: err 关闭错误
|
||||||
|
func (c *socks5PacketConn) Close() error {
|
||||||
|
c.tcpConn.Close()
|
||||||
|
return c.udpConn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// LocalAddr 获取本地地址
|
||||||
|
// 返回: addr 本地地址
|
||||||
|
func (c *socks5PacketConn) LocalAddr() net.Addr {
|
||||||
|
return c.udpConn.LocalAddr()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDeadline 设置读写截止时间
|
||||||
|
// 入参: t 截止时间
|
||||||
|
// 返回: err 设置错误
|
||||||
|
func (c *socks5PacketConn) SetDeadline(t time.Time) error {
|
||||||
|
return c.udpConn.SetDeadline(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetReadDeadline 设置读取截止时间
|
||||||
|
// 入参: t 截止时间
|
||||||
|
// 返回: err 设置错误
|
||||||
|
func (c *socks5PacketConn) SetReadDeadline(t time.Time) error {
|
||||||
|
return c.udpConn.SetReadDeadline(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetWriteDeadline 设置写入截止时间
|
||||||
|
// 入参: t 截止时间
|
||||||
|
// 返回: err 设置错误
|
||||||
|
func (c *socks5PacketConn) SetWriteDeadline(t time.Time) error {
|
||||||
|
return c.udpConn.SetWriteDeadline(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialSocks5UDP 建立SOCKS5 UDP关联
|
||||||
|
// 入参: proxyAddr 代理服务器地址
|
||||||
|
// 返回: conn 数据包连接, err 连接错误
|
||||||
|
func DialSocks5UDP(proxyAddr string) (net.PacketConn, error) {
|
||||||
|
var host string
|
||||||
|
if strings.Contains(proxyAddr, "://") {
|
||||||
|
u, err := url.Parse(proxyAddr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
host = u.Host
|
||||||
|
} else {
|
||||||
|
host = proxyAddr
|
||||||
|
}
|
||||||
|
conn, err := net.DialTimeout("tcp", host, 5*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_, err = conn.Write([]byte{0x05, 0x01, 0x00})
|
||||||
|
if err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
buf := make([]byte, 2)
|
||||||
|
if _, err := io.ReadFull(conn, buf); err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if buf[0] != 0x05 || buf[1] != 0x00 {
|
||||||
|
conn.Close()
|
||||||
|
return nil, errors.New("socks5 handshake failed")
|
||||||
|
}
|
||||||
|
req := []byte{0x05, 0x03, 0x00, 0x01, 0, 0, 0, 0, 0, 0}
|
||||||
|
if _, err := conn.Write(req); err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
header := make([]byte, 4)
|
||||||
|
if _, err := io.ReadFull(conn, header); err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if header[1] != 0x00 {
|
||||||
|
conn.Close()
|
||||||
|
return nil, fmt.Errorf("socks5 udp associate failed: 0x%x", header[1])
|
||||||
|
}
|
||||||
|
var relayIP net.IP
|
||||||
|
var relayPort int
|
||||||
|
switch header[3] {
|
||||||
|
case 0x01:
|
||||||
|
b := make([]byte, 4)
|
||||||
|
if _, err := io.ReadFull(conn, b); err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
relayIP = net.IP(b)
|
||||||
|
case 0x03:
|
||||||
|
lenBuf := make([]byte, 1)
|
||||||
|
if _, err := io.ReadFull(conn, lenBuf); err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
domainBuf := make([]byte, int(lenBuf[0]))
|
||||||
|
if _, err := io.ReadFull(conn, domainBuf); err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
addr, err := net.ResolveIPAddr("ip", string(domainBuf))
|
||||||
|
if err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
relayIP = addr.IP
|
||||||
|
case 0x04:
|
||||||
|
b := make([]byte, 16)
|
||||||
|
if _, err := io.ReadFull(conn, b); err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
relayIP = net.IP(b)
|
||||||
|
}
|
||||||
|
pb := make([]byte, 2)
|
||||||
|
if _, err := io.ReadFull(conn, pb); err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
relayPort = int(binary.BigEndian.Uint16(pb))
|
||||||
|
if relayIP.IsUnspecified() {
|
||||||
|
if remoteAddr, ok := conn.RemoteAddr().(*net.TCPAddr); ok {
|
||||||
|
relayIP = remoteAddr.IP
|
||||||
|
}
|
||||||
|
}
|
||||||
|
relayAddr := &net.UDPAddr{IP: relayIP, Port: relayPort}
|
||||||
|
lConn, err := net.ListenUDP("udp", nil)
|
||||||
|
if err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &socks5PacketConn{tcpConn: conn, udpConn: lConn, relayAddr: relayAddr}, nil
|
||||||
|
}
|
||||||
+209
@@ -0,0 +1,209 @@
|
|||||||
|
// Copyright 2026 肖其顿 (XIAO QI DUN)
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
// stunMagicCookie be32用于STUN协议的魔法数
|
||||||
|
const stunMagicCookie = 0x2112A442
|
||||||
|
|
||||||
|
// bindRequest STUN绑定请求消息类型
|
||||||
|
const bindRequest = 0x0001
|
||||||
|
|
||||||
|
// bindResponse STUN绑定响应消息类型
|
||||||
|
const bindResponse = 0x0101
|
||||||
|
|
||||||
|
// attrMappedAddress 映射地址属性类型
|
||||||
|
const attrMappedAddress = 0x0001
|
||||||
|
|
||||||
|
// attrChangeRequest 修改请求属性类型
|
||||||
|
const attrChangeRequest = 0x0003
|
||||||
|
|
||||||
|
// attrChangedAddress 修改后地址属性类型
|
||||||
|
const attrChangedAddress = 0x0005
|
||||||
|
|
||||||
|
// attrXorMappedAddress 异或映射地址属性类型
|
||||||
|
const attrXorMappedAddress = 0x0020
|
||||||
|
|
||||||
|
// attrOtherAddress 其他地址属性类型
|
||||||
|
const attrOtherAddress = 0x802c
|
||||||
|
|
||||||
|
// stunHeader STUN消息头部结构
|
||||||
|
type stunHeader struct {
|
||||||
|
Type uint16
|
||||||
|
Length uint16
|
||||||
|
Cookie uint32
|
||||||
|
ID [12]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// stunMessage STUN消息完整结构
|
||||||
|
type stunMessage struct {
|
||||||
|
Header stunHeader
|
||||||
|
Attributes []stunAttribute
|
||||||
|
}
|
||||||
|
|
||||||
|
// stunAttribute STUN属性结构
|
||||||
|
type stunAttribute struct {
|
||||||
|
Type uint16
|
||||||
|
Length uint16
|
||||||
|
Value []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodeSTUNRequest 构造并编码STUN绑定请求消息
|
||||||
|
// 入参: id 事务ID, changeIP 是否请求改变IP, changePort 是否请求改变端口
|
||||||
|
// 返回: data 编码后的二进制数据
|
||||||
|
func encodeSTUNRequest(id [12]byte, changeIP, changePort bool) []byte {
|
||||||
|
buf := make([]byte, 1024)
|
||||||
|
binary.BigEndian.PutUint16(buf[0:2], bindRequest)
|
||||||
|
binary.BigEndian.PutUint32(buf[4:8], stunMagicCookie)
|
||||||
|
copy(buf[8:20], id[:])
|
||||||
|
offset := 20
|
||||||
|
if changeIP || changePort {
|
||||||
|
binary.BigEndian.PutUint16(buf[offset:offset+2], attrChangeRequest)
|
||||||
|
binary.BigEndian.PutUint16(buf[offset+2:offset+4], 4)
|
||||||
|
offset += 4
|
||||||
|
val := uint32(0)
|
||||||
|
if changeIP {
|
||||||
|
val |= 0x04
|
||||||
|
}
|
||||||
|
if changePort {
|
||||||
|
val |= 0x02
|
||||||
|
}
|
||||||
|
binary.BigEndian.PutUint32(buf[offset:offset+4], val)
|
||||||
|
offset += 4
|
||||||
|
}
|
||||||
|
binary.BigEndian.PutUint16(buf[2:4], uint16(offset-20))
|
||||||
|
return buf[:offset]
|
||||||
|
}
|
||||||
|
|
||||||
|
// decodeSTUNResponse 解析STUN响应消息
|
||||||
|
// 入参: data 接收到的二进制数据, txID 期望的事务ID
|
||||||
|
// 返回: msg 解析后的消息结构体指针, err 解析错误信息
|
||||||
|
func decodeSTUNResponse(data []byte, txID [12]byte) (*stunMessage, error) {
|
||||||
|
if len(data) < 20 {
|
||||||
|
return nil, errors.New("response too short")
|
||||||
|
}
|
||||||
|
msgType := binary.BigEndian.Uint16(data[0:2])
|
||||||
|
if msgType != bindResponse {
|
||||||
|
return nil, fmt.Errorf("unexpected message type: 0x%x", msgType)
|
||||||
|
}
|
||||||
|
length := binary.BigEndian.Uint16(data[2:4])
|
||||||
|
if len(data) < 20+int(length) {
|
||||||
|
return nil, errors.New("incomplete message")
|
||||||
|
}
|
||||||
|
cookie := binary.BigEndian.Uint32(data[4:8])
|
||||||
|
if string(data[8:20]) != string(txID[:]) {
|
||||||
|
return nil, errors.New("transaction id mismatch")
|
||||||
|
}
|
||||||
|
msg := &stunMessage{Header: stunHeader{Type: msgType, Length: length, Cookie: cookie, ID: txID}}
|
||||||
|
offset := 20
|
||||||
|
end := 20 + int(length)
|
||||||
|
for offset < end {
|
||||||
|
if offset+4 > end {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
attrType := binary.BigEndian.Uint16(data[offset : offset+2])
|
||||||
|
attrLen := binary.BigEndian.Uint16(data[offset+2 : offset+4])
|
||||||
|
offset += 4
|
||||||
|
if offset+int(attrLen) > end {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
val := make([]byte, attrLen)
|
||||||
|
copy(val, data[offset:offset+int(attrLen)])
|
||||||
|
msg.Attributes = append(msg.Attributes, stunAttribute{Type: attrType, Length: attrLen, Value: val})
|
||||||
|
offset += int(attrLen)
|
||||||
|
padding := (4 - (int(attrLen) % 4)) % 4
|
||||||
|
offset += padding
|
||||||
|
}
|
||||||
|
return msg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseAddress 解析STUN属性中的地址信息
|
||||||
|
// 入参: attrType 属性类型, data 属性值数据
|
||||||
|
// 返回: addr 解析出的UDP地址, err 解析错误信息
|
||||||
|
func parseAddress(attrType uint16, data []byte) (*net.UDPAddr, error) {
|
||||||
|
if len(data) < 4 {
|
||||||
|
return nil, errors.New("attribute too short")
|
||||||
|
}
|
||||||
|
family := data[1]
|
||||||
|
port := binary.BigEndian.Uint16(data[2:4])
|
||||||
|
ipLen := 4
|
||||||
|
if family == 0x02 {
|
||||||
|
ipLen = 16
|
||||||
|
} else if family != 0x01 {
|
||||||
|
return nil, fmt.Errorf("unknown address family: %d", family)
|
||||||
|
}
|
||||||
|
if len(data) < 4+ipLen {
|
||||||
|
return nil, errors.New("invalid address length")
|
||||||
|
}
|
||||||
|
ip := make(net.IP, ipLen)
|
||||||
|
copy(ip, data[4:4+ipLen])
|
||||||
|
if attrType == attrXorMappedAddress {
|
||||||
|
port ^= uint16(stunMagicCookie >> 16)
|
||||||
|
if ipLen == 4 {
|
||||||
|
mc := make([]byte, 4)
|
||||||
|
binary.BigEndian.PutUint32(mc, stunMagicCookie)
|
||||||
|
for i := 0; i < 4; i++ {
|
||||||
|
ip[i] ^= mc[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &net.UDPAddr{IP: ip, Port: int(port)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMappedAddress 获取消息中的映射地址属性
|
||||||
|
// 返回: addr 映射的UDP地址
|
||||||
|
func (m *stunMessage) GetMappedAddress() *net.UDPAddr {
|
||||||
|
for _, attr := range m.Attributes {
|
||||||
|
if attr.Type == attrXorMappedAddress {
|
||||||
|
if addr, err := parseAddress(attr.Type, attr.Value); err == nil {
|
||||||
|
return addr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, attr := range m.Attributes {
|
||||||
|
if attr.Type == attrMappedAddress {
|
||||||
|
if addr, err := parseAddress(attr.Type, attr.Value); err == nil {
|
||||||
|
return addr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetChangedAddress 获取消息中的变更地址属性
|
||||||
|
// 返回: addr 变更的UDP地址
|
||||||
|
func (m *stunMessage) GetChangedAddress() *net.UDPAddr {
|
||||||
|
for _, attr := range m.Attributes {
|
||||||
|
if attr.Type == attrChangedAddress {
|
||||||
|
if addr, err := parseAddress(attr.Type, attr.Value); err == nil {
|
||||||
|
return addr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, attr := range m.Attributes {
|
||||||
|
if attr.Type == attrOtherAddress {
|
||||||
|
if addr, err := parseAddress(attr.Type, attr.Value); err == nil {
|
||||||
|
return addr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
// Copyright 2026 肖其顿 (XIAO QI DUN)
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
// displayWidth 计算字符串显示宽度
|
||||||
|
// 入参: s 字符串
|
||||||
|
// 返回: w 视觉宽度 (中字2, 英字1)
|
||||||
|
func displayWidth(s string) int {
|
||||||
|
w := 0
|
||||||
|
for _, r := range s {
|
||||||
|
if r > 127 {
|
||||||
|
w += 2
|
||||||
|
} else {
|
||||||
|
w += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return w
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user