initial commit

This commit is contained in:
ginuerzh
2022-03-16 19:40:29 +08:00
commit 7db81fcfeb
109 changed files with 8782 additions and 0 deletions

115
handler/auto/handler.go Normal file
View File

@ -0,0 +1,115 @@
package auto
import (
"bufio"
"context"
"net"
"time"
netpkg "github.com/go-gost/core/common/net"
"github.com/go-gost/core/handler"
md "github.com/go-gost/core/metadata"
"github.com/go-gost/core/registry"
"github.com/go-gost/gosocks4"
"github.com/go-gost/gosocks5"
)
func init() {
registry.HandlerRegistry().Register("auto", NewHandler)
}
type autoHandler struct {
httpHandler handler.Handler
socks4Handler handler.Handler
socks5Handler handler.Handler
options handler.Options
}
func NewHandler(opts ...handler.Option) handler.Handler {
options := handler.Options{}
for _, opt := range opts {
opt(&options)
}
h := &autoHandler{
options: options,
}
if f := registry.HandlerRegistry().Get("http"); f != nil {
v := append(opts,
handler.LoggerOption(options.Logger.WithFields(map[string]any{"type": "http"})))
h.httpHandler = f(v...)
}
if f := registry.HandlerRegistry().Get("socks4"); f != nil {
v := append(opts,
handler.LoggerOption(options.Logger.WithFields(map[string]any{"type": "socks4"})))
h.socks4Handler = f(v...)
}
if f := registry.HandlerRegistry().Get("socks5"); f != nil {
v := append(opts,
handler.LoggerOption(options.Logger.WithFields(map[string]any{"type": "socks5"})))
h.socks5Handler = f(v...)
}
return h
}
func (h *autoHandler) Init(md md.Metadata) error {
if h.httpHandler != nil {
if err := h.httpHandler.Init(md); err != nil {
return err
}
}
if h.socks4Handler != nil {
if err := h.socks4Handler.Init(md); err != nil {
return err
}
}
if h.socks5Handler != nil {
if err := h.socks5Handler.Init(md); err != nil {
return err
}
}
return nil
}
func (h *autoHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler.HandleOption) error {
log := h.options.Logger.WithFields(map[string]any{
"remote": conn.RemoteAddr().String(),
"local": conn.LocalAddr().String(),
})
start := time.Now()
log.Infof("%s <> %s", conn.RemoteAddr(), conn.LocalAddr())
defer func() {
log.WithFields(map[string]any{
"duration": time.Since(start),
}).Infof("%s >< %s", conn.RemoteAddr(), conn.LocalAddr())
}()
br := bufio.NewReader(conn)
b, err := br.Peek(1)
if err != nil {
log.Error(err)
conn.Close()
return err
}
conn = netpkg.NewBufferReaderConn(conn, br)
switch b[0] {
case gosocks4.Ver4: // socks4
if h.socks4Handler != nil {
return h.socks4Handler.Handle(ctx, conn)
}
case gosocks5.Ver5: // socks5
if h.socks5Handler != nil {
return h.socks5Handler.Handle(ctx, conn)
}
default: // http
if h.httpHandler != nil {
return h.httpHandler.Handle(ctx, conn)
}
}
return nil
}

View File

@ -0,0 +1,117 @@
package local
import (
"context"
"errors"
"fmt"
"net"
"time"
"github.com/go-gost/core/chain"
netpkg "github.com/go-gost/core/common/net"
"github.com/go-gost/core/handler"
md "github.com/go-gost/core/metadata"
"github.com/go-gost/core/registry"
)
func init() {
registry.HandlerRegistry().Register("tcp", NewHandler)
registry.HandlerRegistry().Register("udp", NewHandler)
registry.HandlerRegistry().Register("forward", NewHandler)
}
type forwardHandler struct {
group *chain.NodeGroup
router *chain.Router
md metadata
options handler.Options
}
func NewHandler(opts ...handler.Option) handler.Handler {
options := handler.Options{}
for _, opt := range opts {
opt(&options)
}
return &forwardHandler{
options: options,
}
}
func (h *forwardHandler) Init(md md.Metadata) (err error) {
if err = h.parseMetadata(md); err != nil {
return
}
if h.group == nil {
// dummy node used by relay connector.
h.group = chain.NewNodeGroup(&chain.Node{Name: "dummy", Addr: ":0"})
}
h.router = h.options.Router
if h.router == nil {
h.router = (&chain.Router{}).WithLogger(h.options.Logger)
}
return
}
// Forward implements handler.Forwarder.
func (h *forwardHandler) Forward(group *chain.NodeGroup) {
h.group = group
}
func (h *forwardHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler.HandleOption) error {
defer conn.Close()
start := time.Now()
log := h.options.Logger.WithFields(map[string]any{
"remote": conn.RemoteAddr().String(),
"local": conn.LocalAddr().String(),
})
log.Infof("%s <> %s", conn.RemoteAddr(), conn.LocalAddr())
defer func() {
log.WithFields(map[string]any{
"duration": time.Since(start),
}).Infof("%s >< %s", conn.RemoteAddr(), conn.LocalAddr())
}()
target := h.group.Next()
if target == nil {
err := errors.New("target not available")
log.Error(err)
return err
}
network := "tcp"
if _, ok := conn.(net.PacketConn); ok {
network = "udp"
}
log = log.WithFields(map[string]any{
"dst": fmt.Sprintf("%s/%s", target.Addr, network),
})
log.Infof("%s >> %s", conn.RemoteAddr(), target.Addr)
cc, err := h.router.Dial(ctx, network, target.Addr)
if err != nil {
log.Error(err)
// TODO: the router itself may be failed due to the failed node in the router,
// the dead marker may be a wrong operation.
target.Marker.Mark()
return err
}
defer cc.Close()
target.Marker.Reset()
t := time.Now()
log.Infof("%s <-> %s", conn.RemoteAddr(), target.Addr)
netpkg.Transport(conn, cc)
log.WithFields(map[string]any{
"duration": time.Since(t),
}).Infof("%s >-< %s", conn.RemoteAddr(), target.Addr)
return nil
}

