sing/protocol/socks/handshake.go
2024-12-10 19:53:57 +08:00

233 lines
6.9 KiB
Go

package socks
import (
std_bufio "bufio"
"context"
"io"
"net"
"net/netip"
"os"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/auth"
"github.com/sagernet/sing/common/bufio"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/varbin"
"github.com/sagernet/sing/protocol/socks/socks4"
"github.com/sagernet/sing/protocol/socks/socks5"
)
type HandlerEx interface {
N.TCPConnectionHandlerEx
N.UDPConnectionHandlerEx
}
func ClientHandshake4(conn io.ReadWriter, command byte, destination M.Socksaddr, username string) (socks4.Response, error) {
err := socks4.WriteRequest(conn, socks4.Request{
Command: command,
Destination: destination,
Username: username,
})
if err != nil {
return socks4.Response{}, err
}
response, err := socks4.ReadResponse(varbin.StubReader(conn))
if err != nil {
return socks4.Response{}, err
}
if response.ReplyCode != socks4.ReplyCodeGranted {
err = E.New("socks4: request rejected, code= ", response.ReplyCode)
}
return response, err
}
func ClientHandshake5(conn io.ReadWriter, command byte, destination M.Socksaddr, username string, password string) (socks5.Response, error) {
reader := varbin.StubReader(conn)
var method byte
if username == "" {
method = socks5.AuthTypeNotRequired
} else {
method = socks5.AuthTypeUsernamePassword
}
err := socks5.WriteAuthRequest(conn, socks5.AuthRequest{
Methods: []byte{method},
})
if err != nil {
return socks5.Response{}, err
}
authResponse, err := socks5.ReadAuthResponse(reader)
if err != nil {
return socks5.Response{}, err
}
if authResponse.Method == socks5.AuthTypeUsernamePassword {
err = socks5.WriteUsernamePasswordAuthRequest(conn, socks5.UsernamePasswordAuthRequest{
Username: username,
Password: password,
})
if err != nil {
return socks5.Response{}, err
}
usernamePasswordResponse, err := socks5.ReadUsernamePasswordAuthResponse(reader)
if err != nil {
return socks5.Response{}, err
}
if usernamePasswordResponse.Status != socks5.UsernamePasswordStatusSuccess {
return socks5.Response{}, E.New("socks5: incorrect user name or password")
}
} else if authResponse.Method != socks5.AuthTypeNotRequired {
return socks5.Response{}, E.New("socks5: unsupported auth method: ", authResponse.Method)
}
if command == socks5.CommandUDPAssociate {
if destination.Addr.IsPrivate() {
if destination.Addr.Is6() {
destination.Addr = netip.AddrFrom4([4]byte{127, 0, 0, 1})
} else {
destination.Addr = netip.IPv6Loopback()
}
} else if destination.Addr.IsGlobalUnicast() {
if destination.Addr.Is6() {
destination.Addr = netip.IPv6Unspecified()
} else {
destination.Addr = netip.IPv4Unspecified()
}
} else {
destination.Addr = netip.IPv6Unspecified()
}
destination.Port = 0
}
err = socks5.WriteRequest(conn, socks5.Request{
Command: command,
Destination: destination,
})
if err != nil {
return socks5.Response{}, err
}
response, err := socks5.ReadResponse(reader)
if err != nil {
return socks5.Response{}, err
}
if response.ReplyCode != socks5.ReplyCodeSuccess {
err = E.New("socks5: request rejected, code=", response.ReplyCode)
}
return response, err
}
func HandleConnectionEx(
ctx context.Context, conn net.Conn, reader *std_bufio.Reader,
authenticator *auth.Authenticator,
handler HandlerEx,
source M.Socksaddr,
onClose N.CloseHandlerFunc,
) error {
version, err := reader.ReadByte()
if err != nil {
return err
}
switch version {
case socks4.Version:
var request socks4.Request
request, err = socks4.ReadRequest0(reader)
if err != nil {
return err
}
switch request.Command {
case socks4.CommandConnect:
if authenticator != nil && !authenticator.Verify(request.Username, "") {
err = socks4.WriteResponse(conn, socks4.Response{
ReplyCode: socks4.ReplyCodeRejectedOrFailed,
})
if err != nil {
return err
}
return E.New("socks4: authentication failed, username=", request.Username)
}
handler.NewConnectionEx(auth.ContextWithUser(ctx, request.Username), NewLazyConn(conn, version), source, request.Destination, onClose)
return nil
default:
err = socks4.WriteResponse(conn, socks4.Response{
ReplyCode: socks4.ReplyCodeRejectedOrFailed,
})
if err != nil {
return err
}
return E.New("socks4: unsupported command ", request.Command)
}
case socks5.Version:
var authRequest socks5.AuthRequest
authRequest, err = socks5.ReadAuthRequest0(reader)
if err != nil {
return err
}
var authMethod byte
if authenticator != nil && !common.Contains(authRequest.Methods, socks5.AuthTypeUsernamePassword) {
err = socks5.WriteAuthResponse(conn, socks5.AuthResponse{
Method: socks5.AuthTypeNoAcceptedMethods,
})
if err != nil {
return err
}
}
if authenticator != nil {
authMethod = socks5.AuthTypeUsernamePassword
} else {
authMethod = socks5.AuthTypeNotRequired
}
err = socks5.WriteAuthResponse(conn, socks5.AuthResponse{
Method: authMethod,
})
if err != nil {
return err
}
if authMethod == socks5.AuthTypeUsernamePassword {
var usernamePasswordAuthRequest socks5.UsernamePasswordAuthRequest
usernamePasswordAuthRequest, err = socks5.ReadUsernamePasswordAuthRequest(reader)
if err != nil {
return err
}
ctx = auth.ContextWithUser(ctx, usernamePasswordAuthRequest.Username)
response := socks5.UsernamePasswordAuthResponse{}
if authenticator.Verify(usernamePasswordAuthRequest.Username, usernamePasswordAuthRequest.Password) {
response.Status = socks5.UsernamePasswordStatusSuccess
} else {
response.Status = socks5.UsernamePasswordStatusFailure
}
err = socks5.WriteUsernamePasswordAuthResponse(conn, response)
if err != nil {
return err
}
if response.Status != socks5.UsernamePasswordStatusSuccess {
return E.New("socks5: authentication failed, username=", usernamePasswordAuthRequest.Username, ", password=", usernamePasswordAuthRequest.Password)
}
}
var request socks5.Request
request, err = socks5.ReadRequest(reader)
if err != nil {
return err
}
switch request.Command {
case socks5.CommandConnect:
handler.NewConnectionEx(ctx, NewLazyConn(conn, version), source, request.Destination, onClose)
return nil
case socks5.CommandUDPAssociate:
var udpConn *net.UDPConn
udpConn, err = net.ListenUDP(M.NetworkFromNetAddr("udp", M.AddrFromNet(conn.LocalAddr())), net.UDPAddrFromAddrPort(netip.AddrPortFrom(M.AddrFromNet(conn.LocalAddr()), 0)))
if err != nil {
return err
}
handler.NewPacketConnectionEx(ctx, NewLazyAssociatePacketConn(bufio.NewServerPacketConn(udpConn), conn), source, M.Socksaddr{}, onClose)
default:
err = socks5.WriteResponse(conn, socks5.Response{
ReplyCode: socks5.ReplyCodeUnsupported,
})
if err != nil {
return err
}
return E.New("socks5: unsupported command ", request.Command)
}
}
return os.ErrInvalid
}