-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
288 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,257 @@ | ||
package outbounds | ||
|
||
import ( | ||
"encoding/binary" | ||
"errors" | ||
"fmt" | ||
"io" | ||
"net" | ||
"time" | ||
|
||
"github.com/txthinking/socks5" | ||
) | ||
|
||
const ( | ||
socks5NegotiationTimeout = 10 * time.Second | ||
socks5RequestTimeout = 10 * time.Second | ||
) | ||
|
||
var errSOCKS5AuthFailed = errors.New("SOCKS5 authentication failed") | ||
|
||
type errSOCKS5UnsupportedAuthMethod struct { | ||
Method byte | ||
} | ||
|
||
func (e errSOCKS5UnsupportedAuthMethod) Error() string { | ||
return fmt.Sprintf("unsupported SOCKS5 authentication method: %d", e.Method) | ||
} | ||
|
||
type errSOCKS5RequestFailed struct { | ||
Rep byte | ||
} | ||
|
||
func (e errSOCKS5RequestFailed) Error() string { | ||
return fmt.Sprintf("SOCKS5 request failed: %d", e.Rep) | ||
} | ||
|
||
// socks5Outbound is a PluggableOutbound that connects to the target using | ||
// a SOCKS5 proxy server. | ||
// Since SOCKS5 supports using either IP or domain name as the target address, | ||
// it will ignore ResolveInfo in AddrEx and always only use Host. | ||
type socks5Outbound struct { | ||
Dialer *net.Dialer | ||
Addr string | ||
Username string | ||
Password string | ||
} | ||
|
||
func NewSOCKS5Outbound(addr, username, password string) PluggableOutbound { | ||
return &socks5Outbound{ | ||
Dialer: &net.Dialer{ | ||
Timeout: defaultDialerTimeout, | ||
}, | ||
Addr: addr, | ||
Username: username, | ||
Password: password, | ||
} | ||
} | ||
|
||
// dialAndNegotiate creates a new TCP connection to the SOCKS5 proxy server | ||
// and performs the negotiation. Returns an established connection ready to | ||
// handle requests, or an error if the process fails. | ||
func (o *socks5Outbound) dialAndNegotiate() (net.Conn, error) { | ||
conn, err := o.Dialer.Dial("tcp", o.Addr) | ||
if err != nil { | ||
return nil, err | ||
} | ||
if err := conn.SetDeadline(time.Now().Add(socks5NegotiationTimeout)); err != nil { | ||
_ = conn.Close() | ||
return nil, err | ||
} | ||
authMethods := []byte{socks5.MethodNone} | ||
if o.Username != "" && o.Password != "" { | ||
authMethods = append(authMethods, socks5.MethodUsernamePassword) | ||
} | ||
req := socks5.NewNegotiationRequest(authMethods) | ||
if _, err := req.WriteTo(conn); err != nil { | ||
_ = conn.Close() | ||
return nil, err | ||
} | ||
resp, err := socks5.NewNegotiationReplyFrom(conn) | ||
if err != nil { | ||
_ = conn.Close() | ||
return nil, err | ||
} | ||
if resp.Method == socks5.MethodUsernamePassword { | ||
upReq := socks5.NewUserPassNegotiationRequest([]byte(o.Username), []byte(o.Password)) | ||
if _, err := upReq.WriteTo(conn); err != nil { | ||
_ = conn.Close() | ||
return nil, err | ||
} | ||
upResp, err := socks5.NewUserPassNegotiationReplyFrom(conn) | ||
if err != nil { | ||
_ = conn.Close() | ||
return nil, err | ||
} | ||
if upResp.Status != socks5.UserPassStatusSuccess { | ||
_ = conn.Close() | ||
return nil, errSOCKS5AuthFailed | ||
} | ||
} else if resp.Method != socks5.MethodNone { | ||
// We only support none & username/password authentication methods. | ||
_ = conn.Close() | ||
return nil, errSOCKS5UnsupportedAuthMethod{resp.Method} | ||
} | ||
// Negotiation succeeded, reset the deadline. | ||
if err := conn.SetDeadline(time.Time{}); err != nil { | ||
_ = conn.Close() | ||
return nil, err | ||
} | ||
return conn, nil | ||
} | ||
|
||
// request sends a SOCKS5 request to the proxy server and returns the reply. | ||
// Note that it will return an error if the reply from the server indicates | ||
// a failure. | ||
func (o *socks5Outbound) request(conn net.Conn, req *socks5.Request) (*socks5.Reply, error) { | ||
if err := conn.SetDeadline(time.Now().Add(socks5RequestTimeout)); err != nil { | ||
return nil, err | ||
} | ||
if _, err := req.WriteTo(conn); err != nil { | ||
return nil, err | ||
} | ||
resp, err := socks5.NewReplyFrom(conn) | ||
if err != nil { | ||
return nil, err | ||
} | ||
if resp.Rep != socks5.RepSuccess { | ||
return nil, errSOCKS5RequestFailed{resp.Rep} | ||
} | ||
if err := conn.SetDeadline(time.Time{}); err != nil { | ||
return nil, err | ||
} | ||
return resp, nil | ||
} | ||
|
||
func (s *socks5Outbound) TCP(reqAddr *AddrEx) (net.Conn, error) { | ||
conn, err := s.dialAndNegotiate() | ||
if err != nil { | ||
return nil, err | ||
} | ||
atyp, dstAddr, dstPort := addrExToSOCKS5Addr(reqAddr) | ||
req := socks5.NewRequest(socks5.CmdConnect, atyp, dstAddr, dstPort) | ||
if _, err := s.request(conn, req); err != nil { | ||
_ = conn.Close() | ||
return nil, err | ||
} | ||
return conn, nil | ||
} | ||
|
||
func (s *socks5Outbound) UDP(reqAddr *AddrEx) (UDPConn, error) { | ||
conn, err := s.dialAndNegotiate() | ||
if err != nil { | ||
return nil, err | ||
} | ||
atyp, dstAddr, dstPort := addrExToSOCKS5Addr(reqAddr) | ||
req := socks5.NewRequest(socks5.CmdUDP, atyp, dstAddr, dstPort) | ||
resp, err := s.request(conn, req) | ||
if err != nil { | ||
_ = conn.Close() | ||
return nil, err | ||
} | ||
return newSOCKS5UDPConn(conn, resp.Address()) | ||
} | ||
|
||
type socks5UDPConn struct { | ||
tcpConn net.Conn | ||
udpConn net.Conn | ||
} | ||
|
||
func newSOCKS5UDPConn(tcpConn net.Conn, udpAddr string) (*socks5UDPConn, error) { | ||
udpConn, err := net.Dial("udp", udpAddr) | ||
if err != nil { | ||
return nil, err | ||
} | ||
sc := &socks5UDPConn{ | ||
tcpConn: tcpConn, | ||
udpConn: udpConn, | ||
} | ||
go sc.hold() | ||
return sc, nil | ||
} | ||
|
||
func (c *socks5UDPConn) hold() { | ||
_, _ = io.Copy(io.Discard, c.tcpConn) | ||
_ = c.tcpConn.Close() | ||
_ = c.udpConn.Close() | ||
} | ||
|
||
func (c *socks5UDPConn) ReadFrom(b []byte) (int, *AddrEx, error) { | ||
n, err := c.udpConn.Read(b) | ||
if err != nil { | ||
return 0, nil, err | ||
} | ||
d, err := socks5.NewDatagramFromBytes(b[:n]) | ||
if err != nil { | ||
return 0, nil, err | ||
} | ||
addr := socks5AddrToAddrEx(d.Atyp, d.DstAddr, d.DstPort) | ||
n = copy(b, d.Data) | ||
return n, addr, nil | ||
} | ||
|
||
func (c *socks5UDPConn) WriteTo(b []byte, addr *AddrEx) (int, error) { | ||
atyp, dstAddr, dstPort := addrExToSOCKS5Addr(addr) | ||
d := socks5.NewDatagram(atyp, dstAddr, dstPort, b) | ||
_, err := c.udpConn.Write(d.Bytes()) | ||
if err != nil { | ||
return 0, err | ||
} | ||
return len(b), nil | ||
} | ||
|
||
func (c *socks5UDPConn) Close() error { | ||
_ = c.tcpConn.Close() | ||
_ = c.udpConn.Close() | ||
return nil | ||
} | ||
|
||
func addrExToSOCKS5Addr(addr *AddrEx) (atyp byte, dstAddr, dstPort []byte) { | ||
// Host | ||
ip := net.ParseIP(addr.Host) | ||
if ip != nil { | ||
if ip.To4() != nil { | ||
atyp = socks5.ATYPIPv4 | ||
dstAddr = ip.To4() | ||
} else { | ||
atyp = socks5.ATYPIPv6 | ||
dstAddr = ip.To16() | ||
} | ||
} else { | ||
atyp = socks5.ATYPDomain | ||
dstAddr = []byte(addr.Host) | ||
} | ||
// Port | ||
dstPort = make([]byte, 2) | ||
binary.BigEndian.PutUint16(dstPort, addr.Port) | ||
return | ||
} | ||
|
||
func socks5AddrToAddrEx(atyp byte, dstAddr, dstPort []byte) *AddrEx { | ||
// Host | ||
var host string | ||
if atyp == socks5.ATYPIPv4 { | ||
host = net.IP(dstAddr).To4().String() | ||
} else if atyp == socks5.ATYPIPv6 { | ||
host = net.IP(dstAddr).To16().String() | ||
} else if atyp == socks5.ATYPDomain { | ||
// Need to strip the first byte which is the domain length. | ||
host = string(dstAddr[1:]) | ||
} | ||
// Port | ||
port := binary.BigEndian.Uint16(dstPort) | ||
return &AddrEx{ | ||
Host: host, | ||
Port: port, | ||
} | ||
} |