View File

@ -0,0 +1,20 @@
package local
import (
"time"
mdata "github.com/go-gost/core/metadata"
)
type metadata struct {
readTimeout time.Duration
}
func (h *forwardHandler) parseMetadata(md mdata.Metadata) (err error) {
const (
readTimeout = "readTimeout"
)
h.md.readTimeout = mdata.GetDuration(md, readTimeout)
return
}

View File

@ -0,0 +1,111 @@
package remote
import (
"context"
"errors"
"fmt"
"net"
"time"
"github.com/go-gost/core/chain"
netpkg "github.com/go-gost/core/common/net"
"github.com/go-gost/core/handler"
md "github.com/go-gost/core/metadata"
"github.com/go-gost/core/registry"
)
func init() {
registry.HandlerRegistry().Register("rtcp", NewHandler)
registry.HandlerRegistry().Register("rudp", NewHandler)
}
type forwardHandler struct {
group *chain.NodeGroup
router *chain.Router
md metadata
options handler.Options
}
func NewHandler(opts ...handler.Option) handler.Handler {
options := handler.Options{}
for _, opt := range opts {
opt(&options)
}
return &forwardHandler{
options: options,
}
}
func (h *forwardHandler) Init(md md.Metadata) (err error) {
if err = h.parseMetadata(md); err != nil {
return
}
h.router = h.options.Router
if h.router == nil {
h.router = (&chain.Router{}).WithLogger(h.options.Logger)
}
return
}
// Forward implements handler.Forwarder.
func (h *forwardHandler) Forward(group *chain.NodeGroup) {
h.group = group
}
func (h *forwardHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler.HandleOption) error {
defer conn.Close()
start := time.Now()
log := h.options.Logger.WithFields(map[string]any{
"remote": conn.RemoteAddr().String(),
"local": conn.LocalAddr().String(),
})
log.Infof("%s <> %s", conn.RemoteAddr(), conn.LocalAddr())
defer func() {
log.WithFields(map[string]any{
"duration": time.Since(start),
}).Infof("%s >< %s", conn.RemoteAddr(), conn.LocalAddr())
}()
target := h.group.Next()
if target == nil {
err := errors.New("target not available")
log.Error(err)
return err
}
network := "tcp"
if _, ok := conn.(net.PacketConn); ok {
network = "udp"
}
log = log.WithFields(map[string]any{
"dst": fmt.Sprintf("%s/%s", target.Addr, network),
})
log.Infof("%s >> %s", conn.RemoteAddr(), target.Addr)
cc, err := h.router.Dial(ctx, network, target.Addr)
if err != nil {
log.Error(err)
// TODO: the router itself may be failed due to the failed node in the router,
// the dead marker may be a wrong operation.
target.Marker.Mark()
return err
}
defer cc.Close()
target.Marker.Reset()
t := time.Now()
log.Infof("%s <-> %s", conn.RemoteAddr(), target.Addr)
netpkg.Transport(conn, cc)
log.WithFields(map[string]any{
"duration": time.Since(t),
}).Infof("%s >-< %s", conn.RemoteAddr(), target.Addr)
return nil
}

View File

@ -0,0 +1,20 @@
package remote
import (
"time"
mdata "github.com/go-gost/core/metadata"
)
type metadata struct {
readTimeout time.Duration
}
func (h *forwardHandler) parseMetadata(md mdata.Metadata) (err error) {
const (
readTimeout = "readTimeout"
)
h.md.readTimeout = mdata.GetDuration(md, readTimeout)
return
}

18
handler/handler.go Normal file
View File

@ -0,0 +1,18 @@
package handler
import (
"context"
"net"
"github.com/go-gost/core/chain"
"github.com/go-gost/core/metadata"
)
type Handler interface {
Init(metadata.Metadata) error
Handle(context.Context, net.Conn, ...HandleOption) error
}
type Forwarder interface {
Forward(*chain.NodeGroup)
}

337
handler/http/handler.go Normal file
View File

@ -0,0 +1,337 @@
package http
import (
"bufio"
"context"
"encoding/base64"
"encoding/binary"
"errors"
"hash/crc32"
"net"
"net/http"
"net/http/httputil"
"os"
"strconv"
"strings"
"time"
"github.com/asaskevich/govalidator"
"github.com/go-gost/core/chain"
netpkg "github.com/go-gost/core/common/net"
"github.com/go-gost/core/handler"
"github.com/go-gost/core/logger"
md "github.com/go-gost/core/metadata"
"github.com/go-gost/core/registry"
)
func init() {
registry.HandlerRegistry().Register("http", NewHandler)
}
type httpHandler struct {
router *chain.Router
md metadata
options handler.Options
}
func NewHandler(opts ...handler.Option) handler.Handler {
options := handler.Options{}
for _, opt := range opts {
opt(&options)
}
return &httpHandler{
options: options,
}
}
func (h *httpHandler) Init(md md.Metadata) error {
if err := h.parseMetadata(md); err != nil {
return err
}
h.router = h.options.Router
if h.router == nil {
h.router = (&chain.Router{}).WithLogger(h.options.Logger)
}
return nil
}
func (h *httpHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler.HandleOption) error {
defer conn.Close()
start := time.Now()
log := h.options.Logger.WithFields(map[string]any{
"remote": conn.RemoteAddr().String(),
"local": conn.LocalAddr().String(),
})
log.Infof("%s <> %s", conn.RemoteAddr(), conn.LocalAddr())
defer func() {
log.WithFields(map[string]any{
"duration": time.Since(start),
}).Infof("%s >< %s", conn.RemoteAddr(), conn.LocalAddr())
}()
req, err := http.ReadRequest(bufio.NewReader(conn))
if err != nil {
log.Error(err)
return err
}
defer req.Body.Close()
return h.handleRequest(ctx, conn, req, log)
}
func (h *httpHandler) handleRequest(ctx context.Context, conn net.Conn, req *http.Request, log logger.Logger) error {
if h.md.sni && !req.URL.IsAbs() && govalidator.IsDNSName(req.Host) {
req.URL.Scheme = "http"
}
network := req.Header.Get("X-Gost-Protocol")
if network != "udp" {
network = "tcp"
}
// Try to get the actual host.
// Compatible with GOST 2.x.
if v := req.Header.Get("Gost-Target"); v != "" {
if h, err := h.decodeServerName(v); err == nil {
req.Host = h
}
}
req.Header.Del("Gost-Target")
if v := req.Header.Get("X-Gost-Target"); v != "" {
if h, err := h.decodeServerName(v); err == nil {
req.Host = h
}
}
req.Header.Del("X-Gost-Target")
addr := req.Host
if _, port, _ := net.SplitHostPort(addr); port == "" {
addr = net.JoinHostPort(addr, "80")
}
fields := map[string]any{
"dst": addr,
}
if u, _, _ := h.basicProxyAuth(req.Header.Get("Proxy-Authorization"), log); u != "" {
fields["user"] = u
}
log = log.WithFields(fields)
if log.IsLevelEnabled(logger.DebugLevel) {
dump, _ := httputil.DumpRequest(req, false)
log.Debug(string(dump))
}
log.Infof("%s >> %s", conn.RemoteAddr(), addr)
resp := &http.Response{
ProtoMajor: 1,
ProtoMinor: 1,
Header: h.md.header,
}
if resp.Header == nil {
resp.Header = http.Header{}
}
if h.options.Bypass != nil && h.options.Bypass.Contains(addr) {
resp.StatusCode = http.StatusForbidden
if log.IsLevelEnabled(logger.DebugLevel) {
dump, _ := httputil.DumpResponse(resp, false)
log.Debug(string(dump))
}
log.Info("bypass: ", addr)
return resp.Write(conn)
}
if !h.authenticate(conn, req, resp, log) {
return nil
}
if network == "udp" {
return h.handleUDP(ctx, conn, network, req.Host, log)
}
if req.Method == "PRI" ||
(req.Method != http.MethodConnect && req.URL.Scheme != "http") {
resp.StatusCode = http.StatusBadRequest
if log.IsLevelEnabled(logger.DebugLevel) {
dump, _ := httputil.DumpResponse(resp, false)
log.Debug(string(dump))
}
return resp.Write(conn)
}
req.Header.Del("Proxy-Authorization")
cc, err := h.router.Dial(ctx, network, addr)
if err != nil {
resp.StatusCode = http.StatusServiceUnavailable
if log.IsLevelEnabled(logger.DebugLevel) {
dump, _ := httputil.DumpResponse(resp, false)
log.Debug(string(dump))
}
resp.Write(conn)
return err
}
defer cc.Close()
if req.Method == http.MethodConnect {
resp.StatusCode = http.StatusOK
resp.Status = "200 Connection established"
if log.IsLevelEnabled(logger.DebugLevel) {
dump, _ := httputil.DumpResponse(resp, false)
log.Debug(string(dump))
}
if err = resp.Write(conn); err != nil {
log.Error(err)
return err
}
} else {
req.Header.Del("Proxy-Connection")
if err = req.Write(cc); err != nil {
log.Error(err)
return err
}
}
start := time.Now()
log.Infof("%s <-> %s", conn.RemoteAddr(), addr)
netpkg.Transport(conn, cc)
log.WithFields(map[string]any{
"duration": time.Since(start),
}).Infof("%s >-< %s", conn.RemoteAddr(), addr)
return nil
}
func (h *httpHandler) decodeServerName(s string) (string, error) {
b, err := base64.RawURLEncoding.DecodeString(s)
if err != nil {
return "", err
}
if len(b) < 4 {
return "", errors.New("invalid name")
}
v, err := base64.RawURLEncoding.DecodeString(string(b[4:]))
if err != nil {
return "", err
}
if crc32.ChecksumIEEE(v) != binary.BigEndian.Uint32(b[:4]) {
return "", errors.New("invalid name")
}
return string(v), nil
}
func (h *httpHandler) basicProxyAuth(proxyAuth string, log logger.Logger) (username, password string, ok bool) {
if proxyAuth == "" {
return
}
if !strings.HasPrefix(proxyAuth, "Basic ") {
return
}
c, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(proxyAuth, "Basic "))
if err != nil {
return
}
cs := string(c)
s := strings.IndexByte(cs, ':')
if s < 0 {
return
}
return cs[:s], cs[s+1:], true
}
func (h *httpHandler) authenticate(conn net.Conn, req *http.Request, resp *http.Response, log logger.Logger) (ok bool) {
u, p, _ := h.basicProxyAuth(req.Header.Get("Proxy-Authorization"), log)
if h.options.Auther == nil || h.options.Auther.Authenticate(u, p) {
return true
}
pr := h.md.probeResistance
// probing resistance is enabled, and knocking host is mismatch.
if pr != nil && (pr.Knock == "" || !strings.EqualFold(req.URL.Hostname(), pr.Knock)) {
resp.StatusCode = http.StatusServiceUnavailable // default status code
switch pr.Type {
case "code":
resp.StatusCode, _ = strconv.Atoi(pr.Value)
case "web":
url := pr.Value
if !strings.HasPrefix(url, "http") {
url = "http://" + url
}
r, err := http.Get(url)
if err != nil {
log.Error(err)
break
}
resp = r
defer resp.Body.Close()
case "host":
cc, err := net.Dial("tcp", pr.Value)
if err != nil {
log.Error(err)
break
}
defer cc.Close()
req.Write(cc)
netpkg.Transport(conn, cc)
return
case "file":
f, _ := os.Open(pr.Value)
if f != nil {
defer f.Close()
resp.StatusCode = http.StatusOK
if finfo, _ := f.Stat(); finfo != nil {
resp.ContentLength = finfo.Size()
}
resp.Header.Set("Content-Type", "text/html")
resp.Body = f
}
}
}
if resp.Header == nil {
resp.Header = http.Header{}
}
if resp.StatusCode == 0 {
resp.StatusCode = http.StatusProxyAuthRequired
resp.Header.Add("Proxy-Authenticate", "Basic realm=\"gost\"")
if strings.ToLower(req.Header.Get("Proxy-Connection")) == "keep-alive" {
// XXX libcurl will keep sending auth request in same conn
// which we don't supported yet.
resp.Header.Add("Connection", "close")
resp.Header.Add("Proxy-Connection", "close")
}
log.Info("proxy authentication required")
} else {
resp.Header.Set("Server", "nginx/1.20.1")
resp.Header.Set("Date", time.Now().Format(http.TimeFormat))
if resp.StatusCode == http.StatusOK {
resp.Header.Set("Connection", "keep-alive")
}
}
if log.IsLevelEnabled(logger.DebugLevel) {
dump, _ := httputil.DumpResponse(resp, false)
log.Debug(string(dump))
}
resp.Write(conn)
return
}

53
handler/http/metadata.go Normal file
View File

@ -0,0 +1,53 @@
package http
import (
"net/http"
"strings"
mdata "github.com/go-gost/core/metadata"
)
type metadata struct {
probeResistance *probeResistance
sni bool
enableUDP bool
header http.Header
}
func (h *httpHandler) parseMetadata(md mdata.Metadata) error {
const (
header = "header"
probeResistKey = "probeResistance"
knock = "knock"
sni = "sni"
enableUDP = "udp"
)
if m := mdata.GetStringMapString(md, header); len(m) > 0 {
hd := http.Header{}
for k, v := range m {
hd.Add(k, v)
}
h.md.header = hd
}
if v := mdata.GetString(md, probeResistKey); v != "" {
if ss := strings.SplitN(v, ":", 2); len(ss) == 2 {
h.md.probeResistance = &probeResistance{
Type: ss[0],
Value: ss[1],
Knock: mdata.GetString(md, knock),
}
}
}
h.md.sni = mdata.GetBool(md, sni)
h.md.enableUDP = mdata.GetBool(md, enableUDP)
return nil
}
type probeResistance struct {
Type string
Value string
Knock string
}

80
handler/http/udp.go Normal file
View File

@ -0,0 +1,80 @@
package http
import (
"context"
"errors"
"net"
"net/http"
"net/http/httputil"
"time"
"github.com/go-gost/core/common/net/relay"
"github.com/go-gost/core/internal/util/socks"
"github.com/go-gost/core/logger"
)
func (h *httpHandler) handleUDP(ctx context.Context, conn net.Conn, network, address string, log logger.Logger) error {
log = log.WithFields(map[string]any{
"cmd": "udp",
})
resp := &http.Response{
ProtoMajor: 1,
ProtoMinor: 1,
Header: h.md.header,
}
if resp.Header == nil {
resp.Header = http.Header{}
}
if !h.md.enableUDP {
resp.StatusCode = http.StatusForbidden
if log.IsLevelEnabled(logger.DebugLevel) {
dump, _ := httputil.DumpResponse(resp, false)
log.Debug(string(dump))
}
log.Error("http: UDP relay is disabled")
return resp.Write(conn)
}
resp.StatusCode = http.StatusOK
if log.IsLevelEnabled(logger.DebugLevel) {
dump, _ := httputil.DumpResponse(resp, false)
log.Debug(string(dump))
}
if err := resp.Write(conn); err != nil {
log.Error(err)
return err
}
// obtain a udp connection
c, err := h.router.Dial(ctx, "udp", "") // UDP association
if err != nil {
log.Error(err)
return err
}
defer c.Close()
pc, ok := c.(net.PacketConn)
if !ok {
err = errors.New("wrong connection type")
log.Error(err)
return err
}
relay := relay.NewUDPRelay(socks.UDPTunServerConn(conn), pc).
WithBypass(h.options.Bypass).
WithLogger(log)
t := time.Now()
log.Infof("%s <-> %s", conn.RemoteAddr(), pc.LocalAddr())
relay.Run()
log.WithFields(map[string]any{
"duration": time.Since(t),
}).Infof("%s >-< %s", conn.RemoteAddr(), pc.LocalAddr())
return nil
}

71
handler/option.go Normal file
View File

@ -0,0 +1,71 @@
package handler
import (
"crypto/tls"
"net/url"
"github.com/go-gost/core/auth"
"github.com/go-gost/core/bypass"
"github.com/go-gost/core/chain"
"github.com/go-gost/core/logger"
"github.com/go-gost/core/metadata"
)
type Options struct {
Bypass bypass.Bypass
Router *chain.Router
Auth *url.Userinfo
Auther auth.Authenticator
TLSConfig *tls.Config
Logger logger.Logger
}
type Option func(opts *Options)
func BypassOption(bypass bypass.Bypass) Option {
return func(opts *Options) {
opts.Bypass = bypass
}
}
func RouterOption(router *chain.Router) Option {
return func(opts *Options) {
opts.Router = router
}
}
func AuthOption(auth *url.Userinfo) Option {
return func(opts *Options) {
opts.Auth = auth
}
}
func AutherOption(auther auth.Authenticator) Option {
return func(opts *Options) {
opts.Auther = auther
}
}
func TLSConfigOption(tlsConfig *tls.Config) Option {
return func(opts *Options) {
opts.TLSConfig = tlsConfig
}
}
func LoggerOption(logger logger.Logger) Option {
return func(opts *Options) {
opts.Logger = logger
}
}
type HandleOptions struct {
Metadata metadata.Metadata
}
type HandleOption func(opts *HandleOptions)
func MetadataHandleOption(md metadata.Metadata) HandleOption {
return func(opts *HandleOptions) {
opts.Metadata = md
}
}

152
handler/socks/v4/handler.go Normal file
View File

