commit 7db81fcfeb66f7efe00095ae887ad537e93be5ec Author: ginuerzh Date: Wed Mar 16 19:40:29 2022 +0800 initial commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..94333aa --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# The core library \ No newline at end of file diff --git a/admission/admission.go b/admission/admission.go new file mode 100644 index 0000000..ab3e71e --- /dev/null +++ b/admission/admission.go @@ -0,0 +1,89 @@ +package admission + +import ( + "net" + "strconv" + + "github.com/go-gost/core/common/matcher" + "github.com/go-gost/core/logger" +) + +type Admission interface { + Admit(addr string) bool +} + +type options struct { + logger logger.Logger +} + +type Option func(opts *options) + +func LoggerOption(logger logger.Logger) Option { + return func(opts *options) { + opts.logger = logger + } +} + +type admission struct { + matchers []matcher.Matcher + reversed bool + options options +} + +// NewAdmission creates and initializes a new Admission using matchers as its match rules. +// The rules will be reversed if the reversed is true. +func NewAdmission(reversed bool, matchers []matcher.Matcher, opts ...Option) Admission { + options := options{} + for _, opt := range opts { + opt(&options) + } + return &admission{ + matchers: matchers, + reversed: reversed, + options: options, + } +} + +// NewAdmissionPatterns creates and initializes a new Admission using matcher patterns as its match rules. +// The rules will be reversed if the reverse is true. +func NewAdmissionPatterns(reversed bool, patterns []string, opts ...Option) Admission { + var matchers []matcher.Matcher + for _, pattern := range patterns { + if m := matcher.NewMatcher(pattern); m != nil { + matchers = append(matchers, m) + } + } + return NewAdmission(reversed, matchers, opts...) +} + +func (p *admission) Admit(addr string) bool { + if addr == "" || p == nil || len(p.matchers) == 0 { + p.options.logger.Debugf("admission: %v is denied", addr) + return false + } + + // try to strip the port + if host, port, _ := net.SplitHostPort(addr); host != "" && port != "" { + if p, _ := strconv.Atoi(port); p > 0 { // port is valid + addr = host + } + } + + var matched bool + for _, matcher := range p.matchers { + if matcher == nil { + continue + } + if matcher.Match(addr) { + matched = true + break + } + } + + b := !p.reversed && matched || + p.reversed && !matched + if !b { + p.options.logger.Debugf("admission: %v is denied", addr) + } + return b +} diff --git a/admission/wrapper/conn.go b/admission/wrapper/conn.go new file mode 100644 index 0000000..2cb527f --- /dev/null +++ b/admission/wrapper/conn.go @@ -0,0 +1,223 @@ +package wrapper + +import ( + "errors" + "io" + "net" + "syscall" + + "github.com/go-gost/core/admission" +) + +var ( + errUnsupport = errors.New("unsupported operation") +) + +type packetConn struct { + net.PacketConn + admission admission.Admission +} + +func WrapPacketConn(admission admission.Admission, pc net.PacketConn) net.PacketConn { + if admission == nil { + return pc + } + return &packetConn{ + PacketConn: pc, + admission: admission, + } +} + +func (c *packetConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { + for { + n, addr, err = c.PacketConn.ReadFrom(p) + if err != nil { + return + } + + if c.admission != nil && + !c.admission.Admit(addr.String()) { + continue + } + + return + } +} + +type udpConn struct { + net.PacketConn + admission admission.Admission +} + +func WrapUDPConn(admission admission.Admission, pc net.PacketConn) UDPConn { + return &udpConn{ + PacketConn: pc, + admission: admission, + } +} + +func (c *udpConn) RemoteAddr() net.Addr { + if nc, ok := c.PacketConn.(remoteAddr); ok { + return nc.RemoteAddr() + } + return nil +} + +func (c *udpConn) SetReadBuffer(n int) error { + if nc, ok := c.PacketConn.(setBuffer); ok { + return nc.SetReadBuffer(n) + } + return errUnsupport +} + +func (c *udpConn) SetWriteBuffer(n int) error { + if nc, ok := c.PacketConn.(setBuffer); ok { + return nc.SetWriteBuffer(n) + } + return errUnsupport +} + +func (c *udpConn) Read(b []byte) (n int, err error) { + if nc, ok := c.PacketConn.(io.Reader); ok { + n, err = nc.Read(b) + return + } + err = errUnsupport + return +} + +func (c *udpConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { + for { + n, addr, err = c.PacketConn.ReadFrom(p) + if err != nil { + return + } + if c.admission != nil && + !c.admission.Admit(addr.String()) { + continue + } + return + } +} + +func (c *udpConn) ReadFromUDP(b []byte) (n int, addr *net.UDPAddr, err error) { + if nc, ok := c.PacketConn.(readUDP); ok { + for { + n, addr, err = nc.ReadFromUDP(b) + if err != nil { + return + } + if c.admission != nil && + !c.admission.Admit(addr.String()) { + continue + } + return + } + } + err = errUnsupport + return +} + +func (c *udpConn) ReadMsgUDP(b, oob []byte) (n, oobn, flags int, addr *net.UDPAddr, err error) { + if nc, ok := c.PacketConn.(readUDP); ok { + for { + n, oobn, flags, addr, err = nc.ReadMsgUDP(b, oob) + if err != nil { + return + } + if c.admission != nil && + !c.admission.Admit(addr.String()) { + continue + } + return + } + } + err = errUnsupport + return +} + +func (c *udpConn) Write(b []byte) (n int, err error) { + if nc, ok := c.PacketConn.(io.Writer); ok { + n, err = nc.Write(b) + return + } + err = errUnsupport + return +} + +func (c *udpConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { + n, err = c.PacketConn.WriteTo(p, addr) + return +} + +func (c *udpConn) WriteToUDP(b []byte, addr *net.UDPAddr) (n int, err error) { + if nc, ok := c.PacketConn.(writeUDP); ok { + n, err = nc.WriteToUDP(b, addr) + return + } + err = errUnsupport + return +} + +func (c *udpConn) WriteMsgUDP(b, oob []byte, addr *net.UDPAddr) (n, oobn int, err error) { + if nc, ok := c.PacketConn.(writeUDP); ok { + n, oobn, err = nc.WriteMsgUDP(b, oob, addr) + return + } + err = errUnsupport + return +} + +func (c *udpConn) SyscallConn() (rc syscall.RawConn, err error) { + if nc, ok := c.PacketConn.(syscallConn); ok { + return nc.SyscallConn() + } + err = errUnsupport + return +} + +func (c *udpConn) SetDSCP(n int) error { + if nc, ok := c.PacketConn.(setDSCP); ok { + return nc.SetDSCP(n) + } + return nil +} + +type UDPConn interface { + net.PacketConn + io.Reader + io.Writer + readUDP + writeUDP + setBuffer + syscallConn + remoteAddr +} + +type setBuffer interface { + SetReadBuffer(bytes int) error + SetWriteBuffer(bytes int) error +} + +type readUDP interface { + ReadFromUDP(b []byte) (n int, addr *net.UDPAddr, err error) + ReadMsgUDP(b, oob []byte) (n, oobn, flags int, addr *net.UDPAddr, err error) +} + +type writeUDP interface { + WriteToUDP(b []byte, addr *net.UDPAddr) (int, error) + WriteMsgUDP(b, oob []byte, addr *net.UDPAddr) (n, oobn int, err error) +} + +type syscallConn interface { + SyscallConn() (syscall.RawConn, error) +} + +type remoteAddr interface { + RemoteAddr() net.Addr +} + +// tcpraw.TCPConn +type setDSCP interface { + SetDSCP(int) error +} diff --git a/admission/wrapper/listener.go b/admission/wrapper/listener.go new file mode 100644 index 0000000..05d4dee --- /dev/null +++ b/admission/wrapper/listener.go @@ -0,0 +1,37 @@ +package wrapper + +import ( + "net" + + "github.com/go-gost/core/admission" +) + +type listener struct { + net.Listener + admission admission.Admission +} + +func WrapListener(admission admission.Admission, ln net.Listener) net.Listener { + if admission == nil { + return ln + } + return &listener{ + Listener: ln, + admission: admission, + } +} + +func (ln *listener) Accept() (net.Conn, error) { + for { + c, err := ln.Listener.Accept() + if err != nil { + return nil, err + } + if ln.admission != nil && + !ln.admission.Admit(c.RemoteAddr().String()) { + c.Close() + continue + } + return c, err + } +} diff --git a/auth/auth.go b/auth/auth.go new file mode 100644 index 0000000..6bdf54d --- /dev/null +++ b/auth/auth.go @@ -0,0 +1,28 @@ +package auth + +// Authenticator is an interface for user authentication. +type Authenticator interface { + Authenticate(user, password string) bool +} + +// authenticator is an Authenticator that authenticates client by key-value pairs. +type authenticator struct { + kvs map[string]string +} + +// NewAuthenticator creates an Authenticator that authenticates client by pre-defined user mapping. +func NewAuthenticator(kvs map[string]string) Authenticator { + return &authenticator{ + kvs: kvs, + } +} + +// Authenticate checks the validity of the provided user-password pair. +func (au *authenticator) Authenticate(user, password string) bool { + if au == nil || len(au.kvs) == 0 { + return true + } + + v, ok := au.kvs[user] + return ok && (v == "" || password == v) +} diff --git a/bypass/bypass.go b/bypass/bypass.go new file mode 100644 index 0000000..a3cd3ee --- /dev/null +++ b/bypass/bypass.go @@ -0,0 +1,90 @@ +package bypass + +import ( + "net" + "strconv" + + "github.com/go-gost/core/common/matcher" + "github.com/go-gost/core/logger" +) + +// Bypass is a filter of address (IP or domain). +type Bypass interface { + // Contains reports whether the bypass includes addr. + Contains(addr string) bool +} + +type options struct { + logger logger.Logger +} + +type Option func(opts *options) + +func LoggerOption(logger logger.Logger) Option { + return func(opts *options) { + opts.logger = logger + } +} + +type bypass struct { + matchers []matcher.Matcher + reversed bool + options options +} + +// NewBypass creates and initializes a new Bypass using matchers as its match rules. +// The rules will be reversed if the reversed is true. +func NewBypass(reversed bool, matchers []matcher.Matcher, opts ...Option) Bypass { + options := options{} + for _, opt := range opts { + opt(&options) + } + return &bypass{ + matchers: matchers, + reversed: reversed, + options: options, + } +} + +// NewBypassPatterns creates and initializes a new Bypass using matcher patterns as its match rules. +// The rules will be reversed if the reverse is true. +func NewBypassPatterns(reversed bool, patterns []string, opts ...Option) Bypass { + var matchers []matcher.Matcher + for _, pattern := range patterns { + if m := matcher.NewMatcher(pattern); m != nil { + matchers = append(matchers, m) + } + } + return NewBypass(reversed, matchers, opts...) +} + +func (bp *bypass) Contains(addr string) bool { + if addr == "" || bp == nil || len(bp.matchers) == 0 { + return false + } + + // try to strip the port + if host, port, _ := net.SplitHostPort(addr); host != "" && port != "" { + if p, _ := strconv.Atoi(port); p > 0 { // port is valid + addr = host + } + } + + var matched bool + for _, matcher := range bp.matchers { + if matcher == nil { + continue + } + if matcher.Match(addr) { + matched = true + break + } + } + + b := !bp.reversed && matched || + bp.reversed && !matched + if b { + bp.options.logger.Debugf("bypass: %s", addr) + } + return b +} diff --git a/chain/chain.go b/chain/chain.go new file mode 100644 index 0000000..90c3ba6 --- /dev/null +++ b/chain/chain.go @@ -0,0 +1,41 @@ +package chain + +type Chainer interface { + Route(network, address string) *Route +} + +type Chain struct { + groups []*NodeGroup +} + +func (c *Chain) AddNodeGroup(group *NodeGroup) { + c.groups = append(c.groups, group) +} + +func (c *Chain) Route(network, address string) (r *Route) { + if c == nil || len(c.groups) == 0 { + return + } + + r = &Route{} + for _, group := range c.groups { + node := group.Next() + if node == nil { + return + } + if node.Bypass != nil && node.Bypass.Contains(address) { + break + } + + if node.Transport.Multiplex() { + tr := node.Transport.Copy(). + WithRoute(r) + node = node.Copy() + node.Transport = tr + r = &Route{} + } + + r.addNode(node) + } + return r +} diff --git a/chain/node.go b/chain/node.go new file mode 100644 index 0000000..bd4cfcd --- /dev/null +++ b/chain/node.go @@ -0,0 +1,97 @@ +package chain + +import ( + "sync/atomic" + "time" + + "github.com/go-gost/core/bypass" + "github.com/go-gost/core/hosts" + "github.com/go-gost/core/resolver" +) + +type Node struct { + Name string + Addr string + Transport *Transport + Bypass bypass.Bypass + Resolver resolver.Resolver + Hosts hosts.HostMapper + Marker *FailMarker +} + +func (node *Node) Copy() *Node { + n := &Node{} + *n = *node + return n +} + +type NodeGroup struct { + nodes []*Node + selector Selector +} + +func NewNodeGroup(nodes ...*Node) *NodeGroup { + return &NodeGroup{ + nodes: nodes, + } +} + +func (g *NodeGroup) AddNode(node *Node) { + g.nodes = append(g.nodes, node) +} + +func (g *NodeGroup) WithSelector(selector Selector) *NodeGroup { + g.selector = selector + return g +} + +func (g *NodeGroup) Next() *Node { + if g == nil || len(g.nodes) == 0 { + return nil + } + + s := g.selector + if s == nil { + s = DefaultSelector + } + + return s.Select(g.nodes...) +} + +type FailMarker struct { + failTime int64 + failCount int64 +} + +func (m *FailMarker) FailTime() int64 { + if m == nil { + return 0 + } + + return atomic.LoadInt64(&m.failTime) +} + +func (m *FailMarker) FailCount() int64 { + if m == nil { + return 0 + } + + return atomic.LoadInt64(&m.failCount) +} + +func (m *FailMarker) Mark() { + if m == nil { + return + } + + atomic.AddInt64(&m.failCount, 1) + atomic.StoreInt64(&m.failTime, time.Now().Unix()) +} + +func (m *FailMarker) Reset() { + if m == nil { + return + } + + atomic.StoreInt64(&m.failCount, 0) +} diff --git a/chain/resovle.go b/chain/resovle.go new file mode 100644 index 0000000..3464302 --- /dev/null +++ b/chain/resovle.go @@ -0,0 +1,47 @@ +package chain + +import ( + "context" + "fmt" + "net" + + "github.com/go-gost/core/hosts" + "github.com/go-gost/core/logger" + "github.com/go-gost/core/resolver" +) + +func resolve(ctx context.Context, network, addr string, r resolver.Resolver, hosts hosts.HostMapper, log logger.Logger) (string, error) { + if addr == "" { + return addr, nil + } + + host, port, err := net.SplitHostPort(addr) + if err != nil { + return "", err + } + if host == "" { + return addr, nil + } + + if hosts != nil { + if ips, _ := hosts.Lookup(network, host); len(ips) > 0 { + log.Debugf("hit host mapper: %s -> %s", host, ips) + return net.JoinHostPort(ips[0].String(), port), nil + } + } + + if r != nil { + ips, err := r.Resolve(ctx, network, host) + if err != nil { + if err == resolver.ErrInvalid { + return addr, nil + } + log.Error(err) + } + if len(ips) == 0 { + return "", fmt.Errorf("resolver: domain %s does not exists", host) + } + return net.JoinHostPort(ips[0].String(), port), nil + } + return addr, nil +} diff --git a/chain/route.go b/chain/route.go new file mode 100644 index 0000000..1be04da --- /dev/null +++ b/chain/route.go @@ -0,0 +1,192 @@ +package chain + +import ( + "context" + "errors" + "fmt" + "net" + "time" + + "github.com/go-gost/core/common/net/dialer" + "github.com/go-gost/core/common/util/udp" + "github.com/go-gost/core/connector" + "github.com/go-gost/core/logger" +) + +var ( + ErrEmptyRoute = errors.New("empty route") +) + +type Route struct { + nodes []*Node + ifceName string + logger logger.Logger +} + +func (r *Route) addNode(node *Node) { + r.nodes = append(r.nodes, node) +} + +func (r *Route) Dial(ctx context.Context, network, address string) (net.Conn, error) { + if r.Len() == 0 { + netd := dialer.NetDialer{ + Timeout: 30 * time.Second, + } + if r != nil { + netd.Interface = r.ifceName + } + return netd.Dial(ctx, network, address) + } + + conn, err := r.connect(ctx) + if err != nil { + return nil, err + } + + cc, err := r.GetNode(r.Len()-1).Transport.Connect(ctx, conn, network, address) + if err != nil { + conn.Close() + return nil, err + } + return cc, nil +} + +func (r *Route) Bind(ctx context.Context, network, address string, opts ...connector.BindOption) (net.Listener, error) { + if r.Len() == 0 { + return r.bindLocal(ctx, network, address, opts...) + } + + conn, err := r.connect(ctx) + if err != nil { + return nil, err + } + + ln, err := r.GetNode(r.Len()-1).Transport.Bind(ctx, conn, network, address, opts...) + if err != nil { + conn.Close() + return nil, err + } + + return ln, nil +} + +func (r *Route) connect(ctx context.Context) (conn net.Conn, err error) { + if r.Len() == 0 { + return nil, ErrEmptyRoute + } + + network := "ip" + node := r.nodes[0] + + addr, err := resolve(ctx, network, node.Addr, node.Resolver, node.Hosts, r.logger) + if err != nil { + node.Marker.Mark() + return + } + cc, err := node.Transport.Dial(ctx, addr) + if err != nil { + node.Marker.Mark() + return + } + + cn, err := node.Transport.Handshake(ctx, cc) + if err != nil { + cc.Close() + node.Marker.Mark() + return + } + node.Marker.Reset() + + preNode := node + for _, node := range r.nodes[1:] { + addr, err = resolve(ctx, network, node.Addr, node.Resolver, node.Hosts, r.logger) + if err != nil { + cn.Close() + node.Marker.Mark() + return + } + cc, err = preNode.Transport.Connect(ctx, cn, "tcp", addr) + if err != nil { + cn.Close() + node.Marker.Mark() + return + } + cc, err = node.Transport.Handshake(ctx, cc) + if err != nil { + cn.Close() + node.Marker.Mark() + return + } + node.Marker.Reset() + + cn = cc + preNode = node + } + + conn = cn + return +} + +func (r *Route) Len() int { + if r == nil { + return 0 + } + return len(r.nodes) +} + +func (r *Route) GetNode(index int) *Node { + if r.Len() == 0 || index < 0 || index >= len(r.nodes) { + return nil + } + return r.nodes[index] +} + +func (r *Route) Path() (path []*Node) { + if r == nil || len(r.nodes) == 0 { + return nil + } + + for _, node := range r.nodes { + if node.Transport != nil && node.Transport.route != nil { + path = append(path, node.Transport.route.Path()...) + } + path = append(path, node) + } + return +} + +func (r *Route) bindLocal(ctx context.Context, network, address string, opts ...connector.BindOption) (net.Listener, error) { + options := connector.BindOptions{} + for _, opt := range opts { + opt(&options) + } + + switch network { + case "tcp", "tcp4", "tcp6": + addr, err := net.ResolveTCPAddr(network, address) + if err != nil { + return nil, err + } + return net.ListenTCP(network, addr) + case "udp", "udp4", "udp6": + addr, err := net.ResolveUDPAddr(network, address) + if err != nil { + return nil, err + } + conn, err := net.ListenUDP(network, addr) + if err != nil { + return nil, err + } + logger := logger.Default().WithFields(map[string]any{ + "network": network, + "address": address, + }) + ln := udp.NewListener(conn, addr, + options.Backlog, options.UDPDataQueueSize, options.UDPDataBufferSize, + options.UDPConnTTL, logger) + return ln, err + default: + err := fmt.Errorf("network %s unsupported", network) + return nil, err + } +} diff --git a/chain/router.go b/chain/router.go new file mode 100644 index 0000000..c8e8351 --- /dev/null +++ b/chain/router.go @@ -0,0 +1,169 @@ +package chain + +import ( + "bytes" + "context" + "fmt" + "net" + "time" + + "github.com/go-gost/core/connector" + "github.com/go-gost/core/hosts" + "github.com/go-gost/core/logger" + "github.com/go-gost/core/resolver" +) + +type Router struct { + timeout time.Duration + retries int + ifceName string + chain Chainer + resolver resolver.Resolver + hosts hosts.HostMapper + logger logger.Logger +} + +func (r *Router) WithTimeout(timeout time.Duration) *Router { + r.timeout = timeout + return r +} + +func (r *Router) WithRetries(retries int) *Router { + r.retries = retries + return r +} + +func (r *Router) WithInterface(ifceName string) *Router { + r.ifceName = ifceName + return r +} + +func (r *Router) WithChain(chain Chainer) *Router { + r.chain = chain + return r +} + +func (r *Router) WithResolver(resolver resolver.Resolver) *Router { + r.resolver = resolver + return r +} + +func (r *Router) WithHosts(hosts hosts.HostMapper) *Router { + r.hosts = hosts + return r +} + +func (r *Router) Hosts() hosts.HostMapper { + if r != nil { + return r.hosts + } + return nil +} + +func (r *Router) WithLogger(logger logger.Logger) *Router { + r.logger = logger + return r +} + +func (r *Router) Dial(ctx context.Context, network, address string) (conn net.Conn, err error) { + conn, err = r.dial(ctx, network, address) + if err != nil { + return + } + if network == "udp" || network == "udp4" || network == "udp6" { + if _, ok := conn.(net.PacketConn); !ok { + return &packetConn{conn}, nil + } + } + return +} + +func (r *Router) dial(ctx context.Context, network, address string) (conn net.Conn, err error) { + count := r.retries + 1 + if count <= 0 { + count = 1 + } + r.logger.Debugf("dial %s/%s", address, network) + + for i := 0; i < count; i++ { + var route *Route + if r.chain != nil { + route = r.chain.Route(network, address) + } + + if r.logger.IsLevelEnabled(logger.DebugLevel) { + buf := bytes.Buffer{} + for _, node := range route.Path() { + fmt.Fprintf(&buf, "%s@%s > ", node.Name, node.Addr) + } + fmt.Fprintf(&buf, "%s", address) + r.logger.Debugf("route(retry=%d) %s", i, buf.String()) + } + + address, err = resolve(ctx, "ip", address, r.resolver, r.hosts, r.logger) + if err != nil { + r.logger.Error(err) + break + } + + if route == nil { + route = &Route{} + } + route.ifceName = r.ifceName + route.logger = r.logger + + conn, err = route.Dial(ctx, network, address) + if err == nil { + break + } + r.logger.Errorf("route(retry=%d) %s", i, err) + } + + return +} + +func (r *Router) Bind(ctx context.Context, network, address string, opts ...connector.BindOption) (ln net.Listener, err error) { + count := r.retries + 1 + if count <= 0 { + count = 1 + } + r.logger.Debugf("bind on %s/%s", address, network) + + for i := 0; i < count; i++ { + var route *Route + if r.chain != nil { + route = r.chain.Route(network, address) + } + + if r.logger.IsLevelEnabled(logger.DebugLevel) { + buf := bytes.Buffer{} + for _, node := range route.Path() { + fmt.Fprintf(&buf, "%s@%s > ", node.Name, node.Addr) + } + fmt.Fprintf(&buf, "%s", address) + r.logger.Debugf("route(retry=%d) %s", i, buf.String()) + } + + ln, err = route.Bind(ctx, network, address, opts...) + if err == nil { + break + } + r.logger.Errorf("route(retry=%d) %s", i, err) + } + + return +} + +type packetConn struct { + net.Conn +} + +func (c *packetConn) ReadFrom(b []byte) (n int, addr net.Addr, err error) { + n, err = c.Read(b) + addr = c.Conn.RemoteAddr() + return +} + +func (c *packetConn) WriteTo(b []byte, addr net.Addr) (n int, err error) { + return c.Write(b) +} diff --git a/chain/selector.go b/chain/selector.go new file mode 100644 index 0000000..2ad0a03 --- /dev/null +++ b/chain/selector.go @@ -0,0 +1,170 @@ +package chain + +import ( + "math/rand" + "net" + "strconv" + "sync" + "sync/atomic" + "time" +) + +// default options for FailFilter +const ( + DefaultFailTimeout = 30 * time.Second +) + +var ( + DefaultSelector = NewSelector(RoundRobinStrategy()) +) + +type Selector interface { + Select(nodes ...*Node) *Node +} + +type selector struct { + strategy Strategy + filters []Filter +} + +func NewSelector(strategy Strategy, filters ...Filter) Selector { + return &selector{ + filters: filters, + strategy: strategy, + } +} + +func (s *selector) Select(nodes ...*Node) *Node { + for _, filter := range s.filters { + nodes = filter.Filter(nodes...) + } + if len(nodes) == 0 { + return nil + } + return s.strategy.Apply(nodes...) +} + +type Strategy interface { + Apply(nodes ...*Node) *Node +} + +type roundRobinStrategy struct { + counter uint64 +} + +// RoundRobinStrategy is a strategy for node selector. +// The node will be selected by round-robin algorithm. +func RoundRobinStrategy() Strategy { + return &roundRobinStrategy{} +} + +func (s *roundRobinStrategy) Apply(nodes ...*Node) *Node { + if len(nodes) == 0 { + return nil + } + + n := atomic.AddUint64(&s.counter, 1) - 1 + return nodes[int(n%uint64(len(nodes)))] +} + +type randomStrategy struct { + rand *rand.Rand + mux sync.Mutex +} + +// RandomStrategy is a strategy for node selector. +// The node will be selected randomly. +func RandomStrategy() Strategy { + return &randomStrategy{ + rand: rand.New(rand.NewSource(time.Now().UnixNano())), + } +} + +func (s *randomStrategy) Apply(nodes ...*Node) *Node { + if len(nodes) == 0 { + return nil + } + + s.mux.Lock() + defer s.mux.Unlock() + + r := s.rand.Int() + + return nodes[r%len(nodes)] +} + +type fifoStrategy struct{} + +// FIFOStrategy is a strategy for node selector. +// The node will be selected from first to last, +// and will stick to the selected node until it is failed. +func FIFOStrategy() Strategy { + return &fifoStrategy{} +} + +// Apply applies the fifo strategy for the nodes. +func (s *fifoStrategy) Apply(nodes ...*Node) *Node { + if len(nodes) == 0 { + return nil + } + return nodes[0] +} + +type Filter interface { + Filter(nodes ...*Node) []*Node +} + +type failFilter struct { + maxFails int + failTimeout time.Duration +} + +// FailFilter filters the dead node. +// A node is marked as dead if its failed count is greater than MaxFails. +func FailFilter(maxFails int, timeout time.Duration) Filter { + return &failFilter{ + maxFails: maxFails, + failTimeout: timeout, + } +} + +// Filter filters dead nodes. +func (f *failFilter) Filter(nodes ...*Node) []*Node { + maxFails := f.maxFails + failTimeout := f.failTimeout + if failTimeout == 0 { + failTimeout = DefaultFailTimeout + } + + if len(nodes) <= 1 || maxFails <= 0 { + return nodes + } + var nl []*Node + for _, node := range nodes { + if node.Marker.FailCount() < int64(maxFails) || + time.Since(time.Unix(node.Marker.FailTime(), 0)) >= failTimeout { + nl = append(nl, node) + } + } + return nl +} + +type invalidFilter struct{} + +// InvalidFilter filters the invalid node. +// A node is invalid if its port is invalid (negative or zero value). +func InvalidFilter() Filter { + return &invalidFilter{} +} + +// Filter filters invalid nodes. +func (f *invalidFilter) Filter(nodes ...*Node) []*Node { + var nl []*Node + for _, node := range nodes { + _, sport, _ := net.SplitHostPort(node.Addr) + if port, _ := strconv.Atoi(sport); port > 0 { + nl = append(nl, node) + } + } + return nl +} diff --git a/chain/transport.go b/chain/transport.go new file mode 100644 index 0000000..d70827a --- /dev/null +++ b/chain/transport.go @@ -0,0 +1,100 @@ +package chain + +import ( + "context" + "net" + "time" + + net_dialer "github.com/go-gost/core/common/net/dialer" + "github.com/go-gost/core/connector" + "github.com/go-gost/core/dialer" +) + +type Transport struct { + addr string + ifceName string + route *Route + dialer dialer.Dialer + connector connector.Connector +} + +func (tr *Transport) Copy() *Transport { + tr2 := &Transport{} + *tr2 = *tr + return tr +} + +func (tr *Transport) WithInterface(ifceName string) *Transport { + tr.ifceName = ifceName + return tr +} + +func (tr *Transport) WithDialer(dialer dialer.Dialer) *Transport { + tr.dialer = dialer + return tr +} + +func (tr *Transport) WithConnector(connector connector.Connector) *Transport { + tr.connector = connector + return tr +} + +func (tr *Transport) Dial(ctx context.Context, addr string) (net.Conn, error) { + netd := &net_dialer.NetDialer{ + Interface: tr.ifceName, + Timeout: 30 * time.Second, + } + if tr.route.Len() > 0 { + netd.DialFunc = func(ctx context.Context, network, addr string) (net.Conn, error) { + return tr.route.Dial(ctx, network, addr) + } + } + opts := []dialer.DialOption{ + dialer.HostDialOption(tr.addr), + dialer.NetDialerDialOption(netd), + } + return tr.dialer.Dial(ctx, addr, opts...) +} + +func (tr *Transport) Handshake(ctx context.Context, conn net.Conn) (net.Conn, error) { + var err error + if hs, ok := tr.dialer.(dialer.Handshaker); ok { + conn, err = hs.Handshake(ctx, conn, + dialer.AddrHandshakeOption(tr.addr)) + if err != nil { + return nil, err + } + } + if hs, ok := tr.connector.(connector.Handshaker); ok { + return hs.Handshake(ctx, conn) + } + return conn, nil +} + +func (tr *Transport) Connect(ctx context.Context, conn net.Conn, network, address string) (net.Conn, error) { + return tr.connector.Connect(ctx, conn, network, address) +} + +func (tr *Transport) Bind(ctx context.Context, conn net.Conn, network, address string, opts ...connector.BindOption) (net.Listener, error) { + if binder, ok := tr.connector.(connector.Binder); ok { + return binder.Bind(ctx, conn, network, address, opts...) + } + return nil, connector.ErrBindUnsupported +} + +func (tr *Transport) Multiplex() bool { + if mux, ok := tr.dialer.(dialer.Multiplexer); ok { + return mux.Multiplex() + } + return false +} + +func (tr *Transport) WithRoute(r *Route) *Transport { + tr.route = r + return tr +} + +func (tr *Transport) WithAddr(addr string) *Transport { + tr.addr = addr + return tr +} diff --git a/common/bufpool/pool.go b/common/bufpool/pool.go new file mode 100644 index 0000000..5af2c87 --- /dev/null +++ b/common/bufpool/pool.go @@ -0,0 +1,113 @@ +package bufpool + +import "sync" + +var ( + pools = []struct { + size int + pool sync.Pool + }{ + { + size: 128, + pool: sync.Pool{ + New: func() any { + b := make([]byte, 128) + return &b + }, + }, + }, + { + size: 512, + pool: sync.Pool{ + New: func() any { + b := make([]byte, 512) + return &b + }, + }, + }, + { + size: 1024, + pool: sync.Pool{ + New: func() any { + b := make([]byte, 1024) + return &b + }, + }, + }, + { + size: 4096, + pool: sync.Pool{ + New: func() any { + b := make([]byte, 4096) + return &b + }, + }, + }, + { + size: 8192, + pool: sync.Pool{ + New: func() any { + b := make([]byte, 8192) + return &b + }, + }, + }, + { + size: 16 * 1024, + pool: sync.Pool{ + New: func() any { + b := make([]byte, 16*1024) + return &b + }, + }, + }, + { + size: 32 * 1024, + pool: sync.Pool{ + New: func() any { + b := make([]byte, 32*1024) + return &b + }, + }, + }, + { + size: 64 * 1024, + pool: sync.Pool{ + New: func() any { + b := make([]byte, 64*1024) + return &b + }, + }, + }, + { + size: 65 * 1024, + pool: sync.Pool{ + New: func() any { + b := make([]byte, 65*1024) + return &b + }, + }, + }, + } +) + +// Get returns a buffer of specified size. +func Get(size int) *[]byte { + for i := range pools { + if size <= pools[i].size { + b := pools[i].pool.Get().(*[]byte) + *b = (*b)[:size] + return b + } + } + b := make([]byte, size) + return &b +} + +func Put(b *[]byte) { + for i := range pools { + if cap(*b) == pools[i].size { + pools[i].pool.Put(b) + } + } +} diff --git a/common/matcher/matcher.go b/common/matcher/matcher.go new file mode 100644 index 0000000..81442d3 --- /dev/null +++ b/common/matcher/matcher.go @@ -0,0 +1,99 @@ +package matcher + +import ( + "net" + "strings" + + "github.com/gobwas/glob" +) + +// Matcher is a generic pattern matcher, +// it gives the match result of the given pattern for specific v. +type Matcher interface { + Match(v string) bool +} + +// NewMatcher creates a Matcher for the given pattern. +// The acutal Matcher depends on the pattern: +// IP Matcher if pattern is a valid IP address. +// CIDR Matcher if pattern is a valid CIDR address. +// Domain Matcher if both of the above are not. +func NewMatcher(pattern string) Matcher { + if pattern == "" { + return nil + } + if ip := net.ParseIP(pattern); ip != nil { + return IPMatcher(ip) + } + if _, inet, err := net.ParseCIDR(pattern); err == nil { + return CIDRMatcher(inet) + } + return DomainMatcher(pattern) +} + +type ipMatcher struct { + ip net.IP +} + +// IPMatcher creates a Matcher for a specific IP address. +func IPMatcher(ip net.IP) Matcher { + return &ipMatcher{ + ip: ip, + } +} + +func (m *ipMatcher) Match(ip string) bool { + if m == nil { + return false + } + return m.ip.Equal(net.ParseIP(ip)) +} + +type cidrMatcher struct { + ipNet *net.IPNet +} + +// CIDRMatcher creates a Matcher for a specific CIDR notation IP address. +func CIDRMatcher(inet *net.IPNet) Matcher { + return &cidrMatcher{ + ipNet: inet, + } +} + +func (m *cidrMatcher) Match(ip string) bool { + if m == nil || m.ipNet == nil { + return false + } + return m.ipNet.Contains(net.ParseIP(ip)) +} + +type domainMatcher struct { + pattern string + glob glob.Glob +} + +// DomainMatcher creates a Matcher for a specific domain pattern, +// the pattern can be a plain domain such as 'example.com', +// a wildcard such as '*.exmaple.com' or a special wildcard '.example.com'. +func DomainMatcher(pattern string) Matcher { + p := pattern + if strings.HasPrefix(pattern, ".") { + p = pattern[1:] // trim the prefix '.' + pattern = "*" + p + } + return &domainMatcher{ + pattern: p, + glob: glob.MustCompile(pattern), + } +} + +func (m *domainMatcher) Match(domain string) bool { + if m == nil || m.glob == nil { + return false + } + + if domain == m.pattern { + return true + } + return m.glob.Match(domain) +} diff --git a/common/net/dialer/dialer.go b/common/net/dialer/dialer.go new file mode 100644 index 0000000..32a227b --- /dev/null +++ b/common/net/dialer/dialer.go @@ -0,0 +1,144 @@ +package dialer + +import ( + "context" + "fmt" + "net" + "syscall" + "time" + + "github.com/go-gost/core/logger" +) + +var ( + DefaultNetDialer = &NetDialer{ + Timeout: 30 * time.Second, + } +) + +type NetDialer struct { + Interface string + Timeout time.Duration + DialFunc func(ctx context.Context, network, addr string) (net.Conn, error) + Logger logger.Logger +} + +func (d *NetDialer) Dial(ctx context.Context, network, addr string) (net.Conn, error) { + if d == nil { + d = DefaultNetDialer + } + log := d.Logger + if log == nil { + log = logger.Default() + } + + ifceName, ifAddr, err := parseInterfaceAddr(d.Interface, network) + if err != nil { + return nil, err + } + if d.DialFunc != nil { + return d.DialFunc(ctx, network, addr) + } + logger.Default().Infof("interface: %s %v/%s", ifceName, ifAddr, network) + + switch network { + case "udp", "udp4", "udp6": + if addr == "" { + var laddr *net.UDPAddr + if ifAddr != nil { + laddr, _ = ifAddr.(*net.UDPAddr) + } + + return net.ListenUDP(network, laddr) + } + case "tcp", "tcp4", "tcp6": + default: + return nil, fmt.Errorf("dial: unsupported network %s", network) + } + netd := net.Dialer{ + Timeout: d.Timeout, + LocalAddr: ifAddr, + Control: func(network, address string, c syscall.RawConn) error { + var cerr error + err := c.Control(func(fd uintptr) { + cerr = bindDevice(fd, ifceName) + }) + if err != nil { + return err + } + if cerr != nil { + return cerr + } + return nil + }, + } + return netd.DialContext(ctx, network, addr) +} + +func parseInterfaceAddr(ifceName, network string) (ifce string, addr net.Addr, err error) { + if ifceName == "" { + return + } + + ip := net.ParseIP(ifceName) + if ip == nil { + var ife *net.Interface + ife, err = net.InterfaceByName(ifceName) + if err != nil { + return + } + var addrs []net.Addr + addrs, err = ife.Addrs() + if err != nil { + return + } + if len(addrs) == 0 { + err = fmt.Errorf("addr not found for interface %s", ifceName) + return + } + ip = addrs[0].(*net.IPNet).IP + ifce = ifceName + } else { + ifce, err = findInterfaceByIP(ip) + if err != nil { + return + } + } + + port := 0 + switch network { + case "tcp", "tcp4", "tcp6": + addr = &net.TCPAddr{IP: ip, Port: port} + return + case "udp", "udp4", "udp6": + addr = &net.UDPAddr{IP: ip, Port: port} + return + default: + addr = &net.IPAddr{IP: ip} + return + } +} + +func findInterfaceByIP(ip net.IP) (string, error) { + ifces, err := net.Interfaces() + if err != nil { + return "", err + } + for _, ifce := range ifces { + addrs, _ := ifce.Addrs() + if len(addrs) == 0 { + continue + } + for _, addr := range addrs { + ipAddr, _ := addr.(*net.IPNet) + if ipAddr == nil { + continue + } + // logger.Default().Infof("%s-%s", ipAddr, ip) + if ipAddr.IP.Equal(ip) { + return ifce.Name, nil + } + } + } + return "", nil +} diff --git a/common/net/dialer/dialer_linux.go b/common/net/dialer/dialer_linux.go new file mode 100644 index 0000000..cf7f0aa --- /dev/null +++ b/common/net/dialer/dialer_linux.go @@ -0,0 +1,14 @@ +package dialer + +import ( + "golang.org/x/sys/unix" +) + +func bindDevice(fd uintptr, ifceName string) error { + // unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEADDR, 1) + // unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1) + if ifceName == "" { + return nil + } + return unix.BindToDevice(int(fd), ifceName) +} diff --git a/common/net/dialer/dialer_other.go b/common/net/dialer/dialer_other.go new file mode 100644 index 0000000..6ad6b21 --- /dev/null +++ b/common/net/dialer/dialer_other.go @@ -0,0 +1,7 @@ +//go:build !linux + +package dialer + +func bindDevice(fd uintptr, ifceName string) error { + return nil +} diff --git a/common/net/relay/relay.go b/common/net/relay/relay.go new file mode 100644 index 0000000..c5d8493 --- /dev/null +++ b/common/net/relay/relay.go @@ -0,0 +1,126 @@ +package relay + +import ( + "net" + + "github.com/go-gost/core/bypass" + "github.com/go-gost/core/common/bufpool" + "github.com/go-gost/core/logger" +) + +type UDPRelay struct { + pc1 net.PacketConn + pc2 net.PacketConn + + bypass bypass.Bypass + bufferSize int + logger logger.Logger +} + +func NewUDPRelay(pc1, pc2 net.PacketConn) *UDPRelay { + return &UDPRelay{ + pc1: pc1, + pc2: pc2, + } +} + +func (r *UDPRelay) WithBypass(bp bypass.Bypass) *UDPRelay { + r.bypass = bp + return r +} + +func (r *UDPRelay) WithLogger(logger logger.Logger) *UDPRelay { + r.logger = logger + return r +} + +func (r *UDPRelay) SetBufferSize(n int) { + r.bufferSize = n +} + +func (r *UDPRelay) Run() (err error) { + bufSize := r.bufferSize + if bufSize <= 0 { + bufSize = 1024 + } + + errc := make(chan error, 2) + + go func() { + for { + err := func() error { + b := bufpool.Get(bufSize) + defer bufpool.Put(b) + + n, raddr, err := r.pc1.ReadFrom(*b) + if err != nil { + return err + } + + if r.bypass != nil && r.bypass.Contains(raddr.String()) { + if r.logger != nil { + r.logger.Warn("bypass: ", raddr) + } + return nil + } + + if _, err := r.pc2.WriteTo((*b)[:n], raddr); err != nil { + return err + } + + if r.logger != nil { + r.logger.Debugf("%s >>> %s data: %d", + r.pc2.LocalAddr(), raddr, n) + + } + + return nil + }() + + if err != nil { + errc <- err + return + } + } + }() + + go func() { + for { + err := func() error { + b := bufpool.Get(bufSize) + defer bufpool.Put(b) + + n, raddr, err := r.pc2.ReadFrom(*b) + if err != nil { + return err + } + + if r.bypass != nil && r.bypass.Contains(raddr.String()) { + if r.logger != nil { + r.logger.Warn("bypass: ", raddr) + } + return nil + } + + if _, err := r.pc1.WriteTo((*b)[:n], raddr); err != nil { + return err + } + + if r.logger != nil { + r.logger.Debugf("%s <<< %s data: %d", + r.pc2.LocalAddr(), raddr, n) + + } + + return nil + }() + + if err != nil { + errc <- err + return + } + } + }() + + return <-errc +} diff --git a/common/net/transport.go b/common/net/transport.go new file mode 100644 index 0000000..fc58e2c --- /dev/null +++ b/common/net/transport.go @@ -0,0 +1,50 @@ +package net + +import ( + "bufio" + "io" + "net" + + "github.com/go-gost/core/common/bufpool" +) + +func Transport(rw1, rw2 io.ReadWriter) error { + errc := make(chan error, 1) + go func() { + errc <- copyBuffer(rw1, rw2) + }() + + go func() { + errc <- copyBuffer(rw2, rw1) + }() + + err := <-errc + if err != nil && err == io.EOF { + err = nil + } + return err +} + +func copyBuffer(dst io.Writer, src io.Reader) error { + buf := bufpool.Get(16 * 1024) + defer bufpool.Put(buf) + + _, err := io.CopyBuffer(dst, src, *buf) + return err +} + +type bufferReaderConn struct { + net.Conn + br *bufio.Reader +} + +func NewBufferReaderConn(conn net.Conn, br *bufio.Reader) net.Conn { + return &bufferReaderConn{ + Conn: conn, + br: br, + } +} + +func (c *bufferReaderConn) Read(b []byte) (int, error) { + return c.br.Read(b) +} diff --git a/common/net/udp.go b/common/net/udp.go new file mode 100644 index 0000000..515060f --- /dev/null +++ b/common/net/udp.go @@ -0,0 +1,41 @@ +package net + +import ( + "io" + "net" + "syscall" +) + +type UDPConn interface { + net.PacketConn + io.Reader + io.Writer + readUDP + writeUDP + setBuffer + syscallConn + remoteAddr +} + +type setBuffer interface { + SetReadBuffer(bytes int) error + SetWriteBuffer(bytes int) error +} + +type readUDP interface { + ReadFromUDP(b []byte) (n int, addr *net.UDPAddr, err error) + ReadMsgUDP(b, oob []byte) (n, oobn, flags int, addr *net.UDPAddr, err error) +} + +type writeUDP interface { + WriteToUDP(b []byte, addr *net.UDPAddr) (int, error) + WriteMsgUDP(b, oob []byte, addr *net.UDPAddr) (n, oobn int, err error) +} + +type syscallConn interface { + SyscallConn() (syscall.RawConn, error) +} + +type remoteAddr interface { + RemoteAddr() net.Addr +} diff --git a/common/util/resolver/cache.go b/common/util/resolver/cache.go new file mode 100644 index 0000000..177fe2f --- /dev/null +++ b/common/util/resolver/cache.go @@ -0,0 +1,88 @@ +package resolver + +import ( + "fmt" + "sync" + "time" + + "github.com/go-gost/core/logger" + "github.com/miekg/dns" +) + +type CacheKey string + +// NewCacheKey generates resolver cache key from question of dns query. +func NewCacheKey(q *dns.Question) CacheKey { + if q == nil { + return "" + } + key := fmt.Sprintf("%s%s.%s", q.Name, dns.Class(q.Qclass).String(), dns.Type(q.Qtype).String()) + return CacheKey(key) +} + +type cacheItem struct { + msg *dns.Msg + ts time.Time + ttl time.Duration +} + +type Cache struct { + m sync.Map + logger logger.Logger +} + +func NewCache() *Cache { + return &Cache{} +} + +func (c *Cache) WithLogger(logger logger.Logger) *Cache { + c.logger = logger + return c +} + +func (c *Cache) Load(key CacheKey) *dns.Msg { + v, ok := c.m.Load(key) + if !ok { + return nil + } + + item, ok := v.(*cacheItem) + if !ok { + return nil + } + + if time.Since(item.ts) > item.ttl { + c.m.Delete(key) + return nil + } + + c.logger.Debugf("hit resolver cache: %s", key) + + return item.msg.Copy() +} + +func (c *Cache) Store(key CacheKey, mr *dns.Msg, ttl time.Duration) { + if key == "" || mr == nil || ttl < 0 { + return + } + + if ttl == 0 { + for _, answer := range mr.Answer { + v := time.Duration(answer.Header().Ttl) * time.Second + if ttl == 0 || ttl > v { + ttl = v + } + } + } + if ttl == 0 { + ttl = 30 * time.Second + } + + c.m.Store(key, &cacheItem{ + msg: mr.Copy(), + ts: time.Now(), + ttl: ttl, + }) + + c.logger.Debugf("resolver cache store: %s, ttl: %v", key, ttl) +} diff --git a/common/util/resolver/resolver.go b/common/util/resolver/resolver.go new file mode 100644 index 0000000..74ec536 --- /dev/null +++ b/common/util/resolver/resolver.go @@ -0,0 +1,30 @@ +package resolver + +import ( + "net" + + "github.com/miekg/dns" +) + +func AddSubnetOpt(m *dns.Msg, ip net.IP) { + if m == nil || ip == nil { + return + } + + opt := new(dns.OPT) + opt.Hdr.Name = "." + opt.Hdr.Rrtype = dns.TypeOPT + e := new(dns.EDNS0_SUBNET) + e.Code = dns.EDNS0SUBNET + if ip := ip.To4(); ip != nil { + e.Family = 1 + e.SourceNetmask = 24 + e.Address = ip + } else { + e.Family = 2 + e.SourceNetmask = 128 + e.Address = ip.To16() + } + opt.Option = append(opt.Option, e) + m.Extra = append(m.Extra, opt) +} diff --git a/common/util/tls/tls.go b/common/util/tls/tls.go new file mode 100644 index 0000000..8fa5e3e --- /dev/null +++ b/common/util/tls/tls.go @@ -0,0 +1,178 @@ +package tls + +import ( + "crypto/tls" + "crypto/x509" + "errors" + "io/ioutil" + "net" + "time" +) + +var ( + // DefaultConfig is a default TLS config for global use. + DefaultConfig *tls.Config +) + +// LoadServerConfig loads the certificate from cert & key files and optional client CA file. +func LoadServerConfig(certFile, keyFile, caFile string) (*tls.Config, error) { + if certFile == "" && keyFile == "" { + return DefaultConfig.Clone(), nil + } + + cert, err := tls.LoadX509KeyPair(certFile, keyFile) + if err != nil { + return nil, err + } + + cfg := &tls.Config{Certificates: []tls.Certificate{cert}} + + pool, err := loadCA(caFile) + if err != nil { + return nil, err + } + if pool != nil { + cfg.ClientCAs = pool + cfg.ClientAuth = tls.RequireAndVerifyClientCert + } + + return cfg, nil +} + +// LoadClientConfig loads the certificate from cert & key files and optional CA file. +func LoadClientConfig(certFile, keyFile, caFile string, verify bool, serverName string) (*tls.Config, error) { + var cfg *tls.Config + + if certFile == "" && keyFile == "" { + cfg = &tls.Config{} + } else { + cert, err := tls.LoadX509KeyPair(certFile, keyFile) + if err != nil { + return nil, err + } + + cfg = &tls.Config{ + Certificates: []tls.Certificate{cert}, + } + } + + rootCAs, err := loadCA(caFile) + if err != nil { + return nil, err + } + + cfg.RootCAs = rootCAs + cfg.ServerName = serverName + cfg.InsecureSkipVerify = !verify + + // If the root ca is given, but skip verify, we verify the certificate manually. + if cfg.RootCAs != nil && !verify { + cfg.VerifyConnection = func(state tls.ConnectionState) error { + opts := x509.VerifyOptions{ + Roots: cfg.RootCAs, + CurrentTime: time.Now(), + DNSName: "", + Intermediates: x509.NewCertPool(), + } + + certs := state.PeerCertificates + for i, cert := range certs { + if i == 0 { + continue + } + opts.Intermediates.AddCert(cert) + } + + _, err := certs[0].Verify(opts) + return err + } + } + + return cfg, nil +} + +func loadCA(caFile string) (cp *x509.CertPool, err error) { + if caFile == "" { + return + } + cp = x509.NewCertPool() + data, err := ioutil.ReadFile(caFile) + if err != nil { + return nil, err + } + if !cp.AppendCertsFromPEM(data) { + return nil, errors.New("AppendCertsFromPEM failed") + } + return +} + +// Wrap a net.Conn into a client tls connection, performing any +// additional verification as needed. +// +// As of go 1.3, crypto/tls only supports either doing no certificate +// verification, or doing full verification including of the peer's +// DNS name. For consul, we want to validate that the certificate is +// signed by a known CA, but because consul doesn't use DNS names for +// node names, we don't verify the certificate DNS names. Since go 1.3 +// no longer supports this mode of operation, we have to do it +// manually. +// +// This code is taken from consul: +// https://github.com/hashicorp/consul/blob/master/tlsutil/config.go +func WrapTLSClient(conn net.Conn, tlsConfig *tls.Config, timeout time.Duration) (net.Conn, error) { + var err error + var tlsConn *tls.Conn + + if timeout > 0 { + conn.SetDeadline(time.Now().Add(timeout)) + defer conn.SetDeadline(time.Time{}) + } + + tlsConn = tls.Client(conn, tlsConfig) + + // Otherwise perform handshake, but don't verify the domain + // + // The following is lightly-modified from the doFullHandshake + // method in https://golang.org/src/crypto/tls/handshake_client.go + if err = tlsConn.Handshake(); err != nil { + tlsConn.Close() + return nil, err + } + + // We can do this in `tls.Config.VerifyConnection`, which effective for + // other TLS protocols such as WebSocket. See `route.go:parseChainNode` + /* + // If crypto/tls is doing verification, there's no need to do our own. + if tlsConfig.InsecureSkipVerify == false { + return tlsConn, nil + } + + // Similarly if we use host's CA, we can do full handshake + if tlsConfig.RootCAs == nil { + return tlsConn, nil + } + + opts := x509.VerifyOptions{ + Roots: tlsConfig.RootCAs, + CurrentTime: time.Now(), + DNSName: "", + Intermediates: x509.NewCertPool(), + } + + certs := tlsConn.ConnectionState().PeerCertificates + for i, cert := range certs { + if i == 0 { + continue + } + opts.Intermediates.AddCert(cert) + } + + _, err = certs[0].Verify(opts) + if err != nil { + tlsConn.Close() + return nil, err + } + */ + + return tlsConn, err +} diff --git a/common/util/udp/conn.go b/common/util/udp/conn.go new file mode 100644 index 0000000..5884da0 --- /dev/null +++ b/common/util/udp/conn.go @@ -0,0 +1,102 @@ +package udp + +import ( + "errors" + "net" + "sync" + "sync/atomic" + + "github.com/go-gost/core/common/bufpool" +) + +// Conn is a server side connection for UDP client peer, it implements net.Conn and net.PacketConn. +type Conn struct { + net.PacketConn + localAddr net.Addr + remoteAddr net.Addr + rc chan []byte // data receive queue + idle int32 // indicate the connection is idle + closed chan struct{} + closeMutex sync.Mutex +} + +func NewConn(c net.PacketConn, localAddr, remoteAddr net.Addr, queueSize int) *Conn { + return &Conn{ + PacketConn: c, + localAddr: localAddr, + remoteAddr: remoteAddr, + rc: make(chan []byte, queueSize), + closed: make(chan struct{}), + } +} + +func (c *Conn) ReadFrom(b []byte) (n int, addr net.Addr, err error) { + select { + case bb := <-c.rc: + n = copy(b, bb) + c.SetIdle(false) + bufpool.Put(&bb) + + case <-c.closed: + err = net.ErrClosed + return + } + + addr = c.remoteAddr + + return +} + +func (c *Conn) Read(b []byte) (n int, err error) { + n, _, err = c.ReadFrom(b) + return +} + +func (c *Conn) Write(b []byte) (n int, err error) { + return c.WriteTo(b, c.remoteAddr) +} + +func (c *Conn) Close() error { + c.closeMutex.Lock() + defer c.closeMutex.Unlock() + + select { + case <-c.closed: + default: + close(c.closed) + } + return nil +} + +func (c *Conn) LocalAddr() net.Addr { + return c.localAddr +} + +func (c *Conn) RemoteAddr() net.Addr { + return c.remoteAddr +} + +func (c *Conn) IsIdle() bool { + return atomic.LoadInt32(&c.idle) > 0 +} + +func (c *Conn) SetIdle(idle bool) { + v := int32(0) + if idle { + v = 1 + } + atomic.StoreInt32(&c.idle, v) +} + +func (c *Conn) WriteQueue(b []byte) error { + select { + case c.rc <- b: + return nil + + case <-c.closed: + return net.ErrClosed + + default: + return errors.New("recv queue is full") + } +} diff --git a/common/util/udp/listener.go b/common/util/udp/listener.go new file mode 100644 index 0000000..0aee50b --- /dev/null +++ b/common/util/udp/listener.go @@ -0,0 +1,120 @@ +package udp + +import ( + "net" + "sync" + "time" + + "github.com/go-gost/core/common/bufpool" + "github.com/go-gost/core/logger" +) + +type listener struct { + addr net.Addr + conn net.PacketConn + cqueue chan net.Conn + readQueueSize int + readBufferSize int + connPool *ConnPool + mux sync.Mutex + closed chan struct{} + errChan chan error + logger logger.Logger +} + +func NewListener(conn net.PacketConn, addr net.Addr, backlog, dataQueueSize, dataBufferSize int, ttl time.Duration, logger logger.Logger) net.Listener { + ln := &listener{ + conn: conn, + addr: addr, + cqueue: make(chan net.Conn, backlog), + connPool: NewConnPool(ttl).WithLogger(logger), + readQueueSize: dataQueueSize, + readBufferSize: dataBufferSize, + closed: make(chan struct{}), + errChan: make(chan error, 1), + logger: logger, + } + go ln.listenLoop() + + return ln +} + +func (ln *listener) Accept() (conn net.Conn, err error) { + select { + case conn = <-ln.cqueue: + return + case <-ln.closed: + return nil, net.ErrClosed + case err = <-ln.errChan: + if err == nil { + err = net.ErrClosed + } + return + } +} + +func (ln *listener) listenLoop() { + for { + select { + case <-ln.closed: + return + default: + } + + b := bufpool.Get(ln.readBufferSize) + + n, raddr, err := ln.conn.ReadFrom(*b) + if err != nil { + ln.errChan <- err + close(ln.errChan) + return + } + + c := ln.getConn(raddr) + if c == nil { + bufpool.Put(b) + continue + } + + if err := c.WriteQueue((*b)[:n]); err != nil { + ln.logger.Warn("data discarded: ", err) + } + } +} + +func (ln *listener) Addr() net.Addr { + return ln.addr +} + +func (ln *listener) Close() error { + select { + case <-ln.closed: + default: + close(ln.closed) + ln.conn.Close() + ln.connPool.Close() + } + + return nil +} + +func (ln *listener) getConn(raddr net.Addr) *Conn { + ln.mux.Lock() + defer ln.mux.Unlock() + + c, ok := ln.connPool.Get(raddr.String()) + if ok { + return c + } + + c = NewConn(ln.conn, ln.addr, raddr, ln.readQueueSize) + select { + case ln.cqueue <- c: + ln.connPool.Set(raddr.String(), c) + return c + default: + c.Close() + ln.logger.Warnf("connection queue is full, client %s discarded", raddr) + return nil + } +} diff --git a/common/util/udp/pool.go b/common/util/udp/pool.go new file mode 100644 index 0000000..7048227 --- /dev/null +++ b/common/util/udp/pool.go @@ -0,0 +1,100 @@ +package udp + +import ( + "sync" + "time" + + "github.com/go-gost/core/logger" +) + +type ConnPool struct { + m sync.Map + ttl time.Duration + closed chan struct{} + logger logger.Logger +} + +func NewConnPool(ttl time.Duration) *ConnPool { + p := &ConnPool{ + ttl: ttl, + closed: make(chan struct{}), + } + go p.idleCheck() + return p +} + +func (p *ConnPool) WithLogger(logger logger.Logger) *ConnPool { + p.logger = logger + return p +} + +func (p *ConnPool) Get(key any) (c *Conn, ok bool) { + v, ok := p.m.Load(key) + if ok { + c, ok = v.(*Conn) + } + return +} + +func (p *ConnPool) Set(key any, c *Conn) { + p.m.Store(key, c) +} + +func (p *ConnPool) Delete(key any) { + p.m.Delete(key) +} + +func (p *ConnPool) Close() { + select { + case <-p.closed: + return + default: + } + + close(p.closed) + + p.m.Range(func(k, v any) bool { + if c, ok := v.(*Conn); ok && c != nil { + c.Close() + } + return true + }) +} + +func (p *ConnPool) idleCheck() { + ticker := time.NewTicker(p.ttl) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + size := 0 + idles := 0 + p.m.Range(func(key, value any) bool { + c, ok := value.(*Conn) + if !ok || c == nil { + p.Delete(key) + return true + } + size++ + + if c.IsIdle() { + idles++ + p.Delete(key) + c.Close() + return true + } + + c.SetIdle(true) + + return true + }) + + if idles > 0 { + p.logger.Debugf("connection pool: size=%d, idle=%d", size, idles) + } + case <-p.closed: + return + } + } +} diff --git a/connector/binder.go b/connector/binder.go new file mode 100644 index 0000000..8af8939 --- /dev/null +++ b/connector/binder.go @@ -0,0 +1,15 @@ +package connector + +import ( + "context" + "errors" + "net" +) + +var ( + ErrBindUnsupported = errors.New("bind unsupported") +) + +type Binder interface { + Bind(ctx context.Context, conn net.Conn, network, address string, opts ...BindOption) (net.Listener, error) +} diff --git a/connector/connector.go b/connector/connector.go new file mode 100644 index 0000000..235f621 --- /dev/null +++ b/connector/connector.go @@ -0,0 +1,18 @@ +package connector + +import ( + "context" + "net" + + "github.com/go-gost/core/metadata" +) + +// Connector is responsible for connecting to the destination address. +type Connector interface { + Init(metadata.Metadata) error + Connect(ctx context.Context, conn net.Conn, network, address string, opts ...ConnectOption) (net.Conn, error) +} + +type Handshaker interface { + Handshake(ctx context.Context, conn net.Conn) (net.Conn, error) +} diff --git a/connector/forward/connector.go b/connector/forward/connector.go new file mode 100644 index 0000000..220ca6f --- /dev/null +++ b/connector/forward/connector.go @@ -0,0 +1,45 @@ +package forward + +import ( + "context" + "net" + + "github.com/go-gost/core/connector" + md "github.com/go-gost/core/metadata" + "github.com/go-gost/core/registry" +) + +func init() { + registry.ConnectorRegistry().Register("forward", NewConnector) +} + +type forwardConnector struct { + options connector.Options +} + +func NewConnector(opts ...connector.Option) connector.Connector { + options := connector.Options{} + for _, opt := range opts { + opt(&options) + } + + return &forwardConnector{ + options: options, + } +} + +func (c *forwardConnector) Init(md md.Metadata) (err error) { + return nil +} + +func (c *forwardConnector) Connect(ctx context.Context, conn net.Conn, network, address string, opts ...connector.ConnectOption) (net.Conn, error) { + log := c.options.Logger.WithFields(map[string]any{ + "remote": conn.RemoteAddr().String(), + "local": conn.LocalAddr().String(), + "network": network, + "address": address, + }) + log.Infof("connect %s/%s", address, network) + + return conn, nil +} diff --git a/connector/http/connector.go b/connector/http/connector.go new file mode 100644 index 0000000..5b6120e --- /dev/null +++ b/connector/http/connector.go @@ -0,0 +1,129 @@ +package http + +import ( + "bufio" + "context" + "encoding/base64" + "fmt" + "net" + "net/http" + "net/http/httputil" + "net/url" + "time" + + "github.com/go-gost/core/connector" + "github.com/go-gost/core/internal/util/socks" + "github.com/go-gost/core/logger" + md "github.com/go-gost/core/metadata" + "github.com/go-gost/core/registry" +) + +func init() { + registry.ConnectorRegistry().Register("http", NewConnector) +} + +type httpConnector struct { + md metadata + options connector.Options +} + +func NewConnector(opts ...connector.Option) connector.Connector { + options := connector.Options{} + for _, opt := range opts { + opt(&options) + } + + return &httpConnector{ + options: options, + } +} + +func (c *httpConnector) Init(md md.Metadata) (err error) { + return c.parseMetadata(md) +} + +func (c *httpConnector) Connect(ctx context.Context, conn net.Conn, network, address string, opts ...connector.ConnectOption) (net.Conn, error) { + log := c.options.Logger.WithFields(map[string]any{ + "local": conn.LocalAddr().String(), + "remote": conn.RemoteAddr().String(), + "network": network, + "address": address, + }) + log.Infof("connect %s/%s", address, network) + + req := &http.Request{ + Method: http.MethodConnect, + URL: &url.URL{Host: address}, + Host: address, + ProtoMajor: 1, + ProtoMinor: 1, + Header: c.md.header, + } + + if req.Header == nil { + req.Header = http.Header{} + } + req.Header.Set("Proxy-Connection", "keep-alive") + + if user := c.options.Auth; user != nil { + u := user.Username() + p, _ := user.Password() + req.Header.Set("Proxy-Authorization", + "Basic "+base64.StdEncoding.EncodeToString([]byte(u+":"+p))) + } + + switch network { + case "tcp", "tcp4", "tcp6": + if _, ok := conn.(net.PacketConn); ok { + err := fmt.Errorf("tcp over udp is unsupported") + log.Error(err) + return nil, err + } + case "udp", "udp4", "udp6": + req.Header.Set("X-Gost-Protocol", "udp") + default: + err := fmt.Errorf("network %s is unsupported", network) + log.Error(err) + return nil, err + } + + if log.IsLevelEnabled(logger.DebugLevel) { + dump, _ := httputil.DumpRequest(req, false) + log.Debug(string(dump)) + } + + if c.md.connectTimeout > 0 { + conn.SetDeadline(time.Now().Add(c.md.connectTimeout)) + defer conn.SetDeadline(time.Time{}) + } + + req = req.WithContext(ctx) + if err := req.Write(conn); err != nil { + return nil, err + } + + resp, err := http.ReadResponse(bufio.NewReader(conn), req) + if err != nil { + return nil, err + } + // NOTE: the server may return `Transfer-Encoding: chunked` header, + // then the Content-Length of response will be unknown (-1), + // in this case, close body will be blocked, so we leave it untouched. + // defer resp.Body.Close() + + if log.IsLevelEnabled(logger.DebugLevel) { + dump, _ := httputil.DumpResponse(resp, false) + log.Debug(string(dump)) + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("%s", resp.Status) + } + + if network == "udp" { + addr, _ := net.ResolveUDPAddr(network, address) + return socks.UDPTunClientConn(conn, addr), nil + } + + return conn, nil +} diff --git a/connector/http/metadata.go b/connector/http/metadata.go new file mode 100644 index 0000000..16db7e9 --- /dev/null +++ b/connector/http/metadata.go @@ -0,0 +1,32 @@ +package http + +import ( + "net/http" + "time" + + mdata "github.com/go-gost/core/metadata" +) + +type metadata struct { + connectTimeout time.Duration + header http.Header +} + +func (c *httpConnector) parseMetadata(md mdata.Metadata) (err error) { + const ( + connectTimeout = "timeout" + header = "header" + ) + + c.md.connectTimeout = mdata.GetDuration(md, connectTimeout) + + if mm := mdata.GetStringMapString(md, header); len(mm) > 0 { + hd := http.Header{} + for k, v := range mm { + hd.Add(k, v) + } + c.md.header = hd + } + + return +} diff --git a/connector/option.go b/connector/option.go new file mode 100644 index 0000000..3a3ce06 --- /dev/null +++ b/connector/option.go @@ -0,0 +1,80 @@ +package connector + +import ( + "crypto/tls" + "net/url" + "time" + + "github.com/go-gost/core/logger" +) + +type Options struct { + Auth *url.Userinfo + TLSConfig *tls.Config + Logger logger.Logger +} + +type Option func(opts *Options) + +func AuthOption(auth *url.Userinfo) Option { + return func(opts *Options) { + opts.Auth = auth + } +} + +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 ConnectOptions struct { +} + +type ConnectOption func(opts *ConnectOptions) + +type BindOptions struct { + Mux bool + Backlog int + UDPDataQueueSize int + UDPDataBufferSize int + UDPConnTTL time.Duration +} + +type BindOption func(opts *BindOptions) + +func MuxBindOption(mux bool) BindOption { + return func(opts *BindOptions) { + opts.Mux = mux + } +} + +func BacklogBindOption(backlog int) BindOption { + return func(opts *BindOptions) { + opts.Backlog = backlog + } +} + +func UDPDataQueueSizeBindOption(size int) BindOption { + return func(opts *BindOptions) { + opts.UDPDataQueueSize = size + } +} + +func UDPDataBufferSizeBindOption(size int) BindOption { + return func(opts *BindOptions) { + opts.UDPDataBufferSize = size + } +} + +func UDPConnTTLBindOption(ttl time.Duration) BindOption { + return func(opts *BindOptions) { + opts.UDPConnTTL = ttl + } +} diff --git a/connector/socks/v4/connector.go b/connector/socks/v4/connector.go new file mode 100644 index 0000000..f18427c --- /dev/null +++ b/connector/socks/v4/connector.go @@ -0,0 +1,123 @@ +package v4 + +import ( + "context" + "errors" + "fmt" + "net" + "strconv" + "time" + + "github.com/go-gost/core/connector" + md "github.com/go-gost/core/metadata" + "github.com/go-gost/core/registry" + "github.com/go-gost/gosocks4" +) + +func init() { + registry.ConnectorRegistry().Register("socks4", NewConnector) + registry.ConnectorRegistry().Register("socks4a", NewConnector) +} + +type socks4Connector struct { + md metadata + options connector.Options +} + +func NewConnector(opts ...connector.Option) connector.Connector { + options := connector.Options{} + for _, opt := range opts { + opt(&options) + } + + return &socks4Connector{ + options: options, + } +} + +func (c *socks4Connector) Init(md md.Metadata) (err error) { + return c.parseMetadata(md) +} + +func (c *socks4Connector) Connect(ctx context.Context, conn net.Conn, network, address string, opts ...connector.ConnectOption) (net.Conn, error) { + log := c.options.Logger.WithFields(map[string]any{ + "remote": conn.RemoteAddr().String(), + "local": conn.LocalAddr().String(), + "network": network, + "address": address, + }) + log.Infof("connect %s/%s", address, network) + + switch network { + case "tcp", "tcp4", "tcp6": + if _, ok := conn.(net.PacketConn); ok { + err := fmt.Errorf("tcp over udp is unsupported") + log.Error(err) + return nil, err + } + default: + err := fmt.Errorf("network %s is unsupported", network) + log.Error(err) + return nil, err + } + + var addr *gosocks4.Addr + + if c.md.disable4a { + taddr, err := net.ResolveTCPAddr("tcp4", address) + if err != nil { + log.Error("resolve: ", err) + return nil, err + } + if len(taddr.IP) == 0 { + taddr.IP = net.IPv4zero + } + addr = &gosocks4.Addr{ + Type: gosocks4.AddrIPv4, + Host: taddr.IP.String(), + Port: uint16(taddr.Port), + } + } else { + host, port, err := net.SplitHostPort(address) + if err != nil { + return nil, err + } + p, _ := strconv.Atoi(port) + addr = &gosocks4.Addr{ + Type: gosocks4.AddrDomain, + Host: host, + Port: uint16(p), + } + } + + if c.md.connectTimeout > 0 { + conn.SetDeadline(time.Now().Add(c.md.connectTimeout)) + defer conn.SetDeadline(time.Time{}) + } + + var userid []byte + if c.options.Auth != nil { + userid = []byte(c.options.Auth.Username()) + } + req := gosocks4.NewRequest(gosocks4.CmdConnect, addr, userid) + if err := req.Write(conn); err != nil { + log.Error(err) + return nil, err + } + log.Debug(req) + + reply, err := gosocks4.ReadReply(conn) + if err != nil { + log.Error(err) + return nil, err + } + log.Debug(reply) + + if reply.Code != gosocks4.Granted { + err = errors.New("host unreachable") + log.Error(err) + return nil, err + } + + return conn, nil +} diff --git a/connector/socks/v4/metadata.go b/connector/socks/v4/metadata.go new file mode 100644 index 0000000..da17e76 --- /dev/null +++ b/connector/socks/v4/metadata.go @@ -0,0 +1,24 @@ +package v4 + +import ( + "time" + + mdata "github.com/go-gost/core/metadata" +) + +type metadata struct { + connectTimeout time.Duration + disable4a bool +} + +func (c *socks4Connector) parseMetadata(md mdata.Metadata) (err error) { + const ( + connectTimeout = "timeout" + disable4a = "disable4a" + ) + + c.md.connectTimeout = mdata.GetDuration(md, connectTimeout) + c.md.disable4a = mdata.GetBool(md, disable4a) + + return +} diff --git a/connector/socks/v5/bind.go b/connector/socks/v5/bind.go new file mode 100644 index 0000000..7d28764 --- /dev/null +++ b/connector/socks/v5/bind.go @@ -0,0 +1,130 @@ +package v5 + +import ( + "context" + "fmt" + "net" + + "github.com/go-gost/core/common/util/udp" + "github.com/go-gost/core/connector" + "github.com/go-gost/core/internal/util/mux" + "github.com/go-gost/core/internal/util/socks" + "github.com/go-gost/core/logger" + "github.com/go-gost/gosocks5" +) + +// Bind implements connector.Binder. +func (c *socks5Connector) Bind(ctx context.Context, conn net.Conn, network, address string, opts ...connector.BindOption) (net.Listener, error) { + log := c.options.Logger.WithFields(map[string]any{ + "remote": conn.RemoteAddr().String(), + "local": conn.LocalAddr().String(), + "network": network, + "address": address, + }) + log.Infof("bind on %s/%s", address, network) + + options := connector.BindOptions{} + for _, opt := range opts { + opt(&options) + } + + switch network { + case "tcp", "tcp4", "tcp6": + if options.Mux { + return c.muxBindTCP(ctx, conn, network, address, log) + } + return c.bindTCP(ctx, conn, network, address, log) + case "udp", "udp4", "udp6": + return c.bindUDP(ctx, conn, network, address, &options, log) + default: + err := fmt.Errorf("network %s is unsupported", network) + log.Error(err) + return nil, err + } +} + +func (c *socks5Connector) bindTCP(ctx context.Context, conn net.Conn, network, address string, log logger.Logger) (net.Listener, error) { + laddr, err := c.bind(conn, gosocks5.CmdBind, network, address, log) + if err != nil { + return nil, err + } + + return &tcpListener{ + addr: laddr, + conn: conn, + logger: log, + }, nil +} + +func (c *socks5Connector) muxBindTCP(ctx context.Context, conn net.Conn, network, address string, log logger.Logger) (net.Listener, error) { + laddr, err := c.bind(conn, socks.CmdMuxBind, network, address, log) + if err != nil { + return nil, err + } + + session, err := mux.ServerSession(conn) + if err != nil { + return nil, err + } + + return &tcpMuxListener{ + addr: laddr, + session: session, + logger: log, + }, nil +} + +func (c *socks5Connector) bindUDP(ctx context.Context, conn net.Conn, network, address string, opts *connector.BindOptions, log logger.Logger) (net.Listener, error) { + laddr, err := c.bind(conn, socks.CmdUDPTun, network, address, log) + if err != nil { + return nil, err + } + + ln := udp.NewListener( + socks.UDPTunClientPacketConn(conn), + laddr, + opts.Backlog, + opts.UDPDataQueueSize, opts.UDPDataBufferSize, + opts.UDPConnTTL, + log) + + return ln, nil +} + +func (l *socks5Connector) bind(conn net.Conn, cmd uint8, network, address string, log logger.Logger) (net.Addr, error) { + addr := gosocks5.Addr{} + addr.ParseFrom(address) + req := gosocks5.NewRequest(cmd, &addr) + if err := req.Write(conn); err != nil { + return nil, err + } + log.Debug(req) + + // first reply, bind status + reply, err := gosocks5.ReadReply(conn) + if err != nil { + return nil, err + } + + log.Debug(reply) + + if reply.Rep != gosocks5.Succeeded { + return nil, fmt.Errorf("bind on %s/%s failed", address, network) + } + + var baddr net.Addr + switch network { + case "tcp", "tcp4", "tcp6": + baddr, err = net.ResolveTCPAddr(network, reply.Addr.String()) + case "udp", "udp4", "udp6": + baddr, err = net.ResolveUDPAddr(network, reply.Addr.String()) + default: + err = fmt.Errorf("unknown network %s", network) + } + if err != nil { + return nil, err + } + log.Debugf("bind on %s/%s OK", baddr, baddr.Network()) + + return baddr, nil +} diff --git a/connector/socks/v5/conn.go b/connector/socks/v5/conn.go new file mode 100644 index 0000000..d11f3b5 --- /dev/null +++ b/connector/socks/v5/conn.go @@ -0,0 +1,17 @@ +package v5 + +import "net" + +type bindConn struct { + net.Conn + localAddr net.Addr + remoteAddr net.Addr +} + +func (c *bindConn) LocalAddr() net.Addr { + return c.localAddr +} + +func (c *bindConn) RemoteAddr() net.Addr { + return c.remoteAddr +} diff --git a/connector/socks/v5/connector.go b/connector/socks/v5/connector.go new file mode 100644 index 0000000..ab06af4 --- /dev/null +++ b/connector/socks/v5/connector.go @@ -0,0 +1,173 @@ +package v5 + +import ( + "context" + "crypto/tls" + "errors" + "fmt" + "net" + "time" + + "github.com/go-gost/core/connector" + "github.com/go-gost/core/internal/util/socks" + "github.com/go-gost/core/logger" + md "github.com/go-gost/core/metadata" + "github.com/go-gost/core/registry" + "github.com/go-gost/gosocks5" +) + +func init() { + registry.ConnectorRegistry().Register("socks5", NewConnector) + registry.ConnectorRegistry().Register("socks", NewConnector) +} + +type socks5Connector struct { + selector gosocks5.Selector + md metadata + options connector.Options +} + +func NewConnector(opts ...connector.Option) connector.Connector { + options := connector.Options{} + for _, opt := range opts { + opt(&options) + } + + return &socks5Connector{ + options: options, + } +} + +func (c *socks5Connector) Init(md md.Metadata) (err error) { + if err = c.parseMetadata(md); err != nil { + return + } + + selector := &clientSelector{ + methods: []uint8{ + gosocks5.MethodNoAuth, + gosocks5.MethodUserPass, + }, + User: c.options.Auth, + TLSConfig: c.options.TLSConfig, + logger: c.options.Logger, + } + if !c.md.noTLS { + selector.methods = append(selector.methods, socks.MethodTLS) + if selector.TLSConfig == nil { + selector.TLSConfig = &tls.Config{ + InsecureSkipVerify: true, + } + } + } + c.selector = selector + + return +} + +// Handshake implements connector.Handshaker. +func (c *socks5Connector) Handshake(ctx context.Context, conn net.Conn) (net.Conn, error) { + log := c.options.Logger.WithFields(map[string]any{ + "remote": conn.RemoteAddr().String(), + "local": conn.LocalAddr().String(), + }) + + if c.md.connectTimeout > 0 { + conn.SetDeadline(time.Now().Add(c.md.connectTimeout)) + defer conn.SetDeadline(time.Time{}) + } + + cc := gosocks5.ClientConn(conn, c.selector) + if err := cc.Handleshake(); err != nil { + log.Error(err) + return nil, err + } + + return cc, nil +} + +func (c *socks5Connector) Connect(ctx context.Context, conn net.Conn, network, address string, opts ...connector.ConnectOption) (net.Conn, error) { + log := c.options.Logger.WithFields(map[string]any{ + "remote": conn.RemoteAddr().String(), + "local": conn.LocalAddr().String(), + "network": network, + "address": address, + }) + log.Infof("connect %s/%s", address, network) + + if c.md.connectTimeout > 0 { + conn.SetDeadline(time.Now().Add(c.md.connectTimeout)) + defer conn.SetDeadline(time.Time{}) + } + + switch network { + case "udp", "udp4", "udp6": + return c.connectUDP(ctx, conn, network, address, log) + case "tcp", "tcp4", "tcp6": + if _, ok := conn.(net.PacketConn); ok { + err := fmt.Errorf("tcp over udp is unsupported") + log.Error(err) + return nil, err + } + default: + err := fmt.Errorf("network %s is unsupported", network) + log.Error(err) + return nil, err + } + + addr := gosocks5.Addr{} + if err := addr.ParseFrom(address); err != nil { + log.Error(err) + return nil, err + } + + req := gosocks5.NewRequest(gosocks5.CmdConnect, &addr) + if err := req.Write(conn); err != nil { + log.Error(err) + return nil, err + } + log.Debug(req) + + reply, err := gosocks5.ReadReply(conn) + if err != nil { + log.Error(err) + return nil, err + } + log.Debug(reply) + + if reply.Rep != gosocks5.Succeeded { + err = errors.New("host unreachable") + log.Error(err) + return nil, err + } + + return conn, nil +} + +func (c *socks5Connector) connectUDP(ctx context.Context, conn net.Conn, network, address string, log logger.Logger) (net.Conn, error) { + addr, err := net.ResolveUDPAddr(network, address) + if err != nil { + log.Error(err) + return nil, err + } + + req := gosocks5.NewRequest(socks.CmdUDPTun, nil) + if err := req.Write(conn); err != nil { + log.Error(err) + return nil, err + } + log.Debug(req) + + reply, err := gosocks5.ReadReply(conn) + if err != nil { + log.Error(err) + return nil, err + } + log.Debug(reply) + + if reply.Rep != gosocks5.Succeeded { + return nil, errors.New("get socks5 UDP tunnel failure") + } + + return socks.UDPTunClientConn(conn, addr), nil +} diff --git a/connector/socks/v5/listener.go b/connector/socks/v5/listener.go new file mode 100644 index 0000000..000dc88 --- /dev/null +++ b/connector/socks/v5/listener.go @@ -0,0 +1,102 @@ +package v5 + +import ( + "fmt" + "net" + + "github.com/go-gost/core/internal/util/mux" + "github.com/go-gost/core/logger" + "github.com/go-gost/gosocks5" +) + +type tcpListener struct { + addr net.Addr + conn net.Conn + logger logger.Logger +} + +func (p *tcpListener) Accept() (net.Conn, error) { + // second reply, peer connected + rep, err := gosocks5.ReadReply(p.conn) + if err != nil { + return nil, err + } + p.logger.Debug(rep) + + if rep.Rep != gosocks5.Succeeded { + return nil, fmt.Errorf("peer connect failed") + } + + raddr, err := net.ResolveTCPAddr("tcp", rep.Addr.String()) + if err != nil { + return nil, err + } + + return &bindConn{ + Conn: p.conn, + localAddr: p.addr, + remoteAddr: raddr, + }, nil +} + +func (p *tcpListener) Addr() net.Addr { + return p.addr +} + +func (p *tcpListener) Close() error { + return p.conn.Close() +} + +type tcpMuxListener struct { + addr net.Addr + session *mux.Session + logger logger.Logger +} + +func (p *tcpMuxListener) Accept() (net.Conn, error) { + cc, err := p.session.Accept() + if err != nil { + return nil, err + } + + conn, err := p.getPeerConn(cc) + if err != nil { + cc.Close() + return nil, err + } + + return conn, nil +} + +func (p *tcpMuxListener) getPeerConn(conn net.Conn) (net.Conn, error) { + // second reply, peer connected + rep, err := gosocks5.ReadReply(conn) + if err != nil { + return nil, err + } + p.logger.Debug(rep) + + if rep.Rep != gosocks5.Succeeded { + err = fmt.Errorf("peer connect failed") + return nil, err + } + + raddr, err := net.ResolveTCPAddr("tcp", rep.Addr.String()) + if err != nil { + return nil, err + } + + return &bindConn{ + Conn: conn, + localAddr: p.addr, + remoteAddr: raddr, + }, nil +} + +func (p *tcpMuxListener) Addr() net.Addr { + return p.addr +} + +func (p *tcpMuxListener) Close() error { + return p.session.Close() +} diff --git a/connector/socks/v5/metadata.go b/connector/socks/v5/metadata.go new file mode 100644 index 0000000..59cd528 --- /dev/null +++ b/connector/socks/v5/metadata.go @@ -0,0 +1,24 @@ +package v5 + +import ( + "time" + + mdata "github.com/go-gost/core/metadata" +) + +type metadata struct { + connectTimeout time.Duration + noTLS bool +} + +func (c *socks5Connector) parseMetadata(md mdata.Metadata) (err error) { + const ( + connectTimeout = "timeout" + noTLS = "notls" + ) + + c.md.connectTimeout = mdata.GetDuration(md, connectTimeout) + c.md.noTLS = mdata.GetBool(md, noTLS) + + return +} diff --git a/connector/socks/v5/selector.go b/connector/socks/v5/selector.go new file mode 100644 index 0000000..a51eb98 --- /dev/null +++ b/connector/socks/v5/selector.go @@ -0,0 +1,73 @@ +package v5 + +import ( + "crypto/tls" + "net" + "net/url" + + "github.com/go-gost/core/internal/util/socks" + "github.com/go-gost/core/logger" + "github.com/go-gost/gosocks5" +) + +type clientSelector struct { + methods []uint8 + User *url.Userinfo + TLSConfig *tls.Config + logger logger.Logger +} + +func (s *clientSelector) Methods() []uint8 { + s.logger.Debug("methods: ", s.methods) + return s.methods +} + +func (s *clientSelector) AddMethod(methods ...uint8) { + s.methods = append(s.methods, methods...) +} + +func (s *clientSelector) Select(methods ...uint8) (method uint8) { + return +} + +func (s *clientSelector) OnSelected(method uint8, conn net.Conn) (net.Conn, error) { + s.logger.Debug("method selected: ", method) + + switch method { + case socks.MethodTLS: + conn = tls.Client(conn, s.TLSConfig) + + case gosocks5.MethodUserPass, socks.MethodTLSAuth: + if method == socks.MethodTLSAuth { + conn = tls.Client(conn, s.TLSConfig) + } + + var username, password string + if s.User != nil { + username = s.User.Username() + password, _ = s.User.Password() + } + + req := gosocks5.NewUserPassRequest(gosocks5.UserPassVer, username, password) + if err := req.Write(conn); err != nil { + s.logger.Error(err) + return nil, err + } + s.logger.Debug(req) + + resp, err := gosocks5.ReadUserPassResponse(conn) + if err != nil { + s.logger.Error(err) + return nil, err + } + s.logger.Debug(resp) + + if resp.Status != gosocks5.Succeeded { + return nil, gosocks5.ErrAuthFailure + } + case gosocks5.MethodNoAcceptable: + return nil, gosocks5.ErrBadMethod + } + + return conn, nil +} diff --git a/dialer/dialer.go b/dialer/dialer.go new file mode 100644 index 0000000..9ae0642 --- /dev/null +++ b/dialer/dialer.go @@ -0,0 +1,22 @@ +package dialer + +import ( + "context" + "net" + + "github.com/go-gost/core/metadata" +) + +// Transporter is responsible for dialing to the proxy server. +type Dialer interface { + Init(metadata.Metadata) error + Dial(ctx context.Context, addr string, opts ...DialOption) (net.Conn, error) +} + +type Handshaker interface { + Handshake(ctx context.Context, conn net.Conn, opts ...HandshakeOption) (net.Conn, error) +} + +type Multiplexer interface { + Multiplex() bool +} diff --git a/dialer/option.go b/dialer/option.go new file mode 100644 index 0000000..516c441 --- /dev/null +++ b/dialer/option.go @@ -0,0 +1,66 @@ +package dialer + +import ( + "crypto/tls" + "net/url" + + "github.com/go-gost/core/common/net/dialer" + "github.com/go-gost/core/logger" +) + +type Options struct { + Auth *url.Userinfo + TLSConfig *tls.Config + Logger logger.Logger +} + +type Option func(opts *Options) + +func AuthOption(auth *url.Userinfo) Option { + return func(opts *Options) { + opts.Auth = auth + } +} + +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 DialOptions struct { + Host string + NetDialer *dialer.NetDialer +} + +type DialOption func(opts *DialOptions) + +func HostDialOption(host string) DialOption { + return func(opts *DialOptions) { + opts.Host = host + } +} + +func NetDialerDialOption(netd *dialer.NetDialer) DialOption { + return func(opts *DialOptions) { + opts.NetDialer = netd + } +} + +type HandshakeOptions struct { + Addr string +} + +type HandshakeOption func(opts *HandshakeOptions) + +func AddrHandshakeOption(addr string) HandshakeOption { + return func(opts *HandshakeOptions) { + opts.Addr = addr + } +} diff --git a/dialer/tcp/dialer.go b/dialer/tcp/dialer.go new file mode 100644 index 0000000..fff87b1 --- /dev/null +++ b/dialer/tcp/dialer.go @@ -0,0 +1,48 @@ +package tcp + +import ( + "context" + "net" + + "github.com/go-gost/core/dialer" + "github.com/go-gost/core/logger" + md "github.com/go-gost/core/metadata" + "github.com/go-gost/core/registry" +) + +func init() { + registry.DialerRegistry().Register("tcp", NewDialer) +} + +type tcpDialer struct { + md metadata + logger logger.Logger +} + +func NewDialer(opts ...dialer.Option) dialer.Dialer { + options := &dialer.Options{} + for _, opt := range opts { + opt(options) + } + + return &tcpDialer{ + logger: options.Logger, + } +} + +func (d *tcpDialer) Init(md md.Metadata) (err error) { + return d.parseMetadata(md) +} + +func (d *tcpDialer) Dial(ctx context.Context, addr string, opts ...dialer.DialOption) (net.Conn, error) { + var options dialer.DialOptions + for _, opt := range opts { + opt(&options) + } + + conn, err := options.NetDialer.Dial(ctx, "tcp", addr) + if err != nil { + d.logger.Error(err) + } + return conn, err +} diff --git a/dialer/tcp/metadata.go b/dialer/tcp/metadata.go new file mode 100644 index 0000000..34e3789 --- /dev/null +++ b/dialer/tcp/metadata.go @@ -0,0 +1,23 @@ +package tcp + +import ( + "time" + + md "github.com/go-gost/core/metadata" +) + +const ( + dialTimeout = "dialTimeout" +) + +const ( + defaultDialTimeout = 5 * time.Second +) + +type metadata struct { + dialTimeout time.Duration +} + +func (d *tcpDialer) parseMetadata(md md.Metadata) (err error) { + return +} diff --git a/dialer/tls/dialer.go b/dialer/tls/dialer.go new file mode 100644 index 0000000..8d29e96 --- /dev/null +++ b/dialer/tls/dialer.go @@ -0,0 +1,68 @@ +package tls + +import ( + "context" + "crypto/tls" + "net" + "time" + + "github.com/go-gost/core/dialer" + "github.com/go-gost/core/logger" + md "github.com/go-gost/core/metadata" + "github.com/go-gost/core/registry" +) + +func init() { + registry.DialerRegistry().Register("tls", NewDialer) +} + +type tlsDialer struct { + md metadata + logger logger.Logger + options dialer.Options +} + +func NewDialer(opts ...dialer.Option) dialer.Dialer { + options := dialer.Options{} + for _, opt := range opts { + opt(&options) + } + + return &tlsDialer{ + logger: options.Logger, + options: options, + } +} + +func (d *tlsDialer) Init(md md.Metadata) (err error) { + return d.parseMetadata(md) +} + +func (d *tlsDialer) Dial(ctx context.Context, addr string, opts ...dialer.DialOption) (net.Conn, error) { + var options dialer.DialOptions + for _, opt := range opts { + opt(&options) + } + + conn, err := options.NetDialer.Dial(ctx, "tcp", addr) + if err != nil { + d.logger.Error(err) + } + return conn, err +} + +// Handshake implements dialer.Handshaker +func (d *tlsDialer) Handshake(ctx context.Context, conn net.Conn, options ...dialer.HandshakeOption) (net.Conn, error) { + if d.md.handshakeTimeout > 0 { + conn.SetDeadline(time.Now().Add(d.md.handshakeTimeout)) + defer conn.SetDeadline(time.Time{}) + } + + tlsConn := tls.Client(conn, d.options.TLSConfig) + if err := tlsConn.HandshakeContext(ctx); err != nil { + conn.Close() + return nil, err + } + + return tlsConn, nil +} diff --git a/dialer/tls/metadata.go b/dialer/tls/metadata.go new file mode 100644 index 0000000..e75c270 --- /dev/null +++ b/dialer/tls/metadata.go @@ -0,0 +1,21 @@ +package tls + +import ( + "time" + + mdata "github.com/go-gost/core/metadata" +) + +type metadata struct { + handshakeTimeout time.Duration +} + +func (d *tlsDialer) parseMetadata(md mdata.Metadata) (err error) { + const ( + handshakeTimeout = "handshakeTimeout" + ) + + d.md.handshakeTimeout = mdata.GetDuration(md, handshakeTimeout) + + return +} diff --git a/dialer/udp/conn.go b/dialer/udp/conn.go new file mode 100644 index 0000000..33e962d --- /dev/null +++ b/dialer/udp/conn.go @@ -0,0 +1,17 @@ +package udp + +import "net" + +type conn struct { + *net.UDPConn +} + +func (c *conn) WriteTo(b []byte, addr net.Addr) (int, error) { + return c.UDPConn.Write(b) +} + +func (c *conn) ReadFrom(b []byte) (n int, addr net.Addr, err error) { + n, err = c.UDPConn.Read(b) + addr = c.RemoteAddr() + return +} diff --git a/dialer/udp/dialer.go b/dialer/udp/dialer.go new file mode 100644 index 0000000..1bae731 --- /dev/null +++ b/dialer/udp/dialer.go @@ -0,0 +1,50 @@ +package udp + +import ( + "context" + "net" + + "github.com/go-gost/core/dialer" + "github.com/go-gost/core/logger" + md "github.com/go-gost/core/metadata" + "github.com/go-gost/core/registry" +) + +func init() { + registry.DialerRegistry().Register("udp", NewDialer) +} + +type udpDialer struct { + md metadata + logger logger.Logger +} + +func NewDialer(opts ...dialer.Option) dialer.Dialer { + options := &dialer.Options{} + for _, opt := range opts { + opt(options) + } + + return &udpDialer{ + logger: options.Logger, + } +} + +func (d *udpDialer) Init(md md.Metadata) (err error) { + return d.parseMetadata(md) +} + +func (d *udpDialer) Dial(ctx context.Context, addr string, opts ...dialer.DialOption) (net.Conn, error) { + var options dialer.DialOptions + for _, opt := range opts { + opt(&options) + } + + c, err := options.NetDialer.Dial(ctx, "udp", addr) + if err != nil { + return nil, err + } + return &conn{ + UDPConn: c.(*net.UDPConn), + }, nil +} diff --git a/dialer/udp/metadata.go b/dialer/udp/metadata.go new file mode 100644 index 0000000..23bd5e2 --- /dev/null +++ b/dialer/udp/metadata.go @@ -0,0 +1,23 @@ +package udp + +import ( + "time" + + md "github.com/go-gost/core/metadata" +) + +const ( + dialTimeout = "dialTimeout" +) + +const ( + defaultDialTimeout = 5 * time.Second +) + +type metadata struct { + dialTimeout time.Duration +} + +func (d *udpDialer) parseMetadata(md md.Metadata) (err error) { + return +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7de6fff --- /dev/null +++ b/go.mod @@ -0,0 +1,36 @@ +module github.com/go-gost/core + +go 1.18 + +replace github.com/templexxx/cpu v0.0.7 => github.com/templexxx/cpu v0.0.10-0.20211111114238-98168dcec14a + +require ( + github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d + github.com/go-gost/gosocks4 v0.0.1 + github.com/go-gost/gosocks5 v0.3.1-0.20211109033403-d894d75b7f09 + github.com/go-gost/metrics v0.0.0-20220314135054-2263ae431a5f + github.com/gobwas/glob v0.2.3 + github.com/miekg/dns v1.1.45 + github.com/sirupsen/logrus v1.8.1 + github.com/xtaci/smux v1.5.16 + golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/google/go-cmp v0.5.6 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect + github.com/prometheus/client_golang v1.12.1 // indirect + github.com/prometheus/client_model v0.2.0 // indirect + github.com/prometheus/common v0.32.1 // indirect + github.com/prometheus/procfs v0.7.3 // indirect + github.com/stretchr/testify v1.7.0 // indirect + golang.org/x/mod v0.5.1 // indirect + golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect + golang.org/x/tools v0.1.9 // indirect + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect + google.golang.org/protobuf v1.27.1 // indirect + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..5b28dd7 --- /dev/null +++ b/go.sum @@ -0,0 +1,506 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl3/e6D5CLfI0j/7hiIEtvGVFPCZ7Ei2oq8iQ= +github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gost/gosocks4 v0.0.1 h1:+k1sec8HlELuQV7rWftIkmy8UijzUt2I6t+iMPlGB2s= +github.com/go-gost/gosocks4 v0.0.1/go.mod h1:3B6L47HbU/qugDg4JnoFPHgJXE43Inz8Bah1QaN9qCc= +github.com/go-gost/gosocks5 v0.3.1-0.20211109033403-d894d75b7f09 h1:A95M6UWcfZgOuJkQ7QLfG0Hs5peWIUSysCDNz4pfe04= +github.com/go-gost/gosocks5 v0.3.1-0.20211109033403-d894d75b7f09/go.mod h1:1G6I7HP7VFVxveGkoK8mnprnJqSqJjdcASKsdUn4Pp4= +github.com/go-gost/metrics v0.0.0-20220314135054-2263ae431a5f h1:gNquUvOvPXUpq4Xk7ed7motbVN5t0HMqImf96k+pzlU= +github.com/go-gost/metrics v0.0.0-20220314135054-2263ae431a5f/go.mod h1:Ac2Pigx5GMJEznkP9wLdBJ36+rYwWiJPqWk7lrg3FKg= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.1.45 h1:g5fRIhm9nx7g8osrAvgb16QJfmyMsyOCb+J7LSv+Qzk= +github.com/miekg/dns v1.1.45/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.12.1 h1:ZiaPsmm9uiBeaSMRznKsCDNtPCS0T3JVDGF+06gjBzk= +github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4= +github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= +github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/xtaci/smux v1.5.16 h1:FBPYOkW8ZTjLKUM4LI4xnnuuDC8CQ/dB04HD519WoEk= +github.com/xtaci/smux v1.5.16/go.mod h1:OMlQbT5vcgl2gb49mFkYo6SMf+zP3rcjcwQz7ZU7IGY= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38= +golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 h1:y/woIyUBFbpQGKS0u1aHF/40WUDnek3fPOyD08H5Vng= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.9 h1:j9KsMiaP1c3B0OTQGth0/k+miLGTgLsAFUCrF2vLcF8= +golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/handler/auto/handler.go b/handler/auto/handler.go new file mode 100644 index 0000000..597531d --- /dev/null +++ b/handler/auto/handler.go @@ -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 +} diff --git a/handler/forward/local/handler.go b/handler/forward/local/handler.go new file mode 100644 index 0000000..e281864 --- /dev/null +++ b/handler/forward/local/handler.go @@ -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 +} diff --git a/handler/forward/local/metadata.go b/handler/forward/local/metadata.go new file mode 100644 index 0000000..e9598dd --- /dev/null +++ b/handler/forward/local/metadata.go @@ -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 +} diff --git a/handler/forward/remote/handler.go b/handler/forward/remote/handler.go new file mode 100644 index 0000000..3f31e12 --- /dev/null +++ b/handler/forward/remote/handler.go @@ -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 +} diff --git a/handler/forward/remote/metadata.go b/handler/forward/remote/metadata.go new file mode 100644 index 0000000..474f6d2 --- /dev/null +++ b/handler/forward/remote/metadata.go @@ -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 +} diff --git a/handler/handler.go b/handler/handler.go new file mode 100644 index 0000000..c1ce5f4 --- /dev/null +++ b/handler/handler.go @@ -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) +} diff --git a/handler/http/handler.go b/handler/http/handler.go new file mode 100644 index 0000000..665e3db --- /dev/null +++ b/handler/http/handler.go @@ -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 +} diff --git a/handler/http/metadata.go b/handler/http/metadata.go new file mode 100644 index 0000000..b20b4ba --- /dev/null +++ b/handler/http/metadata.go @@ -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 +} diff --git a/handler/http/udp.go b/handler/http/udp.go new file mode 100644 index 0000000..8f72260 --- /dev/null +++ b/handler/http/udp.go @@ -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 +} diff --git a/handler/option.go b/handler/option.go new file mode 100644 index 0000000..ad86529 --- /dev/null +++ b/handler/option.go @@ -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 + } +} diff --git a/handler/socks/v4/handler.go b/handler/socks/v4/handler.go new file mode 100644 index 0000000..3d0c9ce --- /dev/null +++ b/handler/socks/v4/handler.go @@ -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 +} diff --git a/handler/socks/v4/metadata.go b/handler/socks/v4/metadata.go new file mode 100644 index 0000000..99d4395 --- /dev/null +++ b/handler/socks/v4/metadata.go @@ -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 +} diff --git a/handler/socks/v5/bind.go b/handler/socks/v5/bind.go new file mode 100644 index 0000000..89299fc --- /dev/null +++ b/handler/socks/v5/bind.go @@ -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 + } +} diff --git a/handler/socks/v5/connect.go b/handler/socks/v5/connect.go new file mode 100644 index 0000000..88a8deb --- /dev/null +++ b/handler/socks/v5/connect.go @@ -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 +} diff --git a/handler/socks/v5/handler.go b/handler/socks/v5/handler.go new file mode 100644 index 0000000..ccc55c6 --- /dev/null +++ b/handler/socks/v5/handler.go @@ -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 + } +} diff --git a/handler/socks/v5/mbind.go b/handler/socks/v5/mbind.go new file mode 100644 index 0000000..8164a54 --- /dev/null +++ b/handler/socks/v5/mbind.go @@ -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) + } +} diff --git a/handler/socks/v5/metadata.go b/handler/socks/v5/metadata.go new file mode 100644 index 0000000..5479919 --- /dev/null +++ b/handler/socks/v5/metadata.go @@ -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 +} diff --git a/handler/socks/v5/selector.go b/handler/socks/v5/selector.go new file mode 100644 index 0000000..9c10ca1 --- /dev/null +++ b/handler/socks/v5/selector.go @@ -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 +} diff --git a/handler/socks/v5/udp.go b/handler/socks/v5/udp.go new file mode 100644 index 0000000..c192256 --- /dev/null +++ b/handler/socks/v5/udp.go @@ -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 +} diff --git a/handler/socks/v5/udp_tun.go b/handler/socks/v5/udp_tun.go new file mode 100644 index 0000000..edfd379 --- /dev/null +++ b/handler/socks/v5/udp_tun.go @@ -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 +} diff --git a/hosts/hosts.go b/hosts/hosts.go new file mode 100644 index 0000000..0b51c5e --- /dev/null +++ b/hosts/hosts.go @@ -0,0 +1,132 @@ +package hosts + +import ( + "net" + "strings" + "sync" + + "github.com/go-gost/core/logger" +) + +// HostMapper is a mapping from hostname to IP. +type HostMapper interface { + Lookup(network, host string) ([]net.IP, bool) +} + +type hostMapping struct { + IPs []net.IP + Hostname string +} + +// Hosts is a static table lookup for hostnames. +// For each host a single line should be present with the following information: +// IP_address canonical_hostname [aliases...] +// Fields of the entry are separated by any number of blanks and/or tab characters. +// Text from a "#" character until the end of the line is a comment, and is ignored. +type Hosts struct { + mappings sync.Map + Logger logger.Logger +} + +func NewHosts() *Hosts { + return &Hosts{} +} + +// Map maps ip to hostname or aliases. +func (h *Hosts) Map(ip net.IP, hostname string, aliases ...string) { + if hostname == "" { + return + } + + v, _ := h.mappings.Load(hostname) + m, _ := v.(*hostMapping) + if m == nil { + m = &hostMapping{ + IPs: []net.IP{ip}, + Hostname: hostname, + } + } else { + m.IPs = append(m.IPs, ip) + } + h.mappings.Store(hostname, m) + + for _, alias := range aliases { + // indirect mapping from alias to hostname + if alias != "" { + h.mappings.Store(alias, &hostMapping{ + Hostname: hostname, + }) + } + } +} + +// Lookup searches the IP address corresponds to the given network and host from the host table. +// The network should be 'ip', 'ip4' or 'ip6', default network is 'ip'. +// the host should be a hostname (example.org) or a hostname with dot prefix (.example.org). +func (h *Hosts) Lookup(network, host string) (ips []net.IP, ok bool) { + m := h.lookup(host) + if m == nil { + m = h.lookup("." + host) + } + if m == nil { + s := host + for { + if index := strings.IndexByte(s, '.'); index > 0 { + m = h.lookup(s[index:]) + s = s[index+1:] + if m == nil { + continue + } + } + break + } + } + + if m == nil { + return + } + + // hostname alias + if !strings.HasPrefix(m.Hostname, ".") && host != m.Hostname { + m = h.lookup(m.Hostname) + if m == nil { + return + } + } + + switch network { + case "ip4": + for _, ip := range m.IPs { + if ip = ip.To4(); ip != nil { + ips = append(ips, ip) + } + } + case "ip6": + for _, ip := range m.IPs { + if ip.To4() == nil { + ips = append(ips, ip) + } + } + default: + ips = m.IPs + } + + if len(ips) > 0 { + h.Logger.Debugf("host mapper: %s -> %s", host, ips) + } + + return +} + +func (h *Hosts) lookup(host string) *hostMapping { + if h == nil || host == "" { + return nil + } + + v, ok := h.mappings.Load(host) + if !ok { + return nil + } + m, _ := v.(*hostMapping) + return m +} diff --git a/internal/util/mux/mux.go b/internal/util/mux/mux.go new file mode 100644 index 0000000..4fae020 --- /dev/null +++ b/internal/util/mux/mux.go @@ -0,0 +1,85 @@ +package mux + +import ( + "net" + + smux "github.com/xtaci/smux" +) + +type Session struct { + conn net.Conn + session *smux.Session +} + +func ClientSession(conn net.Conn) (*Session, error) { + s, err := smux.Client(conn, smux.DefaultConfig()) + if err != nil { + return nil, err + } + return &Session{ + conn: conn, + session: s, + }, nil +} + +func ServerSession(conn net.Conn) (*Session, error) { + s, err := smux.Server(conn, smux.DefaultConfig()) + if err != nil { + return nil, err + } + return &Session{ + conn: conn, + session: s, + }, nil +} + +func (session *Session) GetConn() (net.Conn, error) { + stream, err := session.session.OpenStream() + if err != nil { + return nil, err + } + return &streamConn{Conn: session.conn, stream: stream}, nil +} + +func (session *Session) Accept() (net.Conn, error) { + stream, err := session.session.AcceptStream() + if err != nil { + return nil, err + } + return &streamConn{Conn: session.conn, stream: stream}, nil +} + +func (session *Session) Close() error { + if session.session == nil { + return nil + } + return session.session.Close() +} + +func (session *Session) IsClosed() bool { + if session.session == nil { + return true + } + return session.session.IsClosed() +} + +func (session *Session) NumStreams() int { + return session.session.NumStreams() +} + +type streamConn struct { + net.Conn + stream *smux.Stream +} + +func (c *streamConn) Read(b []byte) (n int, err error) { + return c.stream.Read(b) +} + +func (c *streamConn) Write(b []byte) (n int, err error) { + return c.stream.Write(b) +} + +func (c *streamConn) Close() error { + return c.stream.Close() +} diff --git a/internal/util/socks/conn.go b/internal/util/socks/conn.go new file mode 100644 index 0000000..69316e8 --- /dev/null +++ b/internal/util/socks/conn.go @@ -0,0 +1,172 @@ +package socks + +import ( + "bytes" + "net" + + "github.com/go-gost/core/common/bufpool" + "github.com/go-gost/gosocks5" +) + +type udpTunConn struct { + net.Conn + taddr net.Addr +} + +func UDPTunClientConn(c net.Conn, targetAddr net.Addr) net.Conn { + return &udpTunConn{ + Conn: c, + taddr: targetAddr, + } +} + +func UDPTunClientPacketConn(c net.Conn) net.PacketConn { + return &udpTunConn{ + Conn: c, + } +} + +func UDPTunServerConn(c net.Conn) net.PacketConn { + return &udpTunConn{ + Conn: c, + } +} + +func (c *udpTunConn) ReadFrom(b []byte) (n int, addr net.Addr, err error) { + socksAddr := gosocks5.Addr{} + header := gosocks5.UDPHeader{ + Addr: &socksAddr, + } + dgram := gosocks5.UDPDatagram{ + Header: &header, + Data: b, + } + _, err = dgram.ReadFrom(c.Conn) + if err != nil { + return + } + + n = len(dgram.Data) + if n > len(b) { + n = copy(b, dgram.Data) + } + addr, err = net.ResolveUDPAddr("udp", socksAddr.String()) + + return +} + +func (c *udpTunConn) Read(b []byte) (n int, err error) { + n, _, err = c.ReadFrom(b) + return +} + +func (c *udpTunConn) WriteTo(b []byte, addr net.Addr) (n int, err error) { + socksAddr := gosocks5.Addr{} + if err = socksAddr.ParseFrom(addr.String()); err != nil { + return + } + + header := gosocks5.UDPHeader{ + Addr: &socksAddr, + } + dgram := gosocks5.UDPDatagram{ + Header: &header, + Data: b, + } + dgram.Header.Rsv = uint16(len(dgram.Data)) + dgram.Header.Frag = 0xff // UDP tun relay flag, used by shadowsocks + _, err = dgram.WriteTo(c.Conn) + n = len(b) + + return +} + +func (c *udpTunConn) Write(b []byte) (n int, err error) { + return c.WriteTo(b, c.taddr) +} + +var ( + DefaultBufferSize = 4096 +) + +type udpConn struct { + net.PacketConn + raddr net.Addr + taddr net.Addr + bufferSize int +} + +func UDPConn(c net.PacketConn, bufferSize int) net.PacketConn { + return &udpConn{ + PacketConn: c, + bufferSize: bufferSize, + } +} + +// ReadFrom reads an UDP datagram. +// NOTE: for server side, +// the returned addr is the target address the client want to relay to. +func (c *udpConn) ReadFrom(b []byte) (n int, addr net.Addr, err error) { + rbuf := bufpool.Get(c.bufferSize) + defer bufpool.Put(rbuf) + + n, c.raddr, err = c.PacketConn.ReadFrom(*rbuf) + if err != nil { + return + } + + socksAddr := gosocks5.Addr{} + header := gosocks5.UDPHeader{ + Addr: &socksAddr, + } + hlen, err := header.ReadFrom(bytes.NewReader((*rbuf)[:n])) + if err != nil { + return + } + n = copy(b, (*rbuf)[hlen:n]) + + addr, err = net.ResolveUDPAddr("udp", socksAddr.String()) + return +} + +func (c *udpConn) Read(b []byte) (n int, err error) { + n, _, err = c.ReadFrom(b) + return +} + +func (c *udpConn) WriteTo(b []byte, addr net.Addr) (n int, err error) { + wbuf := bufpool.Get(c.bufferSize) + defer bufpool.Put(wbuf) + + socksAddr := gosocks5.Addr{} + if err = socksAddr.ParseFrom(addr.String()); err != nil { + return + } + + header := gosocks5.UDPHeader{ + Addr: &socksAddr, + } + dgram := gosocks5.UDPDatagram{ + Header: &header, + Data: b, + } + + buf := bytes.NewBuffer((*wbuf)[:0]) + _, err = dgram.WriteTo(buf) + if err != nil { + return + } + + _, err = c.PacketConn.WriteTo(buf.Bytes(), c.raddr) + n = len(b) + + return +} + +func (c *udpConn) Write(b []byte) (n int, err error) { + return c.WriteTo(b, c.taddr) +} + +func (c *udpConn) RemoteAddr() net.Addr { + return c.raddr +} diff --git a/internal/util/socks/socks.go b/internal/util/socks/socks.go new file mode 100644 index 0000000..91d241d --- /dev/null +++ b/internal/util/socks/socks.go @@ -0,0 +1,18 @@ +package socks + +const ( + // MethodTLS is an extended SOCKS5 method with tls encryption support. + MethodTLS uint8 = 0x80 + // MethodTLSAuth is an extended SOCKS5 method with tls encryption and authentication support. + MethodTLSAuth uint8 = 0x82 + // MethodMux is an extended SOCKS5 method for stream multiplexing. + MethodMux = 0x88 +) + +const ( + // CmdMuxBind is an extended SOCKS5 request CMD for + // multiplexing transport with the binding server. + CmdMuxBind uint8 = 0xF2 + // CmdUDPTun is an extended SOCKS5 request CMD for UDP over TCP. + CmdUDPTun uint8 = 0xF3 +) diff --git a/internal/util/tcp.go b/internal/util/tcp.go new file mode 100644 index 0000000..b2298fb --- /dev/null +++ b/internal/util/tcp.go @@ -0,0 +1,32 @@ +package util + +import ( + "net" + "time" +) + +const ( + defaultKeepAlivePeriod = 180 * time.Second +) + +// TCPKeepAliveListener is a TCP listener with keep alive enabled. +type TCPKeepAliveListener struct { + KeepAlivePeriod time.Duration + *net.TCPListener +} + +func (l *TCPKeepAliveListener) Accept() (c net.Conn, err error) { + tc, err := l.AcceptTCP() + if err != nil { + return + } + + tc.SetKeepAlive(true) + period := l.KeepAlivePeriod + if period <= 0 { + period = defaultKeepAlivePeriod + } + tc.SetKeepAlivePeriod(period) + + return tc, nil +} diff --git a/listener/listener.go b/listener/listener.go new file mode 100644 index 0000000..f21f9a8 --- /dev/null +++ b/listener/listener.go @@ -0,0 +1,44 @@ +package listener + +import ( + "errors" + "net" + + "github.com/go-gost/core/metadata" +) + +var ( + ErrClosed = errors.New("accpet on closed listener") +) + +// Listener is a server listener, just like a net.Listener. +type Listener interface { + Init(metadata.Metadata) error + Accept() (net.Conn, error) + Addr() net.Addr + Close() error +} + +type AcceptError struct { + err error +} + +func NewAcceptError(err error) error { + return &AcceptError{err: err} +} + +func (e *AcceptError) Error() string { + return e.err.Error() +} + +func (e *AcceptError) Timeout() bool { + return false +} + +func (e *AcceptError) Temporary() bool { + return true +} + +func (e *AcceptError) Unwrap() error { + return e.err +} diff --git a/listener/option.go b/listener/option.go new file mode 100644 index 0000000..5a553c8 --- /dev/null +++ b/listener/option.go @@ -0,0 +1,72 @@ +package listener + +import ( + "crypto/tls" + "net/url" + + "github.com/go-gost/core/admission" + "github.com/go-gost/core/auth" + "github.com/go-gost/core/chain" + "github.com/go-gost/core/logger" +) + +type Options struct { + Addr string + Auther auth.Authenticator + Auth *url.Userinfo + TLSConfig *tls.Config + Admission admission.Admission + Chain chain.Chainer + Logger logger.Logger + Service string +} + +type Option func(opts *Options) + +func AddrOption(addr string) Option { + return func(opts *Options) { + opts.Addr = addr + } +} + +func AutherOption(auther auth.Authenticator) Option { + return func(opts *Options) { + opts.Auther = auther + } +} + +func AuthOption(auth *url.Userinfo) Option { + return func(opts *Options) { + opts.Auth = auth + } +} + +func TLSConfigOption(tlsConfig *tls.Config) Option { + return func(opts *Options) { + opts.TLSConfig = tlsConfig + } +} + +func AdmissionOption(admission admission.Admission) Option { + return func(opts *Options) { + opts.Admission = admission + } +} + +func ChainOption(chain chain.Chainer) Option { + return func(opts *Options) { + opts.Chain = chain + } +} + +func LoggerOption(logger logger.Logger) Option { + return func(opts *Options) { + opts.Logger = logger + } +} + +func ServiceOption(service string) Option { + return func(opts *Options) { + opts.Service = service + } +} diff --git a/listener/rtcp/listener.go b/listener/rtcp/listener.go new file mode 100644 index 0000000..184024b --- /dev/null +++ b/listener/rtcp/listener.go @@ -0,0 +1,102 @@ +package rtcp + +import ( + "context" + "net" + + "github.com/go-gost/core/chain" + "github.com/go-gost/core/connector" + "github.com/go-gost/core/listener" + "github.com/go-gost/core/logger" + md "github.com/go-gost/core/metadata" + "github.com/go-gost/core/registry" + metrics "github.com/go-gost/metrics/wrapper" +) + +func init() { + registry.ListenerRegistry().Register("rtcp", NewListener) +} + +type rtcpListener struct { + laddr net.Addr + ln net.Listener + md metadata + router *chain.Router + logger logger.Logger + closed chan struct{} + options listener.Options +} + +func NewListener(opts ...listener.Option) listener.Listener { + options := listener.Options{} + for _, opt := range opts { + opt(&options) + } + return &rtcpListener{ + closed: make(chan struct{}), + logger: options.Logger, + options: options, + } +} + +func (l *rtcpListener) Init(md md.Metadata) (err error) { + if err = l.parseMetadata(md); err != nil { + return + } + + laddr, err := net.ResolveTCPAddr("tcp", l.options.Addr) + if err != nil { + return + } + + l.laddr = laddr + l.router = (&chain.Router{}). + WithChain(l.options.Chain). + WithLogger(l.logger) + + return +} + +func (l *rtcpListener) Accept() (conn net.Conn, err error) { + select { + case <-l.closed: + return nil, net.ErrClosed + default: + } + + if l.ln == nil { + l.ln, err = l.router.Bind( + context.Background(), "tcp", l.laddr.String(), + connector.MuxBindOption(true), + ) + if err != nil { + return nil, listener.NewAcceptError(err) + } + l.ln = metrics.WrapListener(l.options.Service, l.ln) + } + conn, err = l.ln.Accept() + if err != nil { + l.ln.Close() + l.ln = nil + return nil, listener.NewAcceptError(err) + } + return +} + +func (l *rtcpListener) Addr() net.Addr { + return l.laddr +} + +func (l *rtcpListener) Close() error { + select { + case <-l.closed: + default: + close(l.closed) + if l.ln != nil { + l.ln.Close() + l.ln = nil + } + } + + return nil +} diff --git a/listener/rtcp/metadata.go b/listener/rtcp/metadata.go new file mode 100644 index 0000000..25aea59 --- /dev/null +++ b/listener/rtcp/metadata.go @@ -0,0 +1,19 @@ +package rtcp + +import ( + "time" + + mdata "github.com/go-gost/core/metadata" +) + +const ( + defaultKeepAlivePeriod = 180 * time.Second + defaultBacklog = 128 +) + +type metadata struct { +} + +func (l *rtcpListener) parseMetadata(md mdata.Metadata) (err error) { + return +} diff --git a/listener/rudp/listener.go b/listener/rudp/listener.go new file mode 100644 index 0000000..d4ad230 --- /dev/null +++ b/listener/rudp/listener.go @@ -0,0 +1,109 @@ +package rudp + +import ( + "context" + "net" + + "github.com/go-gost/core/chain" + "github.com/go-gost/core/connector" + "github.com/go-gost/core/listener" + "github.com/go-gost/core/logger" + md "github.com/go-gost/core/metadata" + "github.com/go-gost/core/registry" + metrics "github.com/go-gost/metrics/wrapper" +) + +func init() { + registry.ListenerRegistry().Register("rudp", NewListener) +} + +type rudpListener struct { + laddr net.Addr + ln net.Listener + router *chain.Router + closed chan struct{} + logger logger.Logger + md metadata + options listener.Options +} + +func NewListener(opts ...listener.Option) listener.Listener { + options := listener.Options{} + for _, opt := range opts { + opt(&options) + } + return &rudpListener{ + closed: make(chan struct{}), + logger: options.Logger, + options: options, + } +} + +func (l *rudpListener) Init(md md.Metadata) (err error) { + if err = l.parseMetadata(md); err != nil { + return + } + + laddr, err := net.ResolveUDPAddr("udp", l.options.Addr) + if err != nil { + return + } + + l.laddr = laddr + l.router = (&chain.Router{}). + WithChain(l.options.Chain). + WithLogger(l.logger) + + return +} + +func (l *rudpListener) Accept() (conn net.Conn, err error) { + select { + case <-l.closed: + return nil, net.ErrClosed + default: + } + + if l.ln == nil { + l.ln, err = l.router.Bind( + context.Background(), "udp", l.laddr.String(), + connector.BacklogBindOption(l.md.backlog), + connector.UDPConnTTLBindOption(l.md.ttl), + connector.UDPDataBufferSizeBindOption(l.md.readBufferSize), + connector.UDPDataQueueSizeBindOption(l.md.readQueueSize), + ) + if err != nil { + return nil, listener.NewAcceptError(err) + } + } + conn, err = l.ln.Accept() + if err != nil { + l.ln.Close() + l.ln = nil + return nil, listener.NewAcceptError(err) + } + + if pc, ok := conn.(net.PacketConn); ok { + conn = metrics.WrapUDPConn(l.options.Service, pc) + } + + return +} + +func (l *rudpListener) Addr() net.Addr { + return l.laddr +} + +func (l *rudpListener) Close() error { + select { + case <-l.closed: + default: + close(l.closed) + if l.ln != nil { + l.ln.Close() + l.ln = nil + } + } + + return nil +} diff --git a/listener/rudp/metadata.go b/listener/rudp/metadata.go new file mode 100644 index 0000000..9ad23a9 --- /dev/null +++ b/listener/rudp/metadata.go @@ -0,0 +1,54 @@ +package rudp + +import ( + "time" + + mdata "github.com/go-gost/core/metadata" +) + +const ( + defaultTTL = 5 * time.Second + defaultReadBufferSize = 1024 + defaultReadQueueSize = 128 + defaultBacklog = 128 +) + +type metadata struct { + ttl time.Duration + readBufferSize int + readQueueSize int + backlog int + retryCount int +} + +func (l *rudpListener) parseMetadata(md mdata.Metadata) (err error) { + const ( + ttl = "ttl" + readBufferSize = "readBufferSize" + readQueueSize = "readQueueSize" + backlog = "backlog" + retryCount = "retry" + ) + + l.md.ttl = mdata.GetDuration(md, ttl) + if l.md.ttl <= 0 { + l.md.ttl = defaultTTL + } + l.md.readBufferSize = mdata.GetInt(md, readBufferSize) + if l.md.readBufferSize <= 0 { + l.md.readBufferSize = defaultReadBufferSize + } + + l.md.readQueueSize = mdata.GetInt(md, readQueueSize) + if l.md.readQueueSize <= 0 { + l.md.readQueueSize = defaultReadQueueSize + } + + l.md.backlog = mdata.GetInt(md, backlog) + if l.md.backlog <= 0 { + l.md.backlog = defaultBacklog + } + + l.md.retryCount = mdata.GetInt(md, retryCount) + return +} diff --git a/listener/tcp/listener.go b/listener/tcp/listener.go new file mode 100644 index 0000000..5dcacc9 --- /dev/null +++ b/listener/tcp/listener.go @@ -0,0 +1,60 @@ +package tcp + +import ( + "net" + + "github.com/go-gost/core/listener" + "github.com/go-gost/core/logger" + md "github.com/go-gost/core/metadata" + "github.com/go-gost/core/registry" + metrics "github.com/go-gost/metrics/wrapper" +) + +func init() { + registry.ListenerRegistry().Register("tcp", NewListener) +} + +type tcpListener struct { + ln net.Listener + logger logger.Logger + md metadata + options listener.Options +} + +func NewListener(opts ...listener.Option) listener.Listener { + options := listener.Options{} + for _, opt := range opts { + opt(&options) + } + return &tcpListener{ + logger: options.Logger, + options: options, + } +} + +func (l *tcpListener) Init(md md.Metadata) (err error) { + if err = l.parseMetadata(md); err != nil { + return + } + + ln, err := net.Listen("tcp", l.options.Addr) + if err != nil { + return + } + + l.ln = metrics.WrapListener(l.options.Service, ln) + + return +} + +func (l *tcpListener) Accept() (conn net.Conn, err error) { + return l.ln.Accept() +} + +func (l *tcpListener) Addr() net.Addr { + return l.ln.Addr() +} + +func (l *tcpListener) Close() error { + return l.ln.Close() +} diff --git a/listener/tcp/metadata.go b/listener/tcp/metadata.go new file mode 100644 index 0000000..e1d286a --- /dev/null +++ b/listener/tcp/metadata.go @@ -0,0 +1,12 @@ +package tcp + +import ( + md "github.com/go-gost/core/metadata" +) + +type metadata struct { +} + +func (l *tcpListener) parseMetadata(md md.Metadata) (err error) { + return +} diff --git a/listener/tls/listener.go b/listener/tls/listener.go new file mode 100644 index 0000000..e19a92e --- /dev/null +++ b/listener/tls/listener.go @@ -0,0 +1,64 @@ +package tls + +import ( + "crypto/tls" + "net" + + admission "github.com/go-gost/core/admission/wrapper" + "github.com/go-gost/core/listener" + "github.com/go-gost/core/logger" + md "github.com/go-gost/core/metadata" + "github.com/go-gost/core/registry" + metrics "github.com/go-gost/metrics/wrapper" +) + +func init() { + registry.ListenerRegistry().Register("tls", NewListener) +} + +type tlsListener struct { + ln net.Listener + logger logger.Logger + md metadata + options listener.Options +} + +func NewListener(opts ...listener.Option) listener.Listener { + options := listener.Options{} + for _, opt := range opts { + opt(&options) + } + return &tlsListener{ + logger: options.Logger, + options: options, + } +} + +func (l *tlsListener) Init(md md.Metadata) (err error) { + if err = l.parseMetadata(md); err != nil { + return + } + + ln, err := net.Listen("tcp", l.options.Addr) + if err != nil { + return + } + ln = metrics.WrapListener(l.options.Service, ln) + ln = admission.WrapListener(l.options.Admission, ln) + + l.ln = tls.NewListener(ln, l.options.TLSConfig) + + return +} + +func (l *tlsListener) Accept() (conn net.Conn, err error) { + return l.ln.Accept() +} + +func (l *tlsListener) Addr() net.Addr { + return l.ln.Addr() +} + +func (l *tlsListener) Close() error { + return l.ln.Close() +} diff --git a/listener/tls/metadata.go b/listener/tls/metadata.go new file mode 100644 index 0000000..d515844 --- /dev/null +++ b/listener/tls/metadata.go @@ -0,0 +1,12 @@ +package tls + +import ( + mdata "github.com/go-gost/core/metadata" +) + +type metadata struct { +} + +func (l *tlsListener) parseMetadata(md mdata.Metadata) (err error) { + return +} diff --git a/listener/udp/listener.go b/listener/udp/listener.go new file mode 100644 index 0000000..a5169bd --- /dev/null +++ b/listener/udp/listener.go @@ -0,0 +1,73 @@ +package udp + +import ( + "net" + + "github.com/go-gost/core/common/util/udp" + "github.com/go-gost/core/listener" + "github.com/go-gost/core/logger" + md "github.com/go-gost/core/metadata" + "github.com/go-gost/core/registry" + metrics "github.com/go-gost/metrics/wrapper" +) + +func init() { + registry.ListenerRegistry().Register("udp", NewListener) +} + +type udpListener struct { + ln net.Listener + logger logger.Logger + md metadata + options listener.Options +} + +func NewListener(opts ...listener.Option) listener.Listener { + options := listener.Options{} + for _, opt := range opts { + opt(&options) + } + return &udpListener{ + logger: options.Logger, + options: options, + } +} + +func (l *udpListener) Init(md md.Metadata) (err error) { + if err = l.parseMetadata(md); err != nil { + return + } + + laddr, err := net.ResolveUDPAddr("udp", l.options.Addr) + if err != nil { + return + } + + var conn net.PacketConn + conn, err = net.ListenUDP("udp", laddr) + if err != nil { + return + } + conn = metrics.WrapPacketConn(l.options.Service, conn) + + l.ln = udp.NewListener( + conn, + laddr, + l.md.backlog, + l.md.readQueueSize, l.md.readBufferSize, + l.md.ttl, + l.logger) + return +} + +func (l *udpListener) Accept() (conn net.Conn, err error) { + return l.ln.Accept() +} + +func (l *udpListener) Addr() net.Addr { + return l.ln.Addr() +} + +func (l *udpListener) Close() error { + return l.ln.Close() +} diff --git a/listener/udp/metadata.go b/listener/udp/metadata.go new file mode 100644 index 0000000..f3a4167 --- /dev/null +++ b/listener/udp/metadata.go @@ -0,0 +1,52 @@ +package udp + +import ( + "time" + + mdata "github.com/go-gost/core/metadata" +) + +const ( + defaultTTL = 5 * time.Second + defaultReadBufferSize = 1024 + defaultReadQueueSize = 128 + defaultBacklog = 128 +) + +type metadata struct { + ttl time.Duration + + readBufferSize int + readQueueSize int + backlog int +} + +func (l *udpListener) parseMetadata(md mdata.Metadata) (err error) { + const ( + ttl = "ttl" + readBufferSize = "readBufferSize" + readQueueSize = "readQueueSize" + backlog = "backlog" + ) + + l.md.ttl = mdata.GetDuration(md, ttl) + if l.md.ttl <= 0 { + l.md.ttl = defaultTTL + } + l.md.readBufferSize = mdata.GetInt(md, readBufferSize) + if l.md.readBufferSize <= 0 { + l.md.readBufferSize = defaultReadBufferSize + } + + l.md.readQueueSize = mdata.GetInt(md, readQueueSize) + if l.md.readQueueSize <= 0 { + l.md.readQueueSize = defaultReadQueueSize + } + + l.md.backlog = mdata.GetInt(md, backlog) + if l.md.backlog <= 0 { + l.md.backlog = defaultBacklog + } + + return +} diff --git a/logger/gost_logger.go b/logger/gost_logger.go new file mode 100644 index 0000000..7395fb8 --- /dev/null +++ b/logger/gost_logger.go @@ -0,0 +1,155 @@ +package logger + +import ( + "fmt" + "path/filepath" + "runtime" + + "github.com/sirupsen/logrus" +) + +var ( + defaultLogger = NewLogger() +) + +func Default() Logger { + return defaultLogger +} + +func SetDefault(logger Logger) { + defaultLogger = logger +} + +type logger struct { + logger *logrus.Entry +} + +func NewLogger(opts ...LoggerOption) Logger { + var options LoggerOptions + for _, opt := range opts { + opt(&options) + } + + log := logrus.New() + if options.Output != nil { + log.SetOutput(options.Output) + } + + switch options.Format { + case TextFormat: + log.SetFormatter(&logrus.TextFormatter{ + FullTimestamp: true, + }) + default: + log.SetFormatter(&logrus.JSONFormatter{ + DisableHTMLEscape: true, + // PrettyPrint: true, + }) + } + + switch options.Level { + case DebugLevel, InfoLevel, WarnLevel, ErrorLevel, FatalLevel: + lvl, _ := logrus.ParseLevel(string(options.Level)) + log.SetLevel(lvl) + default: + log.SetLevel(logrus.InfoLevel) + } + + return &logger{ + logger: logrus.NewEntry(log), + } +} + +// WithFields adds new fields to log. +func (l *logger) WithFields(fields map[string]any) Logger { + return &logger{ + logger: l.logger.WithFields(logrus.Fields(fields)), + } +} + +// Debug logs a message at level Debug. +func (l *logger) Debug(args ...any) { + l.log(logrus.DebugLevel, args...) +} + +// Debugf logs a message at level Debug. +func (l *logger) Debugf(format string, args ...any) { + l.logf(logrus.DebugLevel, format, args...) +} + +// Info logs a message at level Info. +func (l *logger) Info(args ...any) { + l.log(logrus.InfoLevel, args...) +} + +// Infof logs a message at level Info. +func (l *logger) Infof(format string, args ...any) { + l.logf(logrus.InfoLevel, format, args...) +} + +// Warn logs a message at level Warn. +func (l *logger) Warn(args ...any) { + l.log(logrus.WarnLevel, args...) +} + +// Warnf logs a message at level Warn. +func (l *logger) Warnf(format string, args ...any) { + l.logf(logrus.WarnLevel, format, args...) +} + +// Error logs a message at level Error. +func (l *logger) Error(args ...any) { + l.log(logrus.ErrorLevel, args...) +} + +// Errorf logs a message at level Error. +func (l *logger) Errorf(format string, args ...any) { + l.logf(logrus.ErrorLevel, format, args...) +} + +// Fatal logs a message at level Fatal then the process will exit with status set to 1. +func (l *logger) Fatal(args ...any) { + l.log(logrus.FatalLevel, args...) + l.logger.Logger.Exit(1) +} + +// Fatalf logs a message at level Fatal then the process will exit with status set to 1. +func (l *logger) Fatalf(format string, args ...any) { + l.logf(logrus.FatalLevel, format, args...) + l.logger.Logger.Exit(1) +} + +func (l *logger) GetLevel() LogLevel { + return LogLevel(l.logger.Logger.GetLevel().String()) +} + +func (l *logger) IsLevelEnabled(level LogLevel) bool { + lvl, _ := logrus.ParseLevel(string(level)) + return l.logger.Logger.IsLevelEnabled(lvl) +} + +func (l *logger) log(level logrus.Level, args ...any) { + lg := l.logger + if l.logger.Logger.IsLevelEnabled(logrus.DebugLevel) { + lg = lg.WithField("caller", l.caller(3)) + } + lg.Log(level, args...) +} + +func (l *logger) logf(level logrus.Level, format string, args ...any) { + lg := l.logger + if l.logger.Logger.IsLevelEnabled(logrus.DebugLevel) { + lg = lg.WithField("caller", l.caller(3)) + } + lg.Logf(level, format, args...) +} + +func (l *logger) caller(skip int) string { + _, file, line, ok := runtime.Caller(skip) + if !ok { + file = "" + } else { + file = filepath.Join(filepath.Base(filepath.Dir(file)), filepath.Base(file)) + } + return fmt.Sprintf("%s:%d", file, line) +} diff --git a/logger/logger.go b/logger/logger.go new file mode 100644 index 0000000..6aa1e99 --- /dev/null +++ b/logger/logger.go @@ -0,0 +1,71 @@ +package logger + +import ( + "io" +) + +// LogFormat is format type +type LogFormat string + +const ( + TextFormat LogFormat = "text" + JSONFormat LogFormat = "json" +) + +// LogLevel is Logger Level type +type LogLevel string + +const ( + // DebugLevel has verbose message + DebugLevel LogLevel = "debug" + // InfoLevel is default log level + InfoLevel LogLevel = "info" + // WarnLevel is for logging messages about possible issues + WarnLevel LogLevel = "warn" + // ErrorLevel is for logging errors + ErrorLevel LogLevel = "error" + // FatalLevel is for logging fatal messages. The system shuts down after logging the message. + FatalLevel LogLevel = "fatal" +) + +type Logger interface { + WithFields(map[string]any) Logger + Debug(args ...any) + Debugf(format string, args ...any) + Info(args ...any) + Infof(format string, args ...any) + Warn(args ...any) + Warnf(format string, args ...any) + Error(args ...any) + Errorf(format string, args ...any) + Fatal(args ...any) + Fatalf(format string, args ...any) + GetLevel() LogLevel + IsLevelEnabled(level LogLevel) bool +} + +type LoggerOptions struct { + Output io.Writer + Format LogFormat + Level LogLevel +} + +type LoggerOption func(opts *LoggerOptions) + +func OutputLoggerOption(out io.Writer) LoggerOption { + return func(opts *LoggerOptions) { + opts.Output = out + } +} + +func FormatLoggerOption(format LogFormat) LoggerOption { + return func(opts *LoggerOptions) { + opts.Format = format + } +} + +func LevelLoggerOption(level LogLevel) LoggerOption { + return func(opts *LoggerOptions) { + opts.Level = level + } +} diff --git a/logger/nop_logger.go b/logger/nop_logger.go new file mode 100644 index 0000000..5a131fe --- /dev/null +++ b/logger/nop_logger.go @@ -0,0 +1,53 @@ +package logger + +var ( + nop = &nopLogger{} +) + +func Nop() Logger { + return nop +} + +type nopLogger struct{} + +func (l *nopLogger) WithFields(fields map[string]any) Logger { + return l +} + +func (l *nopLogger) Debug(args ...any) { +} + +func (l *nopLogger) Debugf(format string, args ...any) { +} + +func (l *nopLogger) Info(args ...any) { +} + +func (l *nopLogger) Infof(format string, args ...any) { +} + +func (l *nopLogger) Warn(args ...any) { +} + +func (l *nopLogger) Warnf(format string, args ...any) { +} + +func (l *nopLogger) Error(args ...any) { +} + +func (l *nopLogger) Errorf(format string, args ...any) { +} + +func (l *nopLogger) Fatal(args ...any) { +} + +func (l *nopLogger) Fatalf(format string, args ...any) { +} + +func (l *nopLogger) GetLevel() LogLevel { + return "" +} + +func (l *nopLogger) IsLevelEnabled(level LogLevel) bool { + return false +} diff --git a/metadata/metadata.go b/metadata/metadata.go new file mode 100644 index 0000000..19aadb9 --- /dev/null +++ b/metadata/metadata.go @@ -0,0 +1,156 @@ +package metadata + +import ( + "fmt" + "strconv" + "time" +) + +type Metadatable interface { + GetMetadata() Metadata +} + +type Metadata interface { + IsExists(key string) bool + Set(key string, value any) + Get(key string) any +} + +type MapMetadata map[string]any + +func (m MapMetadata) IsExists(key string) bool { + _, ok := m[key] + return ok +} + +func (m MapMetadata) Set(key string, value any) { + m[key] = value +} + +func (m MapMetadata) Get(key string) any { + if m != nil { + return m[key] + } + return nil +} + +func (m MapMetadata) Del(key string) { + delete(m, key) +} + +func GetBool(md Metadata, key string) (v bool) { + if md == nil || !md.IsExists(key) { + return + } + switch vv := md.Get(key).(type) { + case bool: + return vv + case int: + return vv != 0 + case string: + v, _ = strconv.ParseBool(vv) + return + } + return +} + +func GetInt(md Metadata, key string) (v int) { + if md == nil { + return + } + + switch vv := md.Get(key).(type) { + case bool: + if vv { + v = 1 + } + case int: + return vv + case string: + v, _ = strconv.Atoi(vv) + return + } + return +} + +func GetFloat(md Metadata, key string) (v float64) { + if md == nil { + return + } + + switch vv := md.Get(key).(type) { + case int: + return float64(vv) + case string: + v, _ = strconv.ParseFloat(vv, 64) + return + } + return +} + +func GetDuration(md Metadata, key string) (v time.Duration) { + if md == nil { + return + } + switch vv := md.Get(key).(type) { + case int: + return time.Duration(vv) * time.Second + case string: + v, _ = time.ParseDuration(vv) + if v == 0 { + n, _ := strconv.Atoi(vv) + v = time.Duration(n) * time.Second + } + } + return +} + +func GetString(md Metadata, key string) (v string) { + if md != nil { + v, _ = md.Get(key).(string) + } + return +} + +func GetStrings(md Metadata, key string) (ss []string) { + switch v := md.Get(key).(type) { + case []string: + ss = v + case []any: + for _, vv := range v { + if s, ok := vv.(string); ok { + ss = append(ss, s) + } + } + } + return +} + +func GetStringMap(md Metadata, key string) (m map[string]any) { + switch vv := md.Get(key).(type) { + case map[string]any: + return vv + case map[any]any: + m = make(map[string]any) + for k, v := range vv { + m[fmt.Sprintf("%v", k)] = v + } + } + return +} + +func GetStringMapString(md Metadata, key string) (m map[string]string) { + switch vv := md.Get(key).(type) { + case map[string]any: + m = make(map[string]string) + for k, v := range vv { + m[k] = fmt.Sprintf("%v", v) + } + case map[any]any: + m = make(map[string]string) + for k, v := range vv { + m[fmt.Sprintf("%v", k)] = fmt.Sprintf("%v", v) + } + } + return +} diff --git a/registry/admission.go b/registry/admission.go new file mode 100644 index 0000000..3fffb28 --- /dev/null +++ b/registry/admission.go @@ -0,0 +1,40 @@ +package registry + +import ( + "github.com/go-gost/core/admission" +) + +type admissionRegistry struct { + registry +} + +func (r *admissionRegistry) Register(name string, v admission.Admission) error { + return r.registry.Register(name, v) +} + +func (r *admissionRegistry) Get(name string) admission.Admission { + if name != "" { + return &admissionWrapper{name: name, r: r} + } + return nil +} + +func (r *admissionRegistry) get(name string) admission.Admission { + if v := r.registry.Get(name); v != nil { + return v.(admission.Admission) + } + return nil +} + +type admissionWrapper struct { + name string + r *admissionRegistry +} + +func (w *admissionWrapper) Admit(addr string) bool { + p := w.r.get(w.name) + if p == nil { + return false + } + return p.Admit(addr) +} diff --git a/registry/auther.go b/registry/auther.go new file mode 100644 index 0000000..ecc2d36 --- /dev/null +++ b/registry/auther.go @@ -0,0 +1,40 @@ +package registry + +import ( + "github.com/go-gost/core/auth" +) + +type autherRegistry struct { + registry +} + +func (r *autherRegistry) Register(name string, v auth.Authenticator) error { + return r.registry.Register(name, v) +} + +func (r *autherRegistry) Get(name string) auth.Authenticator { + if name != "" { + return &autherWrapper{name: name, r: r} + } + return nil +} + +func (r *autherRegistry) get(name string) auth.Authenticator { + if v := r.registry.Get(name); v != nil { + return v.(auth.Authenticator) + } + return nil +} + +type autherWrapper struct { + name string + r *autherRegistry +} + +func (w *autherWrapper) Authenticate(user, password string) bool { + v := w.r.get(w.name) + if v == nil { + return true + } + return v.Authenticate(user, password) +} diff --git a/registry/bypass.go b/registry/bypass.go new file mode 100644 index 0000000..8a77c67 --- /dev/null +++ b/registry/bypass.go @@ -0,0 +1,40 @@ +package registry + +import ( + "github.com/go-gost/core/bypass" +) + +type bypassRegistry struct { + registry +} + +func (r *bypassRegistry) Register(name string, v bypass.Bypass) error { + return r.registry.Register(name, v) +} + +func (r *bypassRegistry) Get(name string) bypass.Bypass { + if name != "" { + return &bypassWrapper{name: name, r: r} + } + return nil +} + +func (r *bypassRegistry) get(name string) bypass.Bypass { + if v := r.registry.Get(name); v != nil { + return v.(bypass.Bypass) + } + return nil +} + +type bypassWrapper struct { + name string + r *bypassRegistry +} + +func (w *bypassWrapper) Contains(addr string) bool { + bp := w.r.get(w.name) + if bp == nil { + return false + } + return bp.Contains(addr) +} diff --git a/registry/chain.go b/registry/chain.go new file mode 100644 index 0000000..36d2465 --- /dev/null +++ b/registry/chain.go @@ -0,0 +1,40 @@ +package registry + +import ( + "github.com/go-gost/core/chain" +) + +type chainRegistry struct { + registry +} + +func (r *chainRegistry) Register(name string, v chain.Chainer) error { + return r.registry.Register(name, v) +} + +func (r *chainRegistry) Get(name string) chain.Chainer { + if name != "" { + return &chainWrapper{name: name, r: r} + } + return nil +} + +func (r *chainRegistry) get(name string) chain.Chainer { + if v := r.registry.Get(name); v != nil { + return v.(chain.Chainer) + } + return nil +} + +type chainWrapper struct { + name string + r *chainRegistry +} + +func (w *chainWrapper) Route(network, address string) *chain.Route { + v := w.r.get(w.name) + if v == nil { + return nil + } + return v.Route(network, address) +} diff --git a/registry/connector.go b/registry/connector.go new file mode 100644 index 0000000..955b49b --- /dev/null +++ b/registry/connector.go @@ -0,0 +1,26 @@ +package registry + +import ( + "github.com/go-gost/core/connector" + "github.com/go-gost/core/logger" +) + +type NewConnector func(opts ...connector.Option) connector.Connector + +type connectorRegistry struct { + registry +} + +func (r *connectorRegistry) Register(name string, v NewConnector) error { + if err := r.registry.Register(name, v); err != nil { + logger.Default().Fatal(err) + } + return nil +} + +func (r *connectorRegistry) Get(name string) NewConnector { + if v := r.registry.Get(name); v != nil { + return v.(NewConnector) + } + return nil +} diff --git a/registry/dialer.go b/registry/dialer.go new file mode 100644 index 0000000..2db3b5a --- /dev/null +++ b/registry/dialer.go @@ -0,0 +1,26 @@ +package registry + +import ( + "github.com/go-gost/core/dialer" + "github.com/go-gost/core/logger" +) + +type NewDialer func(opts ...dialer.Option) dialer.Dialer + +type dialerRegistry struct { + registry +} + +func (r *dialerRegistry) Register(name string, v NewDialer) error { + if err := r.registry.Register(name, v); err != nil { + logger.Default().Fatal(err) + } + return nil +} + +func (r *dialerRegistry) Get(name string) NewDialer { + if v := r.registry.Get(name); v != nil { + return v.(NewDialer) + } + return nil +} diff --git a/registry/handler.go b/registry/handler.go new file mode 100644 index 0000000..c063206 --- /dev/null +++ b/registry/handler.go @@ -0,0 +1,26 @@ +package registry + +import ( + "github.com/go-gost/core/handler" + "github.com/go-gost/core/logger" +) + +type NewHandler func(opts ...handler.Option) handler.Handler + +type handlerRegistry struct { + registry +} + +func (r *handlerRegistry) Register(name string, v NewHandler) error { + if err := r.registry.Register(name, v); err != nil { + logger.Default().Fatal(err) + } + return nil +} + +func (r *handlerRegistry) Get(name string) NewHandler { + if v := r.registry.Get(name); v != nil { + return v.(NewHandler) + } + return nil +} diff --git a/registry/hosts.go b/registry/hosts.go new file mode 100644 index 0000000..16dacad --- /dev/null +++ b/registry/hosts.go @@ -0,0 +1,42 @@ +package registry + +import ( + "net" + + "github.com/go-gost/core/hosts" +) + +type hostsRegistry struct { + registry +} + +func (r *hostsRegistry) Register(name string, v hosts.HostMapper) error { + return r.registry.Register(name, v) +} + +func (r *hostsRegistry) Get(name string) hosts.HostMapper { + if name != "" { + return &hostsWrapper{name: name, r: r} + } + return nil +} + +func (r *hostsRegistry) get(name string) hosts.HostMapper { + if v := r.registry.Get(name); v != nil { + return v.(hosts.HostMapper) + } + return nil +} + +type hostsWrapper struct { + name string + r *hostsRegistry +} + +func (w *hostsWrapper) Lookup(network, host string) ([]net.IP, bool) { + v := w.r.get(w.name) + if v == nil { + return nil, false + } + return v.Lookup(network, host) +} diff --git a/registry/listener.go b/registry/listener.go new file mode 100644 index 0000000..8d55ce3 --- /dev/null +++ b/registry/listener.go @@ -0,0 +1,26 @@ +package registry + +import ( + "github.com/go-gost/core/listener" + "github.com/go-gost/core/logger" +) + +type NewListener func(opts ...listener.Option) listener.Listener + +type listenerRegistry struct { + registry +} + +func (r *listenerRegistry) Register(name string, v NewListener) error { + if err := r.registry.Register(name, v); err != nil { + logger.Default().Fatal(err) + } + return nil +} + +func (r *listenerRegistry) Get(name string) NewListener { + if v := r.registry.Get(name); v != nil { + return v.(NewListener) + } + return nil +} diff --git a/registry/registry.go b/registry/registry.go new file mode 100644 index 0000000..affd3d2 --- /dev/null +++ b/registry/registry.go @@ -0,0 +1,116 @@ +package registry + +import ( + "errors" + "sync" + + "github.com/go-gost/core/admission" + "github.com/go-gost/core/auth" + "github.com/go-gost/core/bypass" + "github.com/go-gost/core/chain" + "github.com/go-gost/core/hosts" + "github.com/go-gost/core/resolver" + "github.com/go-gost/core/service" +) + +var ( + ErrDup = errors.New("registry: duplicate object") +) + +var ( + listenerReg Registry[NewListener] = &listenerRegistry{} + handlerReg Registry[NewHandler] = &handlerRegistry{} + dialerReg Registry[NewDialer] = &dialerRegistry{} + connectorReg Registry[NewConnector] = &connectorRegistry{} + + serviceReg Registry[service.Service] = &serviceRegistry{} + chainReg Registry[chain.Chainer] = &chainRegistry{} + autherReg Registry[auth.Authenticator] = &autherRegistry{} + admissionReg Registry[admission.Admission] = &admissionRegistry{} + bypassReg Registry[bypass.Bypass] = &bypassRegistry{} + resolverReg Registry[resolver.Resolver] = &resolverRegistry{} + hostsReg Registry[hosts.HostMapper] = &hostsRegistry{} +) + +type Registry[T any] interface { + Register(name string, v T) error + Unregister(name string) + IsRegistered(name string) bool + Get(name string) T +} + +type registry struct { + m sync.Map +} + +func (r *registry) Register(name string, v any) error { + if name == "" || v == nil { + return nil + } + if _, loaded := r.m.LoadOrStore(name, v); loaded { + return ErrDup + } + + return nil +} + +func (r *registry) Unregister(name string) { + r.m.Delete(name) +} + +func (r *registry) IsRegistered(name string) bool { + _, ok := r.m.Load(name) + return ok +} + +func (r *registry) Get(name string) any { + if name == "" { + return nil + } + v, _ := r.m.Load(name) + return v +} + +func ListenerRegistry() Registry[NewListener] { + return listenerReg +} + +func HandlerRegistry() Registry[NewHandler] { + return handlerReg +} + +func DialerRegistry() Registry[NewDialer] { + return dialerReg +} + +func ConnectorRegistry() Registry[NewConnector] { + return connectorReg +} + +func ServiceRegistry() Registry[service.Service] { + return serviceReg +} + +func ChainRegistry() Registry[chain.Chainer] { + return chainReg +} + +func AutherRegistry() Registry[auth.Authenticator] { + return autherReg +} + +func AdmissionRegistry() Registry[admission.Admission] { + return admissionReg +} + +func BypassRegistry() Registry[bypass.Bypass] { + return bypassReg +} + +func ResolverRegistry() Registry[resolver.Resolver] { + return resolverReg +} + +func HostsRegistry() Registry[hosts.HostMapper] { + return hostsReg +} diff --git a/registry/resolver.go b/registry/resolver.go new file mode 100644 index 0000000..bd950fa --- /dev/null +++ b/registry/resolver.go @@ -0,0 +1,43 @@ +package registry + +import ( + "context" + "net" + + "github.com/go-gost/core/resolver" +) + +type resolverRegistry struct { + registry +} + +func (r *resolverRegistry) Register(name string, v resolver.Resolver) error { + return r.registry.Register(name, v) +} + +func (r *resolverRegistry) Get(name string) resolver.Resolver { + if name != "" { + return &resolverWrapper{name: name, r: r} + } + return nil +} + +func (r *resolverRegistry) get(name string) resolver.Resolver { + if v := r.registry.Get(name); v != nil { + return v.(resolver.Resolver) + } + return nil +} + +type resolverWrapper struct { + name string + r *resolverRegistry +} + +func (w *resolverWrapper) Resolve(ctx context.Context, network, host string) ([]net.IP, error) { + r := w.r.get(w.name) + if r == nil { + return nil, resolver.ErrInvalid + } + return r.Resolve(ctx, network, host) +} diff --git a/registry/service.go b/registry/service.go new file mode 100644 index 0000000..97bc25a --- /dev/null +++ b/registry/service.go @@ -0,0 +1,20 @@ +package registry + +import ( + "github.com/go-gost/core/service" +) + +type serviceRegistry struct { + registry +} + +func (r *serviceRegistry) Register(name string, v service.Service) error { + return r.registry.Register(name, v) +} + +func (r *serviceRegistry) Get(name string) service.Service { + if v := r.registry.Get(name); v != nil { + return v.(service.Service) + } + return nil +} diff --git a/resolver/exchanger/exchanger.go b/resolver/exchanger/exchanger.go new file mode 100644 index 0000000..509ab16 --- /dev/null +++ b/resolver/exchanger/exchanger.go @@ -0,0 +1,220 @@ +package exchanger + +import ( + "bytes" + "context" + "crypto/tls" + "fmt" + "io/ioutil" + "net" + "net/http" + "net/url" + "strings" + "time" + + "github.com/go-gost/core/chain" + "github.com/go-gost/core/logger" + "github.com/miekg/dns" +) + +type Options struct { + router *chain.Router + tlsConfig *tls.Config + timeout time.Duration + logger logger.Logger +} + +// Option allows a common way to set Exchanger options. +type Option func(opts *Options) + +// RouterOption sets the router for Exchanger. +func RouterOption(router *chain.Router) Option { + return func(opts *Options) { + opts.router = router + } +} + +// TLSConfigOption sets the TLS config for Exchanger. +func TLSConfigOption(cfg *tls.Config) Option { + return func(opts *Options) { + opts.tlsConfig = cfg + } +} + +// LoggerOption sets the logger for Exchanger. +func LoggerOption(logger logger.Logger) Option { + return func(opts *Options) { + opts.logger = logger + } +} + +// TimeoutOption sets the timeout for Exchanger. +func TimeoutOption(timeout time.Duration) Option { + return func(opts *Options) { + opts.timeout = timeout + } +} + +// Exchanger is an interface for DNS synchronous query. +type Exchanger interface { + Exchange(ctx context.Context, msg []byte) ([]byte, error) + String() string +} + +type exchanger struct { + network string + addr string + rawAddr string + router *chain.Router + client *http.Client + options Options +} + +// NewExchanger create an Exchanger. +// The addr should be URL-like format, +// e.g. udp://1.1.1.1:53, tls://1.1.1.1:853, https://1.0.0.1/dns-query +func NewExchanger(addr string, opts ...Option) (Exchanger, error) { + var options Options + for _, opt := range opts { + opt(&options) + } + + if !strings.Contains(addr, "://") { + addr = "udp://" + addr + } + u, err := url.Parse(addr) + if err != nil { + return nil, err + } + + if options.timeout <= 0 { + options.timeout = 5 * time.Second + } + + ex := &exchanger{ + network: u.Scheme, + addr: u.Host, + rawAddr: addr, + router: options.router, + options: options, + } + if _, port, _ := net.SplitHostPort(ex.addr); port == "" { + ex.addr = net.JoinHostPort(ex.addr, "53") + } + if ex.router == nil { + ex.router = (&chain.Router{}).WithLogger(options.logger) + } + + switch ex.network { + case "tcp": + case "dot", "tls": + if ex.options.tlsConfig == nil { + ex.options.tlsConfig = &tls.Config{ + InsecureSkipVerify: true, + } + } + ex.network = "tcp" + case "https": + ex.addr = addr + if ex.options.tlsConfig == nil { + ex.options.tlsConfig = &tls.Config{ + InsecureSkipVerify: true, + } + } + ex.client = &http.Client{ + Timeout: options.timeout, + Transport: &http.Transport{ + TLSClientConfig: options.tlsConfig, + ForceAttemptHTTP2: true, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: options.timeout, + ExpectContinueTimeout: 1 * time.Second, + DialContext: ex.dial, + }, + } + default: + ex.network = "udp" + } + + return ex, nil +} + +func (ex *exchanger) Exchange(ctx context.Context, msg []byte) ([]byte, error) { + if ex.network == "https" { + return ex.dohExchange(ctx, msg) + } + return ex.exchange(ctx, msg) +} + +func (ex *exchanger) dohExchange(ctx context.Context, msg []byte) ([]byte, error) { + req, err := http.NewRequestWithContext(ctx, "POST", ex.addr, bytes.NewBuffer(msg)) + if err != nil { + return nil, fmt.Errorf("failed to create an HTTPS request: %w", err) + } + + // req.Header.Add("Content-Type", "application/dns-udpwireformat") + req.Header.Add("Content-Type", "application/dns-message") + + client := ex.client + if client == nil { + client = http.DefaultClient + } + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to perform an HTTPS request: %w", err) + } + + // Check response status code + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("returned status code %d", resp.StatusCode) + } + + // Read wireformat response from the body + buf, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read the response body: %w", err) + } + + return buf, nil +} + +func (ex *exchanger) exchange(ctx context.Context, msg []byte) ([]byte, error) { + if ex.options.timeout > 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, ex.options.timeout) + defer cancel() + } + + c, err := ex.dial(ctx, ex.network, ex.addr) + if err != nil { + return nil, err + } + defer c.Close() + + if ex.options.tlsConfig != nil { + c = tls.Client(c, ex.options.tlsConfig) + } + + conn := &dns.Conn{Conn: c} + + if _, err = conn.Write(msg); err != nil { + return nil, err + } + + mr, err := conn.ReadMsg() + if err != nil { + return nil, err + } + + return mr.Pack() +} + +func (ex *exchanger) dial(ctx context.Context, network, address string) (net.Conn, error) { + return ex.router.Dial(ctx, network, address) +} + +func (ex *exchanger) String() string { + return ex.rawAddr +} diff --git a/resolver/impl/resolver.go b/resolver/impl/resolver.go new file mode 100644 index 0000000..fd813d9 --- /dev/null +++ b/resolver/impl/resolver.go @@ -0,0 +1,178 @@ +package impl + +import ( + "context" + "net" + "strings" + "time" + + "github.com/go-gost/core/chain" + resolver_util "github.com/go-gost/core/common/util/resolver" + "github.com/go-gost/core/logger" + resolverpkg "github.com/go-gost/core/resolver" + "github.com/go-gost/core/resolver/exchanger" + "github.com/miekg/dns" +) + +type NameServer struct { + Addr string + Chain chain.Chainer + TTL time.Duration + Timeout time.Duration + ClientIP net.IP + Prefer string + Hostname string // for TLS handshake verification + exchanger exchanger.Exchanger +} + +type resolverOptions struct { + domain string + logger logger.Logger +} + +type ResolverOption func(opts *resolverOptions) + +func DomainResolverOption(domain string) ResolverOption { + return func(opts *resolverOptions) { + opts.domain = domain + } +} + +func LoggerResolverOption(logger logger.Logger) ResolverOption { + return func(opts *resolverOptions) { + opts.logger = logger + } +} + +type resolver struct { + servers []NameServer + cache *resolver_util.Cache + options resolverOptions +} + +func NewResolver(nameservers []NameServer, opts ...ResolverOption) (resolverpkg.Resolver, error) { + options := resolverOptions{} + for _, opt := range opts { + opt(&options) + } + + var servers []NameServer + for _, server := range nameservers { + addr := strings.TrimSpace(server.Addr) + if addr == "" { + continue + } + ex, err := exchanger.NewExchanger( + addr, + exchanger.RouterOption( + (&chain.Router{}). + WithChain(server.Chain). + WithLogger(options.logger), + ), + exchanger.TimeoutOption(server.Timeout), + exchanger.LoggerOption(options.logger), + ) + if err != nil { + options.logger.Warnf("parse %s: %v", server, err) + continue + } + + server.exchanger = ex + servers = append(servers, server) + } + cache := resolver_util.NewCache(). + WithLogger(options.logger) + + return &resolver{ + servers: servers, + cache: cache, + options: options, + }, nil +} + +func (r *resolver) Resolve(ctx context.Context, network, host string) (ips []net.IP, err error) { + if ip := net.ParseIP(host); ip != nil { + return []net.IP{ip}, nil + } + + if r.options.domain != "" && + !strings.Contains(host, ".") { + host = host + "." + r.options.domain + } + + for _, server := range r.servers { + ips, err = r.resolve(ctx, &server, host) + if err != nil { + r.options.logger.Error(err) + continue + } + + r.options.logger.Debugf("resolve %s via %s: %v", host, server.exchanger.String(), ips) + + if len(ips) > 0 { + break + } + } + + return +} + +func (r *resolver) resolve(ctx context.Context, server *NameServer, host string) (ips []net.IP, err error) { + if server == nil { + return + } + + if server.Prefer == "ipv6" { // prefer ipv6 + mq := dns.Msg{} + mq.SetQuestion(dns.Fqdn(host), dns.TypeAAAA) + ips, err = r.resolveIPs(ctx, server, &mq) + if err != nil || len(ips) > 0 { + return + } + } + + // fallback to ipv4 + mq := dns.Msg{} + mq.SetQuestion(dns.Fqdn(host), dns.TypeA) + return r.resolveIPs(ctx, server, &mq) +} + +func (r *resolver) resolveIPs(ctx context.Context, server *NameServer, mq *dns.Msg) (ips []net.IP, err error) { + key := resolver_util.NewCacheKey(&mq.Question[0]) + mr := r.cache.Load(key) + if mr == nil { + resolver_util.AddSubnetOpt(mq, server.ClientIP) + mr, err = r.exchange(ctx, server.exchanger, mq) + if err != nil { + return + } + r.cache.Store(key, mr, server.TTL) + } + + for _, ans := range mr.Answer { + if ar, _ := ans.(*dns.AAAA); ar != nil { + ips = append(ips, ar.AAAA) + } + if ar, _ := ans.(*dns.A); ar != nil { + ips = append(ips, ar.A) + } + } + + return +} + +func (r *resolver) exchange(ctx context.Context, ex exchanger.Exchanger, mq *dns.Msg) (mr *dns.Msg, err error) { + query, err := mq.Pack() + if err != nil { + return + } + reply, err := ex.Exchange(ctx, query) + if err != nil { + return + } + + mr = &dns.Msg{} + err = mr.Unpack(reply) + + return +} diff --git a/resolver/resolver.go b/resolver/resolver.go new file mode 100644 index 0000000..ad0435c --- /dev/null +++ b/resolver/resolver.go @@ -0,0 +1,17 @@ +package resolver + +import ( + "context" + "errors" + "net" +) + +var ( + ErrInvalid = errors.New("resolver is invalid") +) + +type Resolver interface { + // Resolve returns a slice of the host's IPv4 and IPv6 addresses. + // The network should be 'ip', 'ip4' or 'ip6', default network is 'ip'. + Resolve(ctx context.Context, network, host string) ([]net.IP, error) +} diff --git a/service/service.go b/service/service.go new file mode 100644 index 0000000..f5ff926 --- /dev/null +++ b/service/service.go @@ -0,0 +1,120 @@ +package service + +import ( + "context" + "net" + "time" + + "github.com/go-gost/core/admission" + "github.com/go-gost/core/handler" + "github.com/go-gost/core/listener" + "github.com/go-gost/core/logger" + "github.com/go-gost/metrics" +) + +type options struct { + admission admission.Admission + logger logger.Logger +} + +type Option func(opts *options) + +func AdmissionOption(admission admission.Admission) Option { + return func(opts *options) { + opts.admission = admission + } +} + +func LoggerOption(logger logger.Logger) Option { + return func(opts *options) { + opts.logger = logger + } +} + +type Service interface { + Serve() error + Addr() net.Addr + Close() error +} + +type service struct { + name string + listener listener.Listener + handler handler.Handler + options options +} + +func NewService(name string, ln listener.Listener, h handler.Handler, opts ...Option) Service { + var options options + for _, opt := range opts { + opt(&options) + } + return &service{ + name: name, + listener: ln, + handler: h, + options: options, + } +} + +func (s *service) Addr() net.Addr { + return s.listener.Addr() +} + +func (s *service) Close() error { + return s.listener.Close() +} + +func (s *service) Serve() error { + metrics.Services().Inc() + defer metrics.Services().Dec() + + var tempDelay time.Duration + for { + conn, e := s.listener.Accept() + if e != nil { + if ne, ok := e.(net.Error); ok && ne.Temporary() { + if tempDelay == 0 { + tempDelay = 1 * time.Second + } else { + tempDelay *= 2 + } + if max := 5 * time.Second; tempDelay > max { + tempDelay = max + } + s.options.logger.Warnf("accept: %v, retrying in %v", e, tempDelay) + time.Sleep(tempDelay) + continue + } + s.options.logger.Errorf("accept: %v", e) + return e + } + tempDelay = 0 + + if s.options.admission != nil && + !s.options.admission.Admit(conn.RemoteAddr().String()) { + conn.Close() + continue + } + + go func() { + metrics.Requests(s.name).Inc() + + metrics.RequestsInFlight(s.name).Inc() + defer metrics.RequestsInFlight(s.name).Dec() + + start := time.Now() + defer func() { + metrics.RequestSeconds(s.name).Observe(time.Since(start).Seconds()) + }() + + if err := s.handler.Handle( + context.Background(), + conn, + ); err != nil { + s.options.logger.Error(err) + metrics.HandlerErrors(s.name).Inc() + } + }() + } +}