commit 60691819b1b59742d685f75f51aefa371cb31b02 Author: 世界 Date: Thu Jun 30 21:27:56 2022 +0800 Init commit diff --git a/.github/update_dependencies.sh b/.github/update_dependencies.sh new file mode 100755 index 00000000..3284229d --- /dev/null +++ b/.github/update_dependencies.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +PROJECTS=$(dirname "$0")/../.. + +go get -x github.com/sagernet/sing@$(git -C $PROJECTS/sing rev-parse HEAD) +go get -x github.com/sagernet/sing-shadowsocks@$(git -C $PROJECTS/sing-shadowsocks rev-parse HEAD) +go mod tidy diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml new file mode 100644 index 00000000..2aa7ecae --- /dev/null +++ b/.github/workflows/linter.yml @@ -0,0 +1,34 @@ +name: Linter + +on: + push: + branches: + - dev + paths: + - "**/*.go" + - ".github/workflows/linter.yml" + pull_request: + types: [ opened, synchronize, reopened ] + paths: + - "**/*.go" + - ".github/workflows/linter.yml" + +jobs: + lint: + if: github.repository == 'sagernet/sing' + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Get latest go version + id: version + run: | + echo ::set-output name=go_version::$(curl -s https://raw.githubusercontent.com/actions/go-versions/main/versions-manifest.json | grep -oE '"version": "[0-9]{1}.[0-9]{1,}(.[0-9]{1,})?"' | head -1 | cut -d':' -f2 | sed 's/ //g; s/"//g') + - name: Setup Go + uses: actions/setup-go@v2 + with: + go-version: ${{ steps.version.outputs.go_version }} + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 + with: + version: latest \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..7825bdf6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/.idea/ +/vendor/ +/*.json \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 00000000..948fcc4d --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,54 @@ +run: + timeout: 5m + +linters: + enable-all: true + disable: + - errcheck + - wrapcheck + - varnamelen + - stylecheck + - nonamedreturns + - nlreturn + - ireturn + - gomnd + - exhaustivestruct + - ifshort + - goerr113 + - gochecknoglobals + - forcetypeassert + - exhaustruct + - exhaustive + - cyclop + - containedctx + - wsl + - nestif + - lll + - funlen + - goconst + - godot + - gocognit + - golint + - goimports + - gochecknoinits + - maligned + - tagliatelle + - gocyclo + - maintidx + - gocritic + - nakedret + +linters-settings: + revive: + rules: + - name: var-naming + disabled: true + govet: + enable-all: true + disable: + - composites + - fieldalignment + - shadow + gosec: + excludes: + - G404 \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..3e3e29e3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,14 @@ +Copyright (C) 2022 by nekohasekai + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . \ No newline at end of file diff --git a/adapter/direct/inbound.go b/adapter/direct/inbound.go new file mode 100644 index 00000000..779b2bbb --- /dev/null +++ b/adapter/direct/inbound.go @@ -0,0 +1,94 @@ +package direct + +import ( + "context" + "net" + "net/netip" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/udpnat" + "github.com/sagernet/sing-box/config" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing/common/buf" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +var _ adapter.InboundHandler = (*Inbound)(nil) + +type Inbound struct { + router adapter.Router + logger log.Logger + network []string + udpNat *udpnat.Service[netip.AddrPort] + overrideOption int + overrideDestination M.Socksaddr +} + +func NewInbound(router adapter.Router, logger log.Logger, options *config.DirectInboundOptions) (inbound *Inbound) { + inbound = &Inbound{ + router: router, + logger: logger, + network: options.Network.Build(), + } + if options.OverrideAddress != "" && options.OverridePort != 0 { + inbound.overrideOption = 1 + inbound.overrideDestination = M.ParseSocksaddrHostPort(options.OverrideAddress, options.OverridePort) + } else if options.OverrideAddress != "" { + inbound.overrideOption = 2 + inbound.overrideDestination = M.ParseSocksaddrHostPort(options.OverrideAddress, options.OverridePort) + } else if options.OverridePort != 0 { + inbound.overrideOption = 3 + inbound.overrideDestination = M.Socksaddr{Port: options.OverridePort} + } + inbound.udpNat = udpnat.New[netip.AddrPort](options.UDPTimeout, inbound) + return +} + +func (d *Inbound) Type() string { + return C.TypeDirect +} + +func (d *Inbound) Network() []string { + return d.network +} + +func (d *Inbound) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { + switch d.overrideOption { + case 0: + metadata.Destination = d.overrideDestination + case 1: + destination := d.overrideDestination + destination.Port = metadata.Destination.Port + metadata.Destination = destination + case 2: + metadata.Destination.Port = d.overrideDestination.Port + } + d.logger.WithContext(ctx).Info("inbound connection to ", metadata.Destination) + return d.router.RouteConnection(ctx, conn, metadata) +} + +func (d *Inbound) NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, metadata adapter.InboundContext) error { + switch d.overrideOption { + case 0: + metadata.Destination = d.overrideDestination + case 1: + destination := d.overrideDestination + destination.Port = metadata.Destination.Port + metadata.Destination = destination + case 2: + metadata.Destination.Port = d.overrideDestination.Port + } + d.udpNat.NewPacketDirect(ctx, metadata.Source, conn, buffer, metadata) + return nil +} + +func (d *Inbound) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { + d.logger.WithContext(ctx).Info("inbound packet connection to ", metadata.Destination) + return d.router.RoutePacketConnection(ctx, conn, metadata) +} + +func (d *Inbound) NewError(ctx context.Context, err error) { + d.logger.WithContext(ctx).Error(err) +} diff --git a/adapter/direct/outbound.go b/adapter/direct/outbound.go new file mode 100644 index 00000000..80522c87 --- /dev/null +++ b/adapter/direct/outbound.go @@ -0,0 +1,93 @@ +package direct + +import ( + "context" + "net" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/dialer" + "github.com/sagernet/sing-box/config" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing/common/bufio" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +var _ adapter.Outbound = (*Outbound)(nil) + +type Outbound struct { + tag string + logger log.Logger + dialer N.Dialer + overrideOption int + overrideDestination M.Socksaddr +} + +func NewOutbound(tag string, router adapter.Router, logger log.Logger, options *config.DirectOutboundOptions) (outbound *Outbound) { + outbound = &Outbound{ + tag: tag, + logger: logger, + dialer: dialer.NewDialer(router, options.DialerOptions), + } + if options.OverrideAddress != "" && options.OverridePort != 0 { + outbound.overrideOption = 1 + outbound.overrideDestination = M.ParseSocksaddrHostPort(options.OverrideAddress, options.OverridePort) + } else if options.OverrideAddress != "" { + outbound.overrideOption = 2 + outbound.overrideDestination = M.ParseSocksaddrHostPort(options.OverrideAddress, options.OverridePort) + } else if options.OverridePort != 0 { + outbound.overrideOption = 3 + outbound.overrideDestination = M.Socksaddr{Port: options.OverridePort} + } + return +} + +func (d *Outbound) Type() string { + return C.TypeDirect +} + +func (d *Outbound) Tag() string { + return d.tag +} + +func (d *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + switch d.overrideOption { + case 0: + destination = d.overrideDestination + case 1: + newDestination := d.overrideDestination + newDestination.Port = destination.Port + destination = newDestination + case 2: + destination.Port = d.overrideDestination.Port + } + switch network { + case C.NetworkTCP: + d.logger.WithContext(ctx).Debug("outbound connection to ", destination) + case C.NetworkUDP: + d.logger.WithContext(ctx).Debug("outbound packet connection to ", destination) + } + return d.dialer.DialContext(ctx, network, destination) +} + +func (d *Outbound) ListenPacket(ctx context.Context) (net.PacketConn, error) { + d.logger.WithContext(ctx).Debug("outbound packet connection") + return d.dialer.ListenPacket(ctx) +} + +func (d *Outbound) NewConnection(ctx context.Context, conn net.Conn, destination M.Socksaddr) error { + outConn, err := d.DialContext(ctx, "tcp", destination) + if err != nil { + return err + } + return bufio.CopyConn(ctx, conn, outConn) +} + +func (d *Outbound) NewPacketConnection(ctx context.Context, conn N.PacketConn, destination M.Socksaddr) error { + outConn, err := d.ListenPacket(ctx) + if err != nil { + return err + } + return bufio.CopyPacketConn(ctx, conn, bufio.NewPacketConn(outConn)) +} diff --git a/adapter/http/inbound.go b/adapter/http/inbound.go new file mode 100644 index 00000000..96e88e77 --- /dev/null +++ b/adapter/http/inbound.go @@ -0,0 +1,67 @@ +package http + +import ( + std_bufio "bufio" + "context" + "net" + "os" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/config" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/auth" + "github.com/sagernet/sing/common/buf" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/protocol/http" +) + +var _ adapter.InboundHandler = (*Inbound)(nil) + +type Inbound struct { + router adapter.Router + logger log.Logger + authenticator auth.Authenticator +} + +func NewInbound(router adapter.Router, logger log.Logger, options *config.SimpleInboundOptions) *Inbound { + return &Inbound{ + router: router, + logger: logger, + authenticator: auth.NewAuthenticator(options.Users), + } +} + +func (i *Inbound) Type() string { + return C.TypeHTTP +} + +func (i *Inbound) Network() []string { + return []string{C.NetworkTCP} +} + +func (i *Inbound) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { + ctx = &inboundContext{ctx, metadata} + return http.HandleConnection(ctx, conn, std_bufio.NewReader(conn), i.authenticator, (*inboundHandler)(i), M.Metadata{}) +} + +func (i *Inbound) NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, metadata adapter.InboundContext) error { + return os.ErrInvalid +} + +type inboundContext struct { + context.Context + metadata adapter.InboundContext +} + +type inboundHandler Inbound + +func (h *inboundHandler) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { + inboundCtx, _ := common.Cast[*inboundContext](ctx) + ctx = inboundCtx.Context + h.logger.WithContext(ctx).Info("inbound connection to ", metadata.Destination) + inboundCtx.metadata.Destination = metadata.Destination + return h.router.RouteConnection(ctx, conn, inboundCtx.metadata) +} diff --git a/adapter/inbound.go b/adapter/inbound.go new file mode 100644 index 00000000..1322c9be --- /dev/null +++ b/adapter/inbound.go @@ -0,0 +1,7 @@ +package adapter + +type Inbound interface { + Service + Type() string + Tag() string +} diff --git a/adapter/inbound_context.go b/adapter/inbound_context.go new file mode 100644 index 00000000..185843d6 --- /dev/null +++ b/adapter/inbound_context.go @@ -0,0 +1,16 @@ +package adapter + +import ( + "net/netip" + + M "github.com/sagernet/sing/common/metadata" +) + +type InboundContext struct { + Source netip.AddrPort + Destination M.Socksaddr + Inbound string + Network string + Protocol string + Domain string +} diff --git a/adapter/inbound_default.go b/adapter/inbound_default.go new file mode 100644 index 00000000..e9c41a60 --- /dev/null +++ b/adapter/inbound_default.go @@ -0,0 +1,291 @@ +package adapter + +import ( + "context" + "net" + "net/netip" + "os" + "sync" + "time" + + "github.com/database64128/tfo-go" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/buf" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +type InboundHandler interface { + Type() string + Network() []string + NewConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error + NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, metadata InboundContext) error +} + +var _ Inbound = (*DefaultInboundService)(nil) + +type DefaultInboundService struct { + ctx context.Context + logger log.Logger + tag string + listen netip.AddrPort + listenerTFO bool + handler InboundHandler + tcpListener *net.TCPListener + udpConn *net.UDPConn + forceAddr6 bool + access sync.RWMutex + closed chan struct{} + outbound chan *defaultInboundUDPServiceOutboundPacket +} + +func NewDefaultInboundService(ctx context.Context, tag string, logger log.Logger, listen netip.AddrPort, listenerTFO bool, handler InboundHandler) *DefaultInboundService { + return &DefaultInboundService{ + ctx: ctx, + logger: logger, + tag: tag, + listen: listen, + listenerTFO: listenerTFO, + handler: handler, + closed: make(chan struct{}), + outbound: make(chan *defaultInboundUDPServiceOutboundPacket), + } +} + +func (s *DefaultInboundService) Type() string { + return s.handler.Type() +} + +func (s *DefaultInboundService) Tag() string { + return s.tag +} + +func (s *DefaultInboundService) Start() error { + var listenAddr net.Addr + if common.Contains(s.handler.Network(), C.NetworkTCP) { + var tcpListener *net.TCPListener + var err error + if !s.listenerTFO { + tcpListener, err = net.ListenTCP(M.NetworkFromNetAddr("tcp", s.listen.Addr()), M.SocksaddrFromNetIP(s.listen).TCPAddr()) + } else { + tcpListener, err = tfo.ListenTCP(M.NetworkFromNetAddr("tcp", s.listen.Addr()), M.SocksaddrFromNetIP(s.listen).TCPAddr()) + } + if err != nil { + return err + } + s.tcpListener = tcpListener + go s.loopTCPIn() + listenAddr = tcpListener.Addr() + } + if common.Contains(s.handler.Network(), C.NetworkUDP) { + udpConn, err := net.ListenUDP(M.NetworkFromNetAddr("udp", s.listen.Addr()), M.SocksaddrFromNetIP(s.listen).UDPAddr()) + if err != nil { + return err + } + s.udpConn = udpConn + s.forceAddr6 = M.SocksaddrFromNet(udpConn.LocalAddr()).Addr.Is6() + if _, threadUnsafeHandler := common.Cast[N.ThreadUnsafeWriter](s.handler); !threadUnsafeHandler { + go s.loopUDPIn() + } else { + go s.loopUDPInThreadSafe() + } + go s.loopUDPOut() + if listenAddr == nil { + listenAddr = udpConn.LocalAddr() + } + } + s.logger.Info("server started at ", listenAddr) + return nil +} + +func (s *DefaultInboundService) Close() error { + return common.Close( + common.PtrOrNil(s.tcpListener), + common.PtrOrNil(s.udpConn), + ) +} + +func (s *DefaultInboundService) Upstream() any { + return s.handler +} + +func (s *DefaultInboundService) loopTCPIn() { + tcpListener := s.tcpListener + for { + conn, err := tcpListener.Accept() + if err != nil { + return + } + var metadata InboundContext + metadata.Inbound = s.tag + metadata.Source = M.AddrPortFromNet(conn.RemoteAddr()) + go func() { + metadata.Network = "tcp" + ctx := log.ContextWithID(s.ctx) + s.logger.WithContext(ctx).Info("inbound connection from ", conn.RemoteAddr()) + hErr := s.handler.NewConnection(ctx, conn, metadata) + if hErr != nil { + s.newContextError(ctx, E.Cause(hErr, "process connection from ", conn.RemoteAddr())) + } + }() + } +} + +func (s *DefaultInboundService) loopUDPIn() { + defer close(s.closed) + _buffer := buf.StackNewPacket() + defer common.KeepAlive(_buffer) + buffer := common.Dup(_buffer) + defer buffer.Release() + buffer.IncRef() + defer buffer.DecRef() + packetService := (*defaultInboundUDPService)(s) + var metadata InboundContext + metadata.Inbound = s.tag + metadata.Network = "udp" + for { + buffer.Reset() + n, addr, err := s.udpConn.ReadFromUDPAddrPort(buffer.FreeBytes()) + if err != nil { + return + } + buffer.Truncate(n) + metadata.Source = addr + err = s.handler.NewPacket(s.ctx, packetService, buffer, metadata) + if err != nil { + s.newError(E.Cause(err, "process packet from ", addr)) + } + } +} + +func (s *DefaultInboundService) loopUDPInThreadSafe() { + defer close(s.closed) + packetService := (*defaultInboundUDPService)(s) + var metadata InboundContext + metadata.Inbound = s.tag + metadata.Network = "udp" + for { + buffer := buf.NewPacket() + n, addr, err := s.udpConn.ReadFromUDPAddrPort(buffer.FreeBytes()) + if err != nil { + return + } + buffer.Truncate(n) + metadata.Source = addr + err = s.handler.NewPacket(s.ctx, packetService, buffer, metadata) + if err != nil { + buffer.Release() + s.newError(E.Cause(err, "process packet from ", addr)) + } + } +} + +func (s *DefaultInboundService) loopUDPOut() { + for { + select { + case packet := <-s.outbound: + err := s.writePacket(packet.buffer, packet.destination) + if err != nil && !E.IsClosed(err) { + s.newError(E.New("write back udp: ", err)) + } + continue + case <-s.closed: + } + for { + select { + case packet := <-s.outbound: + packet.buffer.Release() + default: + return + } + } + } +} + +func (s *DefaultInboundService) newError(err error) { + s.logger.Warn(err) +} + +func (s *DefaultInboundService) newContextError(ctx context.Context, err error) { + common.Close(err) + if E.IsClosed(err) { + s.logger.WithContext(ctx).Debug("connection closed") + return + } + s.logger.Error(err) +} + +func (s *DefaultInboundService) writePacket(buffer *buf.Buffer, destination M.Socksaddr) error { + defer buffer.Release() + if destination.Family().IsFqdn() { + udpAddr, err := net.ResolveUDPAddr("udp", destination.String()) + if err != nil { + return err + } + return common.Error(s.udpConn.WriteTo(buffer.Bytes(), udpAddr)) + } + if s.forceAddr6 && destination.Addr.Is4() { + destination.Addr = netip.AddrFrom16(destination.Addr.As16()) + } + return common.Error(s.udpConn.WriteToUDPAddrPort(buffer.Bytes(), destination.AddrPort())) +} + +type defaultInboundUDPService DefaultInboundService + +func (s *defaultInboundUDPService) ReadPacket(buffer *buf.Buffer) (M.Socksaddr, error) { + n, addr, err := s.udpConn.ReadFromUDPAddrPort(buffer.FreeBytes()) + if err != nil { + return M.Socksaddr{}, err + } + buffer.Truncate(n) + return M.SocksaddrFromNetIP(addr), nil +} + +func (s *defaultInboundUDPService) WriteIsThreadUnsafe() { +} + +type defaultInboundUDPServiceOutboundPacket struct { + buffer *buf.Buffer + destination M.Socksaddr +} + +func (s *defaultInboundUDPService) Upstream() any { + return s.udpConn +} + +func (s *defaultInboundUDPService) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error { + s.access.RLock() + defer s.access.RUnlock() + + select { + case <-s.closed: + return os.ErrClosed + default: + } + + s.outbound <- &defaultInboundUDPServiceOutboundPacket{buffer, destination} + return nil +} + +func (s *defaultInboundUDPService) Close() error { + return s.udpConn.Close() +} + +func (s *defaultInboundUDPService) LocalAddr() net.Addr { + return s.udpConn.LocalAddr() +} + +func (s *defaultInboundUDPService) SetDeadline(t time.Time) error { + return s.udpConn.SetDeadline(t) +} + +func (s *defaultInboundUDPService) SetReadDeadline(t time.Time) error { + return s.udpConn.SetReadDeadline(t) +} + +func (s *defaultInboundUDPService) SetWriteDeadline(t time.Time) error { + return s.udpConn.SetWriteDeadline(t) +} diff --git a/adapter/mixed/inbound.go b/adapter/mixed/inbound.go new file mode 100644 index 00000000..86c9e91e --- /dev/null +++ b/adapter/mixed/inbound.go @@ -0,0 +1,89 @@ +package mixed + +import ( + std_bufio "bufio" + "context" + "net" + "os" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/config" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/auth" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/rw" + "github.com/sagernet/sing/protocol/http" + "github.com/sagernet/sing/protocol/socks" + "github.com/sagernet/sing/protocol/socks/socks4" + "github.com/sagernet/sing/protocol/socks/socks5" +) + +var _ adapter.InboundHandler = (*Inbound)(nil) + +type Inbound struct { + router adapter.Router + logger log.Logger + authenticator auth.Authenticator +} + +func NewInbound(router adapter.Router, logger log.Logger, options *config.SimpleInboundOptions) *Inbound { + return &Inbound{ + router: router, + logger: logger, + authenticator: auth.NewAuthenticator(options.Users), + } +} + +func (i *Inbound) Type() string { + return C.TypeMixed +} + +func (i *Inbound) Network() []string { + return []string{C.NetworkTCP} +} + +func (i *Inbound) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { + headerType, err := rw.ReadByte(conn) + if err != nil { + return err + } + ctx = &inboundContext{ctx, metadata} + switch headerType { + case socks4.Version, socks5.Version: + return socks.HandleConnection0(ctx, conn, headerType, i.authenticator, (*inboundHandler)(i), M.Metadata{}) + } + reader := std_bufio.NewReader(bufio.NewCachedReader(conn, buf.As([]byte{headerType}))) + return http.HandleConnection(ctx, conn, reader, i.authenticator, (*inboundHandler)(i), M.Metadata{}) +} + +func (i *Inbound) NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, metadata adapter.InboundContext) error { + return os.ErrInvalid +} + +type inboundContext struct { + context.Context + metadata adapter.InboundContext +} + +type inboundHandler Inbound + +func (h *inboundHandler) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { + inboundCtx, _ := common.Cast[*inboundContext](ctx) + ctx = inboundCtx.Context + h.logger.WithContext(ctx).Info("inbound connection to ", metadata.Destination) + inboundCtx.metadata.Destination = metadata.Destination + return h.router.RouteConnection(ctx, conn, inboundCtx.metadata) +} + +func (h *inboundHandler) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error { + inboundCtx, _ := common.Cast[*inboundContext](ctx) + ctx = inboundCtx.Context + h.logger.WithContext(ctx).Info("inbound packet connection to ", metadata.Destination) + inboundCtx.metadata.Destination = metadata.Destination + return h.router.RoutePacketConnection(ctx, conn, inboundCtx.metadata) +} diff --git a/adapter/outbound.go b/adapter/outbound.go new file mode 100644 index 00000000..f3a0a877 --- /dev/null +++ b/adapter/outbound.go @@ -0,0 +1,21 @@ +package adapter + +import ( + "context" + "net" + + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +type Outbound interface { + Type() string + Tag() string + NewConnection(ctx context.Context, conn net.Conn, destination M.Socksaddr) error + NewPacketConnection(ctx context.Context, conn N.PacketConn, destination M.Socksaddr) error + N.Dialer +} + +type OutboundInitializer interface { + Init(outbound Outbound) error +} diff --git a/adapter/router.go b/adapter/router.go new file mode 100644 index 00000000..7d352439 --- /dev/null +++ b/adapter/router.go @@ -0,0 +1,15 @@ +package adapter + +import ( + "context" + "net" + + N "github.com/sagernet/sing/common/network" +) + +type Router interface { + DefaultOutbound() Outbound + Outbound(tag string) (Outbound, bool) + RouteConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error + RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error +} diff --git a/adapter/service.go b/adapter/service.go new file mode 100644 index 00000000..5ed0798d --- /dev/null +++ b/adapter/service.go @@ -0,0 +1,6 @@ +package adapter + +type Service interface { + Start() error + Close() error +} diff --git a/adapter/shadowsocks/inbound.go b/adapter/shadowsocks/inbound.go new file mode 100644 index 00000000..41585588 --- /dev/null +++ b/adapter/shadowsocks/inbound.go @@ -0,0 +1,114 @@ +package shadowsocks + +import ( + "context" + "net" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/config" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-shadowsocks" + "github.com/sagernet/sing-shadowsocks/shadowaead" + "github.com/sagernet/sing-shadowsocks/shadowaead_2022" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/buf" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +var ErrUnsupportedMethod = E.New("unsupported method") + +var _ adapter.InboundHandler = (*Inbound)(nil) + +type Inbound struct { + router adapter.Router + logger log.Logger + network []string + service shadowsocks.Service +} + +func (i *Inbound) Network() []string { + return i.network +} + +func NewInbound(router adapter.Router, logger log.Logger, options *config.ShadowsocksInboundOptions) (inbound *Inbound, err error) { + inbound = &Inbound{ + router: router, + logger: logger, + network: options.Network.Build(), + } + handler := (*inboundHandler)(inbound) + + var udpTimeout int64 + if options.UDPTimeout != 0 { + udpTimeout = options.UDPTimeout + } else { + udpTimeout = 300 + } + + switch { + case options.Method == shadowsocks.MethodNone: + inbound.service = shadowsocks.NewNoneService(options.UDPTimeout, handler) + case common.Contains(shadowaead.List, options.Method): + inbound.service, err = shadowaead.NewService(options.Method, nil, options.Password, udpTimeout, handler) + case common.Contains(shadowaead_2022.List, options.Method): + inbound.service, err = shadowaead_2022.NewServiceWithPassword(options.Method, options.Password, udpTimeout, handler) + default: + err = E.Extend(ErrUnsupportedMethod, options.Method) + } + return +} + +func (i *Inbound) Type() string { + return C.TypeShadowsocks +} + +func (i *Inbound) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { + return i.service.NewConnection(&inboundContext{ctx, metadata}, conn, M.Metadata{ + Source: M.SocksaddrFromNetIP(metadata.Source), + }) +} + +func (i *Inbound) NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, metadata adapter.InboundContext) error { + return i.service.NewPacket(&inboundContext{ctx, metadata}, conn, buffer, M.Metadata{ + Source: M.SocksaddrFromNetIP(metadata.Source), + }) +} + +func (i *Inbound) Upstream() any { + return i.service +} + +type inboundContext struct { + context.Context + metadata adapter.InboundContext +} + +type inboundHandler Inbound + +func (h *inboundHandler) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { + inboundCtx, _ := common.Cast[*inboundContext](ctx) + ctx = inboundCtx.Context + h.logger.WithContext(ctx).Info("inbound connection to ", metadata.Destination) + inboundCtx.metadata.Destination = metadata.Destination + return h.router.RouteConnection(ctx, conn, inboundCtx.metadata) +} + +func (h *inboundHandler) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error { + inboundCtx, _ := common.Cast[*inboundContext](ctx) + ctx = log.ContextWithID(inboundCtx.Context) + h.logger.WithContext(ctx).Info("inbound packet connection from ", inboundCtx.metadata.Source) + h.logger.WithContext(ctx).Info("inbound packet connection to ", metadata.Destination) + inboundCtx.metadata.Destination = metadata.Destination + return h.router.RoutePacketConnection(ctx, conn, inboundCtx.metadata) +} + +func (h *inboundHandler) NewError(ctx context.Context, err error) { + common.Close(err) + if E.IsClosed(err) { + return + } + h.logger.WithContext(ctx).Warn(err) +} diff --git a/adapter/shadowsocks/outbound.go b/adapter/shadowsocks/outbound.go new file mode 100644 index 00000000..e678c4ea --- /dev/null +++ b/adapter/shadowsocks/outbound.go @@ -0,0 +1,104 @@ +package shadowsocks + +import ( + "context" + "net" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/dialer" + "github.com/sagernet/sing-box/common/tunnel" + "github.com/sagernet/sing-box/config" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-shadowsocks" + "github.com/sagernet/sing-shadowsocks/shadowimpl" + "github.com/sagernet/sing/common/bufio" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +var _ adapter.Outbound = (*Outbound)(nil) + +type Outbound struct { + tag string + logger log.Logger + dialer N.Dialer + method shadowsocks.Method + serverAddr M.Socksaddr +} + +func NewOutbound(tag string, router adapter.Router, logger log.Logger, options *config.ShadowsocksOutboundOptions) (outbound *Outbound, err error) { + outbound = &Outbound{ + tag: tag, + logger: logger, + dialer: dialer.NewDialer(router, options.DialerOptions), + } + outbound.method, err = shadowimpl.FetchMethod(options.Method, options.Password) + if err != nil { + return + } + if options.Server == "" { + err = E.New("missing server address") + return + } else if options.ServerPort == 0 { + err = E.New("missing server port") + return + } + outbound.serverAddr = M.ParseSocksaddrHostPort(options.Server, options.ServerPort) + return +} + +func (o *Outbound) Type() string { + return C.TypeShadowsocks +} + +func (o *Outbound) Tag() string { + return o.tag +} + +func (o *Outbound) NewConnection(ctx context.Context, conn net.Conn, destination M.Socksaddr) error { + serverConn, err := o.DialContext(ctx, "tcp", destination) + if err != nil { + return err + } + return tunnel.CopyEarlyConn(ctx, conn, serverConn) +} + +func (o *Outbound) NewPacketConnection(ctx context.Context, conn N.PacketConn, destination M.Socksaddr) error { + serverConn, err := o.ListenPacket(ctx) + if err != nil { + return err + } + return bufio.CopyPacketConn(ctx, conn, bufio.NewPacketConn(serverConn)) +} + +func (o *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + switch network { + case C.NetworkTCP: + o.logger.WithContext(ctx).Debug("outbound connection to ", destination) + outConn, err := o.dialer.DialContext(ctx, "tcp", o.serverAddr) + if err != nil { + return nil, err + } + return o.method.DialEarlyConn(outConn, destination), nil + case C.NetworkUDP: + o.logger.WithContext(ctx).Debug("outbound packet connection to ", destination) + outConn, err := o.dialer.DialContext(ctx, "udp", o.serverAddr) + if err != nil { + return nil, err + } + return &bufio.BindPacketConn{PacketConn: o.method.DialPacketConn(outConn), Addr: destination}, nil + default: + panic("unknown network " + network) + } +} + +func (o *Outbound) ListenPacket(ctx context.Context) (net.PacketConn, error) { + o.logger.WithContext(ctx).Debug("outbound packet connection to ", o.serverAddr) + outConn, err := o.dialer.ListenPacket(ctx) + if err != nil { + return nil, err + } + return o.method.DialPacketConn(&bufio.BindPacketConn{PacketConn: outConn, Addr: o.serverAddr.UDPAddr()}), nil +} diff --git a/adapter/socks/inbound.go b/adapter/socks/inbound.go new file mode 100644 index 00000000..f04d4422 --- /dev/null +++ b/adapter/socks/inbound.go @@ -0,0 +1,74 @@ +package socks + +import ( + "context" + "net" + "os" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/config" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/auth" + "github.com/sagernet/sing/common/buf" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/protocol/socks" +) + +var _ adapter.InboundHandler = (*Inbound)(nil) + +type Inbound struct { + router adapter.Router + logger log.Logger + authenticator auth.Authenticator +} + +func NewInbound(router adapter.Router, logger log.Logger, options *config.SimpleInboundOptions) *Inbound { + return &Inbound{ + router: router, + logger: logger, + authenticator: auth.NewAuthenticator(options.Users), + } +} + +func (i *Inbound) Type() string { + return C.TypeSocks +} + +func (i *Inbound) Network() []string { + return []string{C.NetworkTCP} +} + +func (i *Inbound) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { + ctx = &inboundContext{ctx, metadata} + return socks.HandleConnection(ctx, conn, i.authenticator, (*inboundHandler)(i), M.Metadata{}) +} + +func (i *Inbound) NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, metadata adapter.InboundContext) error { + return os.ErrInvalid +} + +type inboundContext struct { + context.Context + metadata adapter.InboundContext +} + +type inboundHandler Inbound + +func (h *inboundHandler) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { + inboundCtx, _ := common.Cast[*inboundContext](ctx) + ctx = inboundCtx.Context + h.logger.WithContext(ctx).Info("inbound connection to ", metadata.Destination) + inboundCtx.metadata.Destination = metadata.Destination + return h.router.RouteConnection(ctx, conn, inboundCtx.metadata) +} + +func (h *inboundHandler) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error { + inboundCtx, _ := common.Cast[*inboundContext](ctx) + ctx = inboundCtx.Context + h.logger.WithContext(ctx).Info("inbound packet connection to ", metadata.Destination) + inboundCtx.metadata.Destination = metadata.Destination + return h.router.RoutePacketConnection(ctx, conn, inboundCtx.metadata) +} diff --git a/box/service.go b/box/service.go new file mode 100644 index 00000000..6093be6e --- /dev/null +++ b/box/service.go @@ -0,0 +1,143 @@ +package box + +import ( + "context" + "net/netip" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/direct" + "github.com/sagernet/sing-box/adapter/http" + "github.com/sagernet/sing-box/adapter/mixed" + "github.com/sagernet/sing-box/adapter/shadowsocks" + "github.com/sagernet/sing-box/adapter/socks" + "github.com/sagernet/sing-box/config" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/route" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" + "github.com/sirupsen/logrus" +) + +var _ adapter.Service = (*Service)(nil) + +type Service struct { + logger *logrus.Logger + inbounds []adapter.Inbound + outbounds []adapter.Outbound + router *route.Router +} + +func NewService(ctx context.Context, options *config.Config) (service *Service, err error) { + logger := logrus.New() + logger.SetLevel(logrus.TraceLevel) + logger.Formatter.(*logrus.TextFormatter).ForceColors = true + logger.AddHook(new(log.Hook)) + if options.Log != nil { + if options.Log.Level != "" { + logger.Level, err = logrus.ParseLevel(options.Log.Level) + if err != nil { + return + } + } + } + service = &Service{ + logger: logger, + router: route.NewRouter(logrus.NewEntry(logger).WithFields(logrus.Fields{"prefix": "router: "})), + } + if len(options.Inbounds) > 0 { + for i, inboundOptions := range options.Inbounds { + var prefix string + if inboundOptions.Tag != "" { + prefix = inboundOptions.Tag + } else { + prefix = F.ToString(i) + } + prefix = F.ToString("inbound/", inboundOptions.Type, "[", prefix, "]: ") + inboundLogger := logrus.NewEntry(logger).WithFields(logrus.Fields{"prefix": prefix}) + var inbound adapter.InboundHandler + + var listenOptions config.ListenOptions + switch inboundOptions.Type { + case C.TypeDirect: + listenOptions = inboundOptions.DirectOptions.ListenOptions + inbound = direct.NewInbound(service.router, inboundLogger, inboundOptions.DirectOptions) + case C.TypeSocks: + listenOptions = inboundOptions.SocksOptions.ListenOptions + inbound = socks.NewInbound(service.router, inboundLogger, inboundOptions.SocksOptions) + case C.TypeHTTP: + listenOptions = inboundOptions.HTTPOptions.ListenOptions + inbound = http.NewInbound(service.router, inboundLogger, inboundOptions.HTTPOptions) + case C.TypeMixed: + listenOptions = inboundOptions.MixedOptions.ListenOptions + inbound = mixed.NewInbound(service.router, inboundLogger, inboundOptions.MixedOptions) + case C.TypeShadowsocks: + listenOptions = inboundOptions.ShadowsocksOptions.ListenOptions + inbound, err = shadowsocks.NewInbound(service.router, inboundLogger, inboundOptions.ShadowsocksOptions) + default: + err = E.New("unknown inbound type: " + inboundOptions.Type) + } + if err != nil { + return + } + service.inbounds = append(service.inbounds, adapter.NewDefaultInboundService( + ctx, + inboundOptions.Tag, + inboundLogger, + netip.AddrPortFrom(netip.Addr(listenOptions.Listen), listenOptions.Port), + listenOptions.TCPFastOpen, + inbound, + )) + } + } + for i, outboundOptions := range options.Outbounds { + var prefix string + if outboundOptions.Tag != "" { + prefix = outboundOptions.Tag + } else { + prefix = F.ToString(i) + } + prefix = F.ToString("outbound/", outboundOptions.Type, "[", prefix, "]: ") + outboundLogger := logrus.NewEntry(logger).WithFields(logrus.Fields{"prefix": prefix}) + var outbound adapter.Outbound + switch outboundOptions.Type { + case C.TypeDirect: + outbound = direct.NewOutbound(outboundOptions.Tag, service.router, outboundLogger, outboundOptions.DirectOptions) + case C.TypeShadowsocks: + outbound, err = shadowsocks.NewOutbound(outboundOptions.Tag, service.router, outboundLogger, outboundOptions.ShadowsocksOptions) + default: + err = E.New("unknown outbound type: " + outboundOptions.Type) + } + if err != nil { + return + } + service.outbounds = append(service.outbounds, outbound) + service.router.AddOutbound(outbound) + } + if len(service.outbounds) == 0 { + service.outbounds = append(service.outbounds, direct.NewOutbound("direct", service.router, logger, &config.DirectOutboundOptions{})) + service.router.AddOutbound(service.outbounds[0]) + } + return +} + +func (s *Service) Start() error { + for _, inbound := range s.inbounds { + err := inbound.Start() + if err != nil { + return err + } + } + return nil +} + +func (s *Service) Close() error { + for _, inbound := range s.inbounds { + inbound.Close() + } + for _, outbound := range s.outbounds { + common.Close(outbound) + } + return nil +} diff --git a/common/dialer/dialer.go b/common/dialer/dialer.go new file mode 100644 index 00000000..b7f53f9e --- /dev/null +++ b/common/dialer/dialer.go @@ -0,0 +1,46 @@ +package dialer + +import ( + "context" + "net" + "time" + + "github.com/database64128/tfo-go" + "github.com/sagernet/sing-box/config" + "github.com/sagernet/sing/common/control" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +type defaultDialer struct { + tfo.Dialer + net.ListenConfig +} + +func (d *defaultDialer) DialContext(ctx context.Context, network string, address M.Socksaddr) (net.Conn, error) { + return d.Dialer.DialContext(ctx, network, address.String()) +} + +func (d *defaultDialer) ListenPacket(ctx context.Context) (net.PacketConn, error) { + return d.ListenConfig.ListenPacket(ctx, "udp", "") +} + +func newDialer(options config.DialerOptions) N.Dialer { + var dialer net.Dialer + var listener net.ListenConfig + if options.BindInterface != "" { + dialer.Control = control.Append(dialer.Control, control.BindToInterface(options.BindInterface)) + listener.Control = control.Append(listener.Control, control.BindToInterface(options.BindInterface)) + } + if options.RoutingMark != 0 { + dialer.Control = control.Append(dialer.Control, control.RoutingMark(options.RoutingMark)) + listener.Control = control.Append(listener.Control, control.RoutingMark(options.RoutingMark)) + } + if options.ReuseAddr { + listener.Control = control.Append(listener.Control, control.ReuseAddr()) + } + if options.ConnectTimeout != 0 { + dialer.Timeout = time.Duration(options.ConnectTimeout) * time.Second + } + return &defaultDialer{tfo.Dialer{Dialer: dialer, DisableTFO: !options.TCPFastOpen}, listener} +} diff --git a/common/dialer/lazy.go b/common/dialer/lazy.go new file mode 100644 index 00000000..6cff629d --- /dev/null +++ b/common/dialer/lazy.go @@ -0,0 +1,59 @@ +package dialer + +import ( + "context" + "net" + "sync" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/config" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +type LazyDialer struct { + router adapter.Router + options config.DialerOptions + dialer N.Dialer + initOnce sync.Once + initErr error +} + +func NewDialer(router adapter.Router, options config.DialerOptions) N.Dialer { + return &LazyDialer{ + router: router, + options: options, + } +} + +func (d *LazyDialer) Dialer() (N.Dialer, error) { + d.initOnce.Do(func() { + if d.options.Detour != "" { + var loaded bool + d.dialer, loaded = d.router.Outbound(d.options.Detour) + if !loaded { + d.initErr = E.New("outbound detour not found: ", d.options.Detour) + } + } else { + d.dialer = newDialer(d.options) + } + }) + return d.dialer, d.initErr +} + +func (d *LazyDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + dialer, err := d.Dialer() + if err != nil { + return nil, err + } + return dialer.DialContext(ctx, network, destination) +} + +func (d *LazyDialer) ListenPacket(ctx context.Context) (net.PacketConn, error) { + dialer, err := d.Dialer() + if err != nil { + return nil, err + } + return dialer.ListenPacket(ctx) +} diff --git a/common/tunnel/copy.go b/common/tunnel/copy.go new file mode 100644 index 00000000..8e82915e --- /dev/null +++ b/common/tunnel/copy.go @@ -0,0 +1,37 @@ +package tunnel + +import ( + "context" + "net" + "runtime" + "time" + + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio" + E "github.com/sagernet/sing/common/exceptions" +) + +func CopyEarlyConn(ctx context.Context, conn net.Conn, serverConn net.Conn) error { + _payload := buf.StackNew() + payload := common.Dup(_payload) + err := conn.SetReadDeadline(time.Now().Add(100 * time.Millisecond)) + if err != nil { + return err + } + _, err = payload.ReadFrom(conn) + if err != nil && !E.IsTimeout(err) { + return E.Cause(err, "read payload") + } + err = conn.SetReadDeadline(time.Time{}) + if err != nil { + payload.Release() + return err + } + _, err = serverConn.Write(payload.Bytes()) + if err != nil { + return E.Cause(err, "client handshake") + } + runtime.KeepAlive(_payload) + return bufio.CopyConn(ctx, conn, serverConn) +} diff --git a/common/udpnat/service.go b/common/udpnat/service.go new file mode 100644 index 00000000..a0c04a79 --- /dev/null +++ b/common/udpnat/service.go @@ -0,0 +1,224 @@ +package udpnat + +import ( + "context" + "io" + "net" + "net/netip" + "os" + "sync/atomic" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/cache" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +type Handler interface { + NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error + E.Handler +} + +type Service[K comparable] struct { + nat *cache.LruCache[K, *conn] + handler Handler +} + +func New[K comparable](maxAge int64, handler Handler) *Service[K] { + return &Service[K]{ + nat: cache.New( + cache.WithAge[K, *conn](maxAge), + cache.WithUpdateAgeOnGet[K, *conn](), + cache.WithEvict[K, *conn](func(key K, conn *conn) { + conn.Close() + }), + ), + handler: handler, + } +} + +func (s *Service[T]) NewPacketDirect(ctx context.Context, key T, conn N.PacketConn, buffer *buf.Buffer, metadata adapter.InboundContext) { + s.NewContextPacket(ctx, key, buffer, metadata, func(natConn N.PacketConn) (context.Context, N.PacketWriter) { + return ctx, &DirectBackWriter{conn, natConn} + }) +} + +type DirectBackWriter struct { + Source N.PacketConn + Nat N.PacketConn +} + +func (w *DirectBackWriter) WritePacket(buffer *buf.Buffer, addr M.Socksaddr) error { + return w.Source.WritePacket(buffer, M.SocksaddrFromNet(w.Nat.LocalAddr())) +} + +func (w *DirectBackWriter) Upstream() any { + return w.Source +} + +func (s *Service[T]) NewPacket(ctx context.Context, key T, buffer *buf.Buffer, metadata adapter.InboundContext, init func(natConn N.PacketConn) N.PacketWriter) { + s.NewContextPacket(ctx, key, buffer, metadata, func(natConn N.PacketConn) (context.Context, N.PacketWriter) { + return ctx, init(natConn) + }) +} + +func (s *Service[T]) NewContextPacket(ctx context.Context, key T, buffer *buf.Buffer, metadata adapter.InboundContext, init func(natConn N.PacketConn) (context.Context, N.PacketWriter)) { + var maxAge int64 + switch metadata.Destination.Port { + case 443, 853: + maxAge = 30 + case 53, 3478: + maxAge = 10 + } + c, loaded := s.nat.LoadOrStoreWithAge(key, maxAge, func() *conn { + c := &conn{ + data: make(chan packet, 64), + localAddr: metadata.Source, + remoteAddr: metadata.Destination, + fastClose: metadata.Destination.Port == 53, + } + c.ctx, c.cancel = context.WithCancel(ctx) + return c + }) + if !loaded { + ctx, c.source = init(c) + go func() { + err := s.handler.NewPacketConnection(ctx, c, metadata) + if err != nil { + s.handler.NewError(ctx, err) + } + c.Close() + s.nat.Delete(key) + }() + } else { + c.localAddr = metadata.Source + } + if common.Done(c.ctx) { + s.nat.Delete(key) + if !common.Done(ctx) { + s.NewContextPacket(ctx, key, buffer, metadata, init) + } + return + } + c.data <- packet{ + data: buffer, + destination: metadata.Destination, + } +} + +type packet struct { + data *buf.Buffer + destination M.Socksaddr +} + +type conn struct { + ctx context.Context + cancel context.CancelFunc + data chan packet + localAddr netip.AddrPort + remoteAddr M.Socksaddr + source N.PacketWriter + fastClose bool + readDeadline atomic.Value +} + +func (c *conn) ReadPacketThreadSafe() (buffer *buf.Buffer, addr M.Socksaddr, err error) { + var deadline <-chan time.Time + if d, ok := c.readDeadline.Load().(time.Time); ok && !d.IsZero() { + timer := time.NewTimer(time.Until(d)) + defer timer.Stop() + deadline = timer.C + } + select { + case p := <-c.data: + return p.data, p.destination, nil + case <-c.ctx.Done(): + return nil, M.Socksaddr{}, io.ErrClosedPipe + case <-deadline: + return nil, M.Socksaddr{}, os.ErrDeadlineExceeded + } +} + +func (c *conn) ReadPacket(buffer *buf.Buffer) (addr M.Socksaddr, err error) { + var deadline <-chan time.Time + if d, ok := c.readDeadline.Load().(time.Time); ok && !d.IsZero() { + timer := time.NewTimer(time.Until(d)) + defer timer.Stop() + deadline = timer.C + } + select { + case p := <-c.data: + _, err = buffer.ReadFrom(p.data) + p.data.Release() + return p.destination, err + case <-c.ctx.Done(): + return M.Socksaddr{}, io.ErrClosedPipe + case <-deadline: + return M.Socksaddr{}, os.ErrDeadlineExceeded + } +} + +func (c *conn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error { + if c.fastClose { + defer c.Close() + } + return c.source.WritePacket(buffer, destination) +} + +func (c *conn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { + select { + case pkt := <-c.data: + n = copy(p, pkt.data.Bytes()) + pkt.data.Release() + addr = pkt.destination.UDPAddr() + return n, addr, nil + case <-c.ctx.Done(): + return 0, nil, io.ErrClosedPipe + } +} + +func (c *conn) WriteTo(p []byte, addr net.Addr) (n int, err error) { + if c.fastClose { + defer c.Close() + } + return len(p), c.source.WritePacket(buf.As(p).ToOwned(), M.SocksaddrFromNet(addr)) +} + +func (c *conn) Close() error { + select { + case <-c.ctx.Done(): + return os.ErrClosed + default: + } + c.cancel() + return nil +} + +func (c *conn) LocalAddr() net.Addr { + return M.SocksaddrFromNetIP(c.localAddr).UDPAddr() +} + +func (c *conn) RemoteAddr() net.Addr { + return c.remoteAddr.UDPAddr() +} + +func (c *conn) SetDeadline(t time.Time) error { + return os.ErrInvalid +} + +func (c *conn) SetReadDeadline(t time.Time) error { + c.readDeadline.Store(t) + return nil +} + +func (c *conn) SetWriteDeadline(t time.Time) error { + return os.ErrInvalid +} + +func (c *conn) Upstream() any { + return c.source +} diff --git a/config/address.go b/config/address.go new file mode 100644 index 00000000..b06a2091 --- /dev/null +++ b/config/address.go @@ -0,0 +1,50 @@ +package config + +import ( + "encoding/json" + "net/netip" + + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" +) + +type ListenAddress netip.Addr + +func (a *ListenAddress) MarshalJSON() ([]byte, error) { + value := netip.Addr(*a).String() + return json.Marshal(value) +} + +func (a *ListenAddress) UnmarshalJSON(bytes []byte) error { + var value string + err := json.Unmarshal(bytes, &value) + if err != nil { + return err + } + addr, err := netip.ParseAddr(value) + if err != nil { + return err + } + *a = ListenAddress(addr) + return nil +} + +type ServerAddress M.Socksaddr + +func (a *ServerAddress) MarshalJSON() ([]byte, error) { + value := M.Socksaddr(*a).String() + return json.Marshal(value) +} + +func (a *ServerAddress) UnmarshalJSON(bytes []byte) error { + var value string + err := json.Unmarshal(bytes, &value) + if err != nil { + return err + } + if value == "" { + return E.New("empty server address") + } + *a = ServerAddress(M.ParseSocksaddr(value)) + return nil +} diff --git a/config/config.go b/config/config.go new file mode 100644 index 00000000..c412c86d --- /dev/null +++ b/config/config.go @@ -0,0 +1,12 @@ +package config + +type Config struct { + Log *LogConfig `json:"log"` + Inbounds []Inbound `json:"inbounds,omitempty"` + Outbounds []Outbound `json:"outbounds,omitempty"` + Routes []Route `json:"routes,omitempty"` +} + +type LogConfig struct { + Level string `json:"level,omitempty"` +} diff --git a/config/inbound.go b/config/inbound.go new file mode 100644 index 00000000..e0e3c55d --- /dev/null +++ b/config/inbound.go @@ -0,0 +1,115 @@ +package config + +import ( + "encoding/json" + + "github.com/sagernet/sing/common/auth" + E "github.com/sagernet/sing/common/exceptions" +) + +var ErrUnknownInboundType = E.New("unknown inbound type") + +type _Inbound struct { + Tag string `json:"tag,omitempty"` + Type string `json:"type,omitempty"` + DirectOptions *DirectInboundOptions `json:"directOptions,omitempty"` + SocksOptions *SimpleInboundOptions `json:"socksOptions,omitempty"` + HTTPOptions *SimpleInboundOptions `json:"httpOptions,omitempty"` + MixedOptions *SimpleInboundOptions `json:"mixedOptions,omitempty"` + ShadowsocksOptions *ShadowsocksInboundOptions `json:"shadowsocksOptions,omitempty"` +} + +type Inbound _Inbound + +func (i *Inbound) MarshalJSON() ([]byte, error) { + var options []byte + var err error + switch i.Type { + case "direct": + options, err = json.Marshal(i.DirectOptions) + case "socks": + options, err = json.Marshal(i.SocksOptions) + case "http": + options, err = json.Marshal(i.HTTPOptions) + case "mixed": + options, err = json.Marshal(i.MixedOptions) + case "shadowsocks": + options, err = json.Marshal(i.ShadowsocksOptions) + default: + return nil, E.Extend(ErrUnknownInboundType, i.Type) + } + if err != nil { + return nil, err + } + var content map[string]any + err = json.Unmarshal(options, &content) + if err != nil { + return nil, err + } + content["tag"] = i.Tag + content["type"] = i.Type + return json.Marshal(content) +} + +func (i *Inbound) UnmarshalJSON(bytes []byte) error { + err := json.Unmarshal(bytes, (*_Inbound)(i)) + if err != nil { + return err + } + switch i.Type { + case "direct": + if i.DirectOptions != nil { + break + } + err = json.Unmarshal(bytes, &i.DirectOptions) + case "socks": + if i.SocksOptions != nil { + break + } + err = json.Unmarshal(bytes, &i.SocksOptions) + case "http": + if i.HTTPOptions != nil { + break + } + err = json.Unmarshal(bytes, &i.HTTPOptions) + case "mixed": + if i.MixedOptions != nil { + break + } + err = json.Unmarshal(bytes, &i.MixedOptions) + case "shadowsocks": + if i.ShadowsocksOptions != nil { + break + } + err = json.Unmarshal(bytes, &i.ShadowsocksOptions) + default: + return E.Extend(ErrUnknownInboundType, i.Type) + } + return err +} + +type ListenOptions struct { + Listen ListenAddress `json:"listen"` + Port uint16 `json:"listen_port"` + TCPFastOpen bool `json:"tcpFastOpen,omitempty"` + UDPTimeout int64 `json:"udpTimeout,omitempty"` +} + +type SimpleInboundOptions struct { + ListenOptions + Users []auth.User `json:"users,omitempty"` +} + +type DirectInboundOptions struct { + ListenOptions + Network NetworkList `json:"network,omitempty"` + OverrideAddress string `json:"overrideAddress,omitempty"` + OverridePort uint16 `json:"overridePort,omitempty"` +} + +type ShadowsocksInboundOptions struct { + ListenOptions + Network NetworkList `json:"network,omitempty"` + Method string `json:"method"` + Password string `json:"password"` +} diff --git a/config/network.go b/config/network.go new file mode 100644 index 00000000..409386d3 --- /dev/null +++ b/config/network.go @@ -0,0 +1,35 @@ +package config + +import ( + "encoding/json" + "strings" + + C "github.com/sagernet/sing-box/constant" + E "github.com/sagernet/sing/common/exceptions" +) + +type NetworkList []string + +func (v *NetworkList) UnmarshalJSON(data []byte) error { + var networkList string + err := json.Unmarshal(data, &networkList) + if err != nil { + return err + } + for _, networkName := range strings.Split(networkList, ",") { + switch networkName { + case "tcp", "udp": + *v = append(*v, networkName) + default: + return E.New("unknown network: " + networkName) + } + } + return nil +} + +func (v *NetworkList) Build() []string { + if len(*v) == 0 { + return []string{C.NetworkTCP, C.NetworkUDP} + } + return *v +} diff --git a/config/outbound.go b/config/outbound.go new file mode 100644 index 00000000..91d0e354 --- /dev/null +++ b/config/outbound.go @@ -0,0 +1,90 @@ +package config + +import ( + "encoding/json" + + E "github.com/sagernet/sing/common/exceptions" +) + +var ErrUnknownOutboundType = E.New("unknown outbound type") + +type _Outbound struct { + Tag string `json:"tag,omitempty"` + Type string `json:"type,omitempty"` + DirectOptions *DirectOutboundOptions `json:"directOptions,omitempty"` + ShadowsocksOptions *ShadowsocksOutboundOptions `json:"shadowsocksOptions,omitempty"` +} + +type Outbound _Outbound + +func (i *Outbound) MarshalJSON() ([]byte, error) { + var options []byte + var err error + switch i.Type { + case "direct": + options, err = json.Marshal(i.DirectOptions) + case "shadowsocks": + options, err = json.Marshal(i.ShadowsocksOptions) + default: + return nil, E.Extend(ErrUnknownOutboundType, i.Type) + } + if err != nil { + return nil, err + } + var content map[string]any + err = json.Unmarshal(options, &content) + if err != nil { + return nil, err + } + content["tag"] = i.Tag + content["type"] = i.Type + return json.Marshal(content) +} + +func (i *Outbound) UnmarshalJSON(bytes []byte) error { + if err := json.Unmarshal(bytes, (*_Outbound)(i)); err != nil { + return err + } + switch i.Type { + case "direct": + if i.DirectOptions != nil { + break + } + if err := json.Unmarshal(bytes, &i.DirectOptions); err != nil { + return err + } + case "shadowsocks": + if i.ShadowsocksOptions != nil { + break + } + if err := json.Unmarshal(bytes, &i.ShadowsocksOptions); err != nil { + return err + } + default: + return E.Extend(ErrUnknownOutboundType, i.Type) + } + return nil +} + +type DialerOptions struct { + Detour string `json:"detour,omitempty"` + BindInterface string `json:"bind_interface,omitempty"` + RoutingMark int `json:"routing_mark,omitempty"` + ReuseAddr bool `json:"reuse_addr,omitempty"` + ConnectTimeout int `json:"connect_timeout,omitempty"` + TCPFastOpen bool `json:"tcp_fast_open,omitempty"` +} + +type DirectOutboundOptions struct { + DialerOptions + OverrideAddress string `json:"override_address,omitempty"` + OverridePort uint16 `json:"override_port,omitempty"` +} + +type ShadowsocksOutboundOptions struct { + DialerOptions + Server string `json:"server"` + ServerPort uint16 `json:"server_port"` + Method string `json:"method"` + Password string `json:"password"` +} diff --git a/config/route.go b/config/route.go new file mode 100644 index 00000000..498f73ba --- /dev/null +++ b/config/route.go @@ -0,0 +1,13 @@ +package config + +type Route struct { + Type string `json:"type"` +} + +type SimpleRule struct { + Inbound []string `json:"inbound,omitempty"` + IPVersion []int `json:"ip_version,omitempty"` + Network []string `json:"network,omitempty"` + Protocol []string `json:"protocol,omitempty"` + Outbound string `json:"outbound,omitempty"` +} diff --git a/constant/network.go b/constant/network.go new file mode 100644 index 00000000..f673da29 --- /dev/null +++ b/constant/network.go @@ -0,0 +1,6 @@ +package constant + +const ( + NetworkTCP = "tcp" + NetworkUDP = "udp" +) diff --git a/constant/proxy.go b/constant/proxy.go new file mode 100644 index 00000000..90b33b4f --- /dev/null +++ b/constant/proxy.go @@ -0,0 +1,9 @@ +package constant + +const ( + TypeDirect = "direct" + TypeSocks = "socks" + TypeHTTP = "http" + TypeMixed = "mixed" + TypeShadowsocks = "shadowsocks" +) diff --git a/constant/version.go b/constant/version.go new file mode 100644 index 00000000..037699f4 --- /dev/null +++ b/constant/version.go @@ -0,0 +1,6 @@ +package constant + +var ( + Version = "nightly" + BuildTime = "unknown" +) diff --git a/format.go b/format.go new file mode 100644 index 00000000..37faa453 --- /dev/null +++ b/format.go @@ -0,0 +1,7 @@ +package main + +//go:generate go install -v mvdan.cc/gofumpt@latest +//go:generate go install -v github.com/daixiang0/gci@latest +//go:generate gofumpt -l -w . +//go:generate gofmt -s -w . +//go:generate gci write . diff --git a/go.mod b/go.mod new file mode 100644 index 00000000..e9ffddd8 --- /dev/null +++ b/go.mod @@ -0,0 +1,21 @@ +module github.com/sagernet/sing-box + +go 1.18 + +require ( + github.com/database64128/tfo-go v1.0.4 + github.com/logrusorgru/aurora v2.0.3+incompatible + github.com/sagernet/sing v0.0.0-20220701053004-e85528b42fb3 + github.com/sagernet/sing-shadowsocks v0.0.0-20220701010118-c2032fe11c46 + github.com/sirupsen/logrus v1.8.1 + github.com/spf13/cobra v1.5.0 +) + +require ( + github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/klauspost/cpuid/v2 v2.0.12 // indirect + github.com/spf13/pflag v1.0.5 // indirect + golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect + golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b // indirect + lukechampine.com/blake3 v1.1.7 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..49b3a8e9 --- /dev/null +++ b/go.sum @@ -0,0 +1,36 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/database64128/tfo-go v1.0.4 h1:0D9CsLor6q+2UrLhFYY3MkKkxRGf2W+27beMAo43SJc= +github.com/database64128/tfo-go v1.0.4/go.mod h1:q5W+W0+2IHrw/Lnl0yg4sz7Kz5IDsm9x0vhwZXkRwG4= +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/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.0.12 h1:p9dKCg8i4gmOxtv35DvrYoWqYzQrvEVdjQ762Y0OqZE= +github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= +github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= +github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= +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/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagernet/sing v0.0.0-20220701053004-e85528b42fb3 h1:N+I+g5md4NG7iwznCktg6N+sMPKMSEAG1Yq8rWNuQmE= +github.com/sagernet/sing v0.0.0-20220701053004-e85528b42fb3/go.mod h1:3ZmoGNg/nNJTyHAZFNRSPaXpNIwpDvyIiAUd0KIWV5c= +github.com/sagernet/sing-shadowsocks v0.0.0-20220701010118-c2032fe11c46 h1:LvMohGqz36YU9mst2BVpxdmbP+B3q3Jw3iu9Ni7ENZ4= +github.com/sagernet/sing-shadowsocks v0.0.0-20220701010118-c2032fe11c46/go.mod h1:4N/34mC/YhclM/dlI7DA4/pPpXGZy360d4ofmLB5XX8= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= +github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b h1:2n253B2r0pYSmEV+UNCQoPfU/FiaizQEK5Gu4Bq4JE8= +golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +lukechampine.com/blake3 v1.1.7 h1:GgRMhmdsuK8+ii6UZFDL8Nb+VyMwadAgcJyfYHxG6n0= +lukechampine.com/blake3 v1.1.7/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA= diff --git a/log/id.go b/log/id.go new file mode 100644 index 00000000..aac6214a --- /dev/null +++ b/log/id.go @@ -0,0 +1,15 @@ +package log + +import ( + "context" + "math/rand" +) + +type idContext struct { + context.Context + id uint32 +} + +func ContextWithID(ctx context.Context) context.Context { + return &idContext{ctx, rand.Uint32()} +} diff --git a/log/log.go b/log/log.go new file mode 100644 index 00000000..e57b3b09 --- /dev/null +++ b/log/log.go @@ -0,0 +1,52 @@ +package log + +import ( + "context" + + "github.com/logrusorgru/aurora" + "github.com/sagernet/sing/common" + F "github.com/sagernet/sing/common/format" + "github.com/sirupsen/logrus" +) + +type Logger interface { + logrus.FieldLogger + WithContext(ctx context.Context) *logrus.Entry +} + +type Hook struct{} + +func (h *Hook) Levels() []logrus.Level { + return logrus.AllLevels +} + +func (h *Hook) Fire(entry *logrus.Entry) error { + if prefix, loaded := entry.Data["prefix"]; loaded { + prefixStr := prefix.(string) + delete(entry.Data, "prefix") + entry.Message = prefixStr + entry.Message + } + if idCtx, loaded := common.Cast[*idContext](entry.Context); loaded { + var color aurora.Color + color = aurora.Color(uint8(idCtx.id)) + color %= 215 + row := uint(color / 36) + column := uint(color % 36) + + var r, g, b float32 + r = float32(row * 51) + g = float32(column / 6 * 51) + b = float32((column % 6) * 51) + luma := 0.2126*r + 0.7152*g + 0.0722*b + if luma < 60 { + row = 5 - row + column = 35 - column + color = aurora.Color(row*36 + column) + } + color += 16 + color = color << 16 + color |= 1 << 14 + entry.Message = F.ToString("[", aurora.Colorize(idCtx.id, color).String(), "] ", entry.Message) + } + return nil +} diff --git a/main.go b/main.go new file mode 100644 index 00000000..7fdaae14 --- /dev/null +++ b/main.go @@ -0,0 +1,65 @@ +package main + +import ( + "context" + "crypto/rand" + "encoding/binary" + "encoding/json" + mRand "math/rand" + "os" + "os/signal" + "syscall" + + "github.com/sagernet/sing-box/box" + "github.com/sagernet/sing-box/config" + "github.com/sagernet/sing/common" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +func init() { + logrus.StandardLogger().SetLevel(logrus.TraceLevel) + logrus.StandardLogger().Formatter.(*logrus.TextFormatter).ForceColors = true + var seed int64 + common.Must(binary.Read(rand.Reader, binary.LittleEndian, &seed)) + mRand.Seed(seed) +} + +var configPath string + +func main() { + command := &cobra.Command{ + Use: "sing-box", + Run: run, + } + command.Flags().StringVarP(&configPath, "config", "c", "config.json", "set configuration file path") + if err := command.Execute(); err != nil { + logrus.Fatal(err) + } +} + +func run(cmd *cobra.Command, args []string) { + configContent, err := os.ReadFile(configPath) + if err != nil { + logrus.Fatal("read config: ", err) + } + var boxConfig config.Config + err = json.Unmarshal(configContent, &boxConfig) + if err != nil { + logrus.Fatal("parse config: ", err) + } + ctx, cancel := context.WithCancel(context.Background()) + service, err := box.NewService(ctx, &boxConfig) + if err != nil { + logrus.Fatal("create service: ", err) + } + err = service.Start() + if err != nil { + logrus.Fatal("start service: ", err) + } + osSignals := make(chan os.Signal, 1) + signal.Notify(osSignals, os.Interrupt, syscall.SIGTERM) + <-osSignals + cancel() + service.Close() +} diff --git a/route/router.go b/route/router.go new file mode 100644 index 00000000..157db37f --- /dev/null +++ b/route/router.go @@ -0,0 +1,58 @@ +package route + +import ( + "context" + "net" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/log" + N "github.com/sagernet/sing/common/network" +) + +var _ adapter.Router = (*Router)(nil) + +type Router struct { + logger log.Logger + defaultOutbound adapter.Outbound + outboundByTag map[string]adapter.Outbound +} + +func NewRouter(logger log.Logger) *Router { + return &Router{ + logger: logger, + outboundByTag: make(map[string]adapter.Outbound), + } +} + +func (r *Router) AddOutbound(outbound adapter.Outbound) { + if outbound.Tag() != "" { + r.outboundByTag[outbound.Tag()] = outbound + } + if r.defaultOutbound == nil { + r.defaultOutbound = outbound + } +} + +func (r *Router) DefaultOutbound() adapter.Outbound { + if r.defaultOutbound == nil { + panic("missing default outbound") + } + return r.defaultOutbound +} + +func (r *Router) Outbound(tag string) (adapter.Outbound, bool) { + outbound, loaded := r.outboundByTag[tag] + return outbound, loaded +} + +func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { + r.logger.WithContext(ctx).Debug("no match") + r.logger.WithContext(ctx).Debug("route connection to default outbound") + return r.defaultOutbound.NewConnection(ctx, conn, metadata.Destination) +} + +func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { + r.logger.WithContext(ctx).Debug("no match") + r.logger.WithContext(ctx).Debug("route packet connection to default outbound") + return r.defaultOutbound.NewPacketConnection(ctx, conn, metadata.Destination) +}