@ -0,0 +1,152 @@
package v4
import (
"context"
"errors"
"net"
"time"
"github.com/go-gost/core/chain"
netpkg "github.com/go-gost/core/common/net"
"github.com/go-gost/core/handler"
"github.com/go-gost/core/logger"
md "github.com/go-gost/core/metadata"
"github.com/go-gost/core/registry"
"github.com/go-gost/gosocks4"
)
var (
ErrUnknownCmd = errors.New("socks4: unknown command")
ErrUnimplemented = errors.New("socks4: unimplemented")
)
func init() {
registry.HandlerRegistry().Register("socks4", NewHandler)
registry.HandlerRegistry().Register("socks4a", NewHandler)
}
type socks4Handler struct {
router *chain.Router
md metadata
options handler.Options
}
func NewHandler(opts ...handler.Option) handler.Handler {
options := handler.Options{}
for _, opt := range opts {
opt(&options)
}
return &socks4Handler{
options: options,
}
}
func (h *socks4Handler) Init(md md.Metadata) (err error) {
if err := h.parseMetadata(md); err != nil {
return err
}
h.router = h.options.Router
if h.router == nil {
h.router = (&chain.Router{}).WithLogger(h.options.Logger)
}
return nil
}
func (h *socks4Handler) Handle(ctx context.Context, conn net.Conn, opts ...handler.HandleOption) error {
defer conn.Close()
start := time.Now()
log := h.options.Logger.WithFields(map[string]any{
"remote": conn.RemoteAddr().String(),
"local": conn.LocalAddr().String(),
})
log.Infof("%s <> %s", conn.RemoteAddr(), conn.LocalAddr())
defer func() {
log.WithFields(map[string]any{
"duration": time.Since(start),
}).Infof("%s >< %s", conn.RemoteAddr(), conn.LocalAddr())
}()
if h.md.readTimeout > 0 {
conn.SetReadDeadline(time.Now().Add(h.md.readTimeout))
}
req, err := gosocks4.ReadRequest(conn)
if err != nil {
log.Error(err)
return err
}
log.Debug(req)
conn.SetReadDeadline(time.Time{})
if h.options.Auther != nil &&
!h.options.Auther.Authenticate(string(req.Userid), "") {
resp := gosocks4.NewReply(gosocks4.RejectedUserid, nil)
log.Debug(resp)
return resp.Write(conn)
}
switch req.Cmd {
case gosocks4.CmdConnect:
return h.handleConnect(ctx, conn, req, log)
case gosocks4.CmdBind:
return h.handleBind(ctx, conn, req)
default:
err = ErrUnknownCmd
log.Error(err)
return err
}
}
func (h *socks4Handler) handleConnect(ctx context.Context, conn net.Conn, req *gosocks4.Request, log logger.Logger) error {
addr := req.Addr.String()
log = log.WithFields(map[string]any{
"dst": addr,
})
log.Infof("%s >> %s", conn.RemoteAddr(), addr)
if h.options.Bypass != nil && h.options.Bypass.Contains(addr) {
resp := gosocks4.NewReply(gosocks4.Rejected, nil)
log.Debug(resp)
log.Info("bypass: ", addr)
return resp.Write(conn)
}
cc, err := h.router.Dial(ctx, "tcp", addr)
if err != nil {
resp := gosocks4.NewReply(gosocks4.Failed, nil)
resp.Write(conn)
log.Debug(resp)
return err
}
defer cc.Close()
resp := gosocks4.NewReply(gosocks4.Granted, nil)
if err := resp.Write(conn); err != nil {
log.Error(err)
return err
}
log.Debug(resp)
t := time.Now()
log.Infof("%s <-> %s", conn.RemoteAddr(), addr)
netpkg.Transport(conn, cc)
log.WithFields(map[string]any{
"duration": time.Since(t),
}).Infof("%s >-< %s", conn.RemoteAddr(), addr)
return nil
}
func (h *socks4Handler) handleBind(ctx context.Context, conn net.Conn, req *gosocks4.Request) error {
// TODO: bind
return ErrUnimplemented
}

View File

@ -0,0 +1,20 @@
package v4
import (
"time"
mdata "github.com/go-gost/core/metadata"
)
type metadata struct {
readTimeout time.Duration
}
func (h *socks4Handler) parseMetadata(md mdata.Metadata) (err error) {
const (
readTimeout = "readTimeout"
)
h.md.readTimeout = mdata.GetDuration(md, readTimeout)
return
}

149
handler/socks/v5/bind.go Normal file
View File

@ -0,0 +1,149 @@
package v5
import (
"context"
"fmt"
"net"
"time"
netpkg "github.com/go-gost/core/common/net"
"github.com/go-gost/core/logger"
"github.com/go-gost/gosocks5"
)
func (h *socks5Handler) handleBind(ctx context.Context, conn net.Conn, network, address string, log logger.Logger) error {
log = log.WithFields(map[string]any{
"dst": fmt.Sprintf("%s/%s", address, network),
"cmd": "bind",
})
log.Infof("%s >> %s", conn.RemoteAddr(), address)
if !h.md.enableBind {
reply := gosocks5.NewReply(gosocks5.NotAllowed, nil)
log.Debug(reply)
log.Error("socks5: BIND is disabled")
return reply.Write(conn)
}
// BIND does not support chain.
return h.bindLocal(ctx, conn, network, address, log)
}
func (h *socks5Handler) bindLocal(ctx context.Context, conn net.Conn, network, address string, log logger.Logger) error {
ln, err := net.Listen(network, address) // strict mode: if the port already in use, it will return error
if err != nil {
log.Error(err)
reply := gosocks5.NewReply(gosocks5.Failure, nil)
if err := reply.Write(conn); err != nil {
log.Error(err)
}
log.Debug(reply)
return err
}
socksAddr := gosocks5.Addr{}
if err := socksAddr.ParseFrom(ln.Addr().String()); err != nil {
log.Warn(err)
}
// Issue: may not reachable when host has multi-interface
socksAddr.Host, _, _ = net.SplitHostPort(conn.LocalAddr().String())
socksAddr.Type = 0
reply := gosocks5.NewReply(gosocks5.Succeeded, &socksAddr)
if err := reply.Write(conn); err != nil {
log.Error(err)
ln.Close()
return err
}
log.Debug(reply)
log = log.WithFields(map[string]any{
"bind": fmt.Sprintf("%s/%s", ln.Addr(), ln.Addr().Network()),
})
log.Debugf("bind on %s OK", ln.Addr())
h.serveBind(ctx, conn, ln, log)
return nil
}
func (h *socks5Handler) serveBind(ctx context.Context, conn net.Conn, ln net.Listener, log logger.Logger) {
var rc net.Conn
accept := func() <-chan error {
errc := make(chan error, 1)
go func() {
defer close(errc)
defer ln.Close()
c, err := ln.Accept()
if err != nil {
errc <- err
}
rc = c
}()
return errc
}
pc1, pc2 := net.Pipe()
pipe := func() <-chan error {
errc := make(chan error, 1)
go func() {
defer close(errc)
defer pc1.Close()
errc <- netpkg.Transport(conn, pc1)
}()
return errc
}
defer pc2.Close()
select {
case err := <-accept():
if err != nil {
log.Error(err)
reply := gosocks5.NewReply(gosocks5.Failure, nil)
if err := reply.Write(pc2); err != nil {
log.Error(err)
}
log.Debug(reply)
return
}
defer rc.Close()
log.Debugf("peer %s accepted", rc.RemoteAddr())
log = log.WithFields(map[string]any{
"local": rc.LocalAddr().String(),
"remote": rc.RemoteAddr().String(),
})
raddr := gosocks5.Addr{}
raddr.ParseFrom(rc.RemoteAddr().String())
reply := gosocks5.NewReply(gosocks5.Succeeded, &raddr)
if err := reply.Write(pc2); err != nil {
log.Error(err)
}
log.Debug(reply)
start := time.Now()
log.Infof("%s <-> %s", rc.LocalAddr(), rc.RemoteAddr())
netpkg.Transport(pc2, rc)
log.WithFields(map[string]any{"duration": time.Since(start)}).
Infof("%s >-< %s", rc.LocalAddr(), rc.RemoteAddr())
case err := <-pipe():
if err != nil {
log.Error(err)
}
ln.Close()
return
}
}

