forked from doreamon-design/clash
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feature: SOCKS4/SOCKS4A Inbound Compatible Support (#1491)
- Loading branch information
Showing
5 changed files
with
241 additions
and
11 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
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,195 @@ | ||
package socks4 | ||
|
||
import ( | ||
"bytes" | ||
"encoding/binary" | ||
"errors" | ||
"io" | ||
"net" | ||
"strconv" | ||
|
||
"github.com/Dreamacro/clash/component/auth" | ||
) | ||
|
||
const Version = 0x04 | ||
|
||
type Command = uint8 | ||
|
||
const ( | ||
CmdConnect Command = 0x01 | ||
CmdBind Command = 0x02 | ||
) | ||
|
||
type Code = uint8 | ||
|
||
const ( | ||
RequestGranted Code = 90 | ||
RequestRejected Code = 91 | ||
RequestIdentdFailed Code = 92 | ||
RequestIdentdMismatched Code = 93 | ||
) | ||
|
||
var ( | ||
errVersionMismatched = errors.New("version code mismatched") | ||
errCommandNotSupported = errors.New("command not supported") | ||
errIPv6NotSupported = errors.New("IPv6 not supported") | ||
|
||
ErrRequestRejected = errors.New("request rejected or failed") | ||
ErrRequestIdentdFailed = errors.New("request rejected because SOCKS server cannot connect to identd on the client") | ||
ErrRequestIdentdMismatched = errors.New("request rejected because the client program and identd report different user-ids") | ||
ErrRequestUnknownCode = errors.New("request failed with unknown code") | ||
) | ||
|
||
func ServerHandshake(rw io.ReadWriter, authenticator auth.Authenticator) (addr string, command Command, err error) { | ||
var req [8]byte | ||
if _, err = io.ReadFull(rw, req[:]); err != nil { | ||
return | ||
} | ||
|
||
if req[0] != Version { | ||
err = errVersionMismatched | ||
return | ||
} | ||
|
||
if command = req[1]; command != CmdConnect { | ||
err = errCommandNotSupported | ||
return | ||
} | ||
|
||
var ( | ||
dstIP = req[4:8] // [4]byte | ||
dstPort = req[2:4] // [2]byte | ||
) | ||
|
||
var ( | ||
host string | ||
port string | ||
code uint8 | ||
userID []byte | ||
) | ||
if userID, err = readUntilNull(rw); err != nil { | ||
return | ||
} | ||
|
||
if isReservedIP(dstIP) { | ||
var target []byte | ||
if target, err = readUntilNull(rw); err != nil { | ||
return | ||
} | ||
host = string(target) | ||
} | ||
|
||
port = strconv.Itoa(int(binary.BigEndian.Uint16(dstPort))) | ||
if host != "" { | ||
addr = net.JoinHostPort(host, port) | ||
} else { | ||
addr = net.JoinHostPort(net.IP(dstIP).String(), port) | ||
} | ||
|
||
// SOCKS4 only support USERID auth. | ||
if authenticator == nil || authenticator.Verify(string(userID), "") { | ||
code = RequestGranted | ||
} else { | ||
code = RequestIdentdMismatched | ||
} | ||
|
||
var reply [8]byte | ||
reply[0] = 0x00 // reply code | ||
reply[1] = code // result code | ||
copy(reply[4:8], dstIP) | ||
copy(reply[2:4], dstPort) | ||
|
||
_, err = rw.Write(reply[:]) | ||
return | ||
} | ||
|
||
func ClientHandshake(rw io.ReadWriter, addr string, command Command, userID string) (err error) { | ||
host, portStr, err := net.SplitHostPort(addr) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
port, err := strconv.ParseUint(portStr, 10, 16) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
ip := net.ParseIP(host) | ||
if ip == nil /* HOST */ { | ||
ip = net.IPv4(0, 0, 0, 1).To4() | ||
} else if ip.To4() == nil /* IPv6 */ { | ||
return errIPv6NotSupported | ||
} | ||
|
||
dstIP := ip.To4() | ||
|
||
req := &bytes.Buffer{} | ||
req.WriteByte(Version) | ||
req.WriteByte(command) | ||
binary.Write(req, binary.BigEndian, uint16(port)) | ||
req.Write(dstIP) | ||
req.WriteString(userID) | ||
req.WriteByte(0) /* NULL */ | ||
|
||
if isReservedIP(dstIP) /* SOCKS4A */ { | ||
req.WriteString(host) | ||
req.WriteByte(0) /* NULL */ | ||
} | ||
|
||
if _, err = rw.Write(req.Bytes()); err != nil { | ||
return err | ||
} | ||
|
||
var resp [8]byte | ||
if _, err = io.ReadFull(rw, resp[:]); err != nil { | ||
return err | ||
} | ||
|
||
if resp[0] != 0x00 { | ||
return errVersionMismatched | ||
} | ||
|
||
switch resp[1] { | ||
case RequestGranted: | ||
return nil | ||
case RequestRejected: | ||
return ErrRequestRejected | ||
case RequestIdentdFailed: | ||
return ErrRequestIdentdFailed | ||
case RequestIdentdMismatched: | ||
return ErrRequestIdentdMismatched | ||
default: | ||
return ErrRequestUnknownCode | ||
} | ||
} | ||
|
||
// For version 4A, if the client cannot resolve the destination host's | ||
// domain name to find its IP address, it should set the first three bytes | ||
// of DSTIP to NULL and the last byte to a non-zero value. (This corresponds | ||
// to IP address 0.0.0.x, with x nonzero. As decreed by IANA -- The | ||
// Internet Assigned Numbers Authority -- such an address is inadmissible | ||
// as a destination IP address and thus should never occur if the client | ||
// can resolve the domain name.) | ||
func isReservedIP(ip net.IP) bool { | ||
subnet := net.IPNet{ | ||
IP: net.IPv4zero, | ||
Mask: net.IPv4Mask(0xff, 0xff, 0xff, 0x00), | ||
} | ||
|
||
return !ip.IsUnspecified() && subnet.Contains(ip) | ||
} | ||
|
||
func readUntilNull(r io.Reader) ([]byte, error) { | ||
var buf = &bytes.Buffer{} | ||
var data [1]byte | ||
|
||
for { | ||
if _, err := r.Read(data[:]); err != nil { | ||
return nil, err | ||
} | ||
if data[0] == 0 { | ||
return buf.Bytes(), nil | ||
} | ||
buf.WriteByte(data[0]) | ||
} | ||
} |