View File

@ -0,0 +1,53 @@
package v5
import (
"context"
"fmt"
"net"
"time"
netpkg "github.com/go-gost/core/common/net"
"github.com/go-gost/core/logger"
"github.com/go-gost/gosocks5"
)
func (h *socks5Handler) handleConnect(ctx context.Context, conn net.Conn, network, address string, log logger.Logger) error {
log = log.WithFields(map[string]any{
"dst": fmt.Sprintf("%s/%s", address, network),
"cmd": "connect",
})
log.Infof("%s >> %s", conn.RemoteAddr(), address)
if h.options.Bypass != nil && h.options.Bypass.Contains(address) {
resp := gosocks5.NewReply(gosocks5.NotAllowed, nil)
log.Debug(resp)
log.Info("bypass: ", address)
return resp.Write(conn)
}
cc, err := h.router.Dial(ctx, network, address)
if err != nil {
resp := gosocks5.NewReply(gosocks5.NetUnreachable, nil)
log.Debug(resp)
resp.Write(conn)
return err
}
defer cc.Close()
resp := gosocks5.NewReply(gosocks5.Succeeded, nil)
if err := resp.Write(conn); err != nil {
log.Error(err)
return err
}
log.Debug(resp)
t := time.Now()
log.Infof("%s <-> %s", conn.RemoteAddr(), address)
netpkg.Transport(conn, cc)
log.WithFields(map[string]any{
"duration": time.Since(t),
}).Infof("%s >-< %s", conn.RemoteAddr(), address)
return nil
}

115
handler/socks/v5/handler.go Normal file
View File

@ -0,0 +1,115 @@
package v5
import (
"context"
"errors"
"net"
"time"
"github.com/go-gost/core/chain"
"github.com/go-gost/core/handler"
"github.com/go-gost/core/internal/util/socks"
md "github.com/go-gost/core/metadata"
"github.com/go-gost/core/registry"
"github.com/go-gost/gosocks5"
)
var (
ErrUnknownCmd = errors.New("socks5: unknown command")
)
func init() {
registry.HandlerRegistry().Register("socks5", NewHandler)
registry.HandlerRegistry().Register("socks", NewHandler)
}
type socks5Handler struct {
selector gosocks5.Selector
router *chain.Router
md metadata
options handler.Options
}
func NewHandler(opts ...handler.Option) handler.Handler {
options := handler.Options{}
for _, opt := range opts {
opt(&options)
}
return &socks5Handler{
options: options,
}
}
func (h *socks5Handler) Init(md md.Metadata) (err error) {
if err = h.parseMetadata(md); err != nil {
return
}
h.router = h.options.Router
if h.router == nil {
h.router = (&chain.Router{}).WithLogger(h.options.Logger)
}
h.selector = &serverSelector{
Authenticator: h.options.Auther,
TLSConfig: h.options.TLSConfig,
logger: h.options.Logger,
noTLS: h.md.noTLS,
}
return
}
func (h *socks5Handler) Handle(ctx context.Context, conn net.Conn, opts ...handler.HandleOption) error {
defer conn.Close()
start := time.Now()
log := h.options.Logger.WithFields(map[string]any{
"remote": conn.RemoteAddr().String(),
"local": conn.LocalAddr().String(),
})
log.Infof("%s <> %s", conn.RemoteAddr(), conn.LocalAddr())
defer func() {
log.WithFields(map[string]any{
"duration": time.Since(start),
}).Infof("%s >< %s", conn.RemoteAddr(), conn.LocalAddr())
}()
if h.md.readTimeout > 0 {
conn.SetReadDeadline(time.Now().Add(h.md.readTimeout))
}
conn = gosocks5.ServerConn(conn, h.selector)
req, err := gosocks5.ReadRequest(conn)
if err != nil {
log.Error(err)
return err
}
log.Debug(req)
conn.SetReadDeadline(time.Time{})
address := req.Addr.String()
switch req.Cmd {
case gosocks5.CmdConnect:
return h.handleConnect(ctx, conn, "tcp", address, log)
case gosocks5.CmdBind:
return h.handleBind(ctx, conn, "tcp", address, log)
case socks.CmdMuxBind:
return h.handleMuxBind(ctx, conn, "tcp", address, log)
case gosocks5.CmdUdp:
return h.handleUDP(ctx, conn, log)
case socks.CmdUDPTun:
return h.handleUDPTun(ctx, conn, "udp", address, log)
default:
err = ErrUnknownCmd
log.Error(err)
resp := gosocks5.NewReply(gosocks5.CmdUnsupported, nil)
resp.Write(conn)
log.Debug(resp)
return err
}
}

133
handler/socks/v5/mbind.go Normal file
View File

@ -0,0 +1,133 @@
package v5
import (
"context"
"fmt"
"net"
"time"
netpkg "github.com/go-gost/core/common/net"
"github.com/go-gost/core/internal/util/mux"
"github.com/go-gost/core/logger"
"github.com/go-gost/gosocks5"
)
func (h *socks5Handler) handleMuxBind(ctx context.Context, conn net.Conn, network, address string, log logger.Logger) error {
log = log.WithFields(map[string]any{
"dst": fmt.Sprintf("%s/%s", address, network),
"cmd": "mbind",
})
log.Infof("%s >> %s", conn.RemoteAddr(), address)
if !h.md.enableBind {
reply := gosocks5.NewReply(gosocks5.NotAllowed, nil)
log.Debug(reply)
log.Error("socks5: BIND is disabled")
return reply.Write(conn)
}
return h.muxBindLocal(ctx, conn, network, address, log)
}
func (h *socks5Handler) muxBindLocal(ctx context.Context, conn net.Conn, network, address string, log logger.Logger) error {
ln, err := net.Listen(network, address) // strict mode: if the port already in use, it will return error
if err != nil {
log.Error(err)
reply := gosocks5.NewReply(gosocks5.Failure, nil)
if err := reply.Write(conn); err != nil {
log.Error(err)
}
log.Debug(reply)
return err
}
socksAddr := gosocks5.Addr{}
err = socksAddr.ParseFrom(ln.Addr().String())
if err != nil {
log.Warn(err)
}
// Issue: may not reachable when host has multi-interface
socksAddr.Host, _, _ = net.SplitHostPort(conn.LocalAddr().String())
socksAddr.Type = 0
reply := gosocks5.NewReply(gosocks5.Succeeded, &socksAddr)
if err := reply.Write(conn); err != nil {
log.Error(err)
ln.Close()
return err
}
log.Debug(reply)
log = log.WithFields(map[string]any{
"bind": fmt.Sprintf("%s/%s", ln.Addr(), ln.Addr().Network()),
})
log.Debugf("bind on %s OK", ln.Addr())
return h.serveMuxBind(ctx, conn, ln, log)
}
func (h *socks5Handler) serveMuxBind(ctx context.Context, conn net.Conn, ln net.Listener, log logger.Logger) error {
// Upgrade connection to multiplex stream.
session, err := mux.ClientSession(conn)
if err != nil {
log.Error(err)
return err
}
defer session.Close()
go func() {
defer ln.Close()
for {
conn, err := session.Accept()
if err != nil {
log.Error(err)
return
}
conn.Close() // we do not handle incoming connections.
}
}()
for {
rc, err := ln.Accept()
if err != nil {
log.Error(err)
return err
}
log.Debugf("peer %s accepted", rc.RemoteAddr())
go func(c net.Conn) {
defer c.Close()
log = log.WithFields(map[string]any{
"local": rc.LocalAddr().String(),
"remote": rc.RemoteAddr().String(),
})
sc, err := session.GetConn()
if err != nil {
log.Error(err)
return
}
defer sc.Close()
// incompatible with GOST v2.x
if !h.md.compatibilityMode {
addr := gosocks5.Addr{}
addr.ParseFrom(c.RemoteAddr().String())
reply := gosocks5.NewReply(gosocks5.Succeeded, &addr)
if err := reply.Write(sc); err != nil {
log.Error(err)
return
}
log.Debug(reply)
}
t := time.Now()
log.Infof("%s <-> %s", c.LocalAddr(), c.RemoteAddr())
netpkg.Transport(sc, c)
log.WithFields(map[string]any{"duration": time.Since(t)}).
Infof("%s >-< %s", c.LocalAddr(), c.RemoteAddr())
}(rc)
}
}

View File

@ -0,0 +1,43 @@
package v5
import (
"math"
"time"
mdata "github.com/go-gost/core/metadata"
)
type metadata struct {
readTimeout time.Duration
noTLS bool
enableBind bool
enableUDP bool
udpBufferSize int
compatibilityMode bool
}
func (h *socks5Handler) parseMetadata(md mdata.Metadata) (err error) {
const (
readTimeout = "readTimeout"
noTLS = "notls"
enableBind = "bind"
enableUDP = "udp"
udpBufferSize = "udpBufferSize"
compatibilityMode = "comp"
)
h.md.readTimeout = mdata.GetDuration(md, readTimeout)
h.md.noTLS = mdata.GetBool(md, noTLS)
h.md.enableBind = mdata.GetBool(md, enableBind)
h.md.enableUDP = mdata.GetBool(md, enableUDP)
if bs := mdata.GetInt(md, udpBufferSize); bs > 0 {
h.md.udpBufferSize = int(math.Min(math.Max(float64(bs), 512), 64*1024))
} else {
h.md.udpBufferSize = 1024
}
h.md.compatibilityMode = mdata.GetBool(md, compatibilityMode)
return nil
}

View File

@ -0,0 +1,90 @@
package v5
import (
"crypto/tls"
"net"
"github.com/go-gost/core/auth"
"github.com/go-gost/core/internal/util/socks"
"github.com/go-gost/core/logger"
"github.com/go-gost/gosocks5"
)
type serverSelector struct {
methods []uint8
Authenticator auth.Authenticator
TLSConfig *tls.Config
logger logger.Logger
noTLS bool
}
func (selector *serverSelector) Methods() []uint8 {
return selector.methods
}
func (s *serverSelector) Select(methods ...uint8) (method uint8) {
s.logger.Debugf("%d %d %v", gosocks5.Ver5, len(methods), methods)
method = gosocks5.MethodNoAuth
for _, m := range methods {
if m == socks.MethodTLS && !s.noTLS {
method = m
break
}
}
// when Authenticator is set, auth is mandatory
if s.Authenticator != nil {
if method == gosocks5.MethodNoAuth {
method = gosocks5.MethodUserPass
}
if method == socks.MethodTLS && !s.noTLS {
method = socks.MethodTLSAuth
}
}
return
}
func (s *serverSelector) OnSelected(method uint8, conn net.Conn) (net.Conn, error) {
s.logger.Debugf("%d %d", gosocks5.Ver5, method)
switch method {
case socks.MethodTLS:
conn = tls.Server(conn, s.TLSConfig)
case gosocks5.MethodUserPass, socks.MethodTLSAuth:
if method == socks.MethodTLSAuth {
conn = tls.Server(conn, s.TLSConfig)
}
req, err := gosocks5.ReadUserPassRequest(conn)
if err != nil {
s.logger.Error(err)
return nil, err
}
s.logger.Debug(req)
if s.Authenticator != nil &&
!s.Authenticator.Authenticate(req.Username, req.Password) {
resp := gosocks5.NewUserPassResponse(gosocks5.UserPassVer, gosocks5.Failure)
if err := resp.Write(conn); err != nil {
s.logger.Error(err)
return nil, err
}
s.logger.Info(resp)
return nil, gosocks5.ErrAuthFailure
}
resp := gosocks5.NewUserPassResponse(gosocks5.UserPassVer, gosocks5.Succeeded)
if err := resp.Write(conn); err != nil {
s.logger.Error(err)
return nil, err
}
s.logger.Debug(resp)
case gosocks5.MethodNoAcceptable:
return nil, gosocks5.ErrBadMethod
}
return conn, nil
}

85
handler/socks/v5/udp.go Normal file
View File

@ -0,0 +1,85 @@
package v5
import (
"context"
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"time"
"github.com/go-gost/core/common/net/relay"
"github.com/go-gost/core/internal/util/socks"
"github.com/go-gost/core/logger"
"github.com/go-gost/gosocks5"
)
func (h *socks5Handler) handleUDP(ctx context.Context, conn net.Conn, log logger.Logger) error {
log = log.WithFields(map[string]any{
"cmd": "udp",
})
if !h.md.enableUDP {
reply := gosocks5.NewReply(gosocks5.NotAllowed, nil)
log.Debug(reply)
log.Error("socks5: UDP relay is disabled")
return reply.Write(conn)
}
cc, err := net.ListenUDP("udp", nil)
if err != nil {
log.Error(err)
reply := gosocks5.NewReply(gosocks5.Failure, nil)
reply.Write(conn)
log.Debug(reply)
return err
}
defer cc.Close()
saddr := gosocks5.Addr{}
saddr.ParseFrom(cc.LocalAddr().String())
saddr.Type = 0
saddr.Host, _, _ = net.SplitHostPort(conn.LocalAddr().String()) // replace the IP to the out-going interface's
reply := gosocks5.NewReply(gosocks5.Succeeded, &saddr)
if err := reply.Write(conn); err != nil {
log.Error(err)
return err
}
log.Debug(reply)
log = log.WithFields(map[string]any{
"bind": fmt.Sprintf("%s/%s", cc.LocalAddr(), cc.LocalAddr().Network()),
})
log.Debugf("bind on %s OK", cc.LocalAddr())
// obtain a udp connection
c, err := h.router.Dial(ctx, "udp", "") // UDP association
if err != nil {
log.Error(err)
return err
}
defer c.Close()
pc, ok := c.(net.PacketConn)
if !ok {
err := errors.New("socks5: wrong connection type")
log.Error(err)
return err
}
r := relay.NewUDPRelay(socks.UDPConn(cc, h.md.udpBufferSize), pc).
WithBypass(h.options.Bypass).
WithLogger(log)
r.SetBufferSize(h.md.udpBufferSize)
go r.Run()
t := time.Now()
log.Infof("%s <-> %s", conn.RemoteAddr(), cc.LocalAddr())
io.Copy(ioutil.Discard, conn)
log.WithFields(map[string]any{"duration": time.Since(t)}).
Infof("%s >-< %s", conn.RemoteAddr(), cc.LocalAddr())
return nil
}

View File

@ -0,0 +1,72 @@
package v5
import (
"context"
"net"
"time"
"github.com/go-gost/core/common/net/relay"
"github.com/go-gost/core/internal/util/socks"
"github.com/go-gost/core/logger"
"github.com/go-gost/gosocks5"
)
func (h *socks5Handler) handleUDPTun(ctx context.Context, conn net.Conn, network, address string, log logger.Logger) error {
log = log.WithFields(map[string]any{
"cmd": "udp-tun",
})
bindAddr, _ := net.ResolveUDPAddr(network, address)
if bindAddr == nil {
bindAddr = &net.UDPAddr{}
}
if bindAddr.Port == 0 {
// relay mode
if !h.md.enableUDP {
reply := gosocks5.NewReply(gosocks5.NotAllowed, nil)
log.Debug(reply)
log.Error("socks5: UDP relay is disabled")
return reply.Write(conn)
}
} else {
// BIND mode
if !h.md.enableBind {
reply := gosocks5.NewReply(gosocks5.NotAllowed, nil)
log.Debug(reply)
log.Error("socks5: BIND is disabled")
return reply.Write(conn)
}
}
pc, err := net.ListenUDP(network, bindAddr)
if err != nil {
log.Error(err)
return err
}
defer pc.Close()
saddr := gosocks5.Addr{}
saddr.ParseFrom(pc.LocalAddr().String())
reply := gosocks5.NewReply(gosocks5.Succeeded, &saddr)
if err := reply.Write(conn); err != nil {
log.Error(err)
return err
}
log.Debug(reply)
log.Debugf("bind on %s OK", pc.LocalAddr())
r := relay.NewUDPRelay(socks.UDPTunServerConn(conn), pc).
WithBypass(h.options.Bypass).
WithLogger(log)
r.SetBufferSize(h.md.udpBufferSize)
t := time.Now()
log.Infof("%s <-> %s", conn.RemoteAddr(), pc.LocalAddr())
r.Run()
log.WithFields(map[string]any{
"duration": time.Since(t),
}).Infof("%s >-< %s", conn.RemoteAddr(), pc.LocalAddr())
return nil
}