mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-04-13 20:28:32 +10:00
Compare commits
70 Commits
dev-mitm-3
...
v1.12.0-be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf9c0ea0d5 | ||
|
|
63d7444512 | ||
|
|
3533a86e53 | ||
|
|
80d55f3d55 | ||
|
|
cd506f1e50 | ||
|
|
5990cc18ff | ||
|
|
a44c7ffb91 | ||
|
|
8292ac1043 | ||
|
|
30c1c364b3 | ||
|
|
a64c520de2 | ||
|
|
74bfea713d | ||
|
|
63f10d37ff | ||
|
|
8c231fbc38 | ||
|
|
1cfbee8293 | ||
|
|
529d41ed6a | ||
|
|
8af464eb7e | ||
|
|
3766bbbf9d | ||
|
|
0d65af5d0a | ||
|
|
439f3c05ec | ||
|
|
e3a1e71e4d | ||
|
|
83c284749e | ||
|
|
d95fc51be4 | ||
|
|
6cccfafc10 | ||
|
|
650e85e684 | ||
|
|
edfd6fb29d | ||
|
|
452ca3f5e6 | ||
|
|
693da37d62 | ||
|
|
4f902b8507 | ||
|
|
de9ceb82bb | ||
|
|
112508ccbb | ||
|
|
6fb224dd05 | ||
|
|
683c5b71ed | ||
|
|
174b857658 | ||
|
|
5d63c7a0da | ||
|
|
09f89b4181 | ||
|
|
c9522fd6d6 | ||
|
|
9e9886b140 | ||
|
|
f5dc2ec1dc | ||
|
|
e0202da833 | ||
|
|
db01fe90e4 | ||
|
|
104ea172c0 | ||
|
|
341958d7c1 | ||
|
|
05fea2a199 | ||
|
|
cc294c4616 | ||
|
|
b99c6a0025 | ||
|
|
845138a1d8 | ||
|
|
0645ebe73f | ||
|
|
1847cb6dfb | ||
|
|
1dd716453d | ||
|
|
456eb3dcdc | ||
|
|
8f9454ce72 | ||
|
|
3bae0c96bc | ||
|
|
0153fc7e08 | ||
|
|
a52ee299e6 | ||
|
|
bf0e71f32a | ||
|
|
b2dcb4dc03 | ||
|
|
221c003ce0 | ||
|
|
8b7c8dcdb4 | ||
|
|
360b25e53c | ||
|
|
6c9e61a0a0 | ||
|
|
572ee775b1 | ||
|
|
4f98009a15 | ||
|
|
0d54aee584 | ||
|
|
f4c29840c3 | ||
|
|
47fc3ebda4 | ||
|
|
9774a659b0 | ||
|
|
2e4a6de4e7 | ||
|
|
a530e424e9 | ||
|
|
0bfd487ee9 | ||
|
|
6aae834493 |
9
.github/workflows/build.yml
vendored
9
.github/workflows/build.yml
vendored
@@ -176,6 +176,9 @@ jobs:
|
|||||||
PKG_NAME="sing-box_${{ needs.calculate_version.outputs.version }}_${{ matrix.os }}_${{ matrix.arch }}${ARM_VERSION}"
|
PKG_NAME="sing-box_${{ needs.calculate_version.outputs.version }}_${{ matrix.os }}_${{ matrix.arch }}${ARM_VERSION}"
|
||||||
echo "DIR_NAME=${DIR_NAME}" >> "${GITHUB_ENV}"
|
echo "DIR_NAME=${DIR_NAME}" >> "${GITHUB_ENV}"
|
||||||
echo "PKG_NAME=${PKG_NAME}" >> "${GITHUB_ENV}"
|
echo "PKG_NAME=${PKG_NAME}" >> "${GITHUB_ENV}"
|
||||||
|
PKG_VERSION="${{ needs.calculate_version.outputs.version }}"
|
||||||
|
PKG_VERSION="${PKG_VERSION//-/\~}"
|
||||||
|
echo "PKG_VERSION=${PKG_VERSION}" >> "${GITHUB_ENV}"
|
||||||
- name: Package DEB
|
- name: Package DEB
|
||||||
if: matrix.debian != ''
|
if: matrix.debian != ''
|
||||||
run: |
|
run: |
|
||||||
@@ -183,7 +186,7 @@ jobs:
|
|||||||
sudo gem install fpm
|
sudo gem install fpm
|
||||||
sudo apt-get install -y debsigs
|
sudo apt-get install -y debsigs
|
||||||
fpm -t deb \
|
fpm -t deb \
|
||||||
-v "${{ needs.calculate_version.outputs.version }}" \
|
-v "$PKG_VERSION" \
|
||||||
-p "dist/${PKG_NAME}.deb" \
|
-p "dist/${PKG_NAME}.deb" \
|
||||||
--architecture ${{ matrix.debian }} \
|
--architecture ${{ matrix.debian }} \
|
||||||
dist/sing-box=/usr/bin/sing-box
|
dist/sing-box=/usr/bin/sing-box
|
||||||
@@ -200,7 +203,7 @@ jobs:
|
|||||||
set -xeuo pipefail
|
set -xeuo pipefail
|
||||||
sudo gem install fpm
|
sudo gem install fpm
|
||||||
fpm -t rpm \
|
fpm -t rpm \
|
||||||
-v "${{ needs.calculate_version.outputs.version }}" \
|
-v "$PKG_VERSION" \
|
||||||
-p "dist/${PKG_NAME}.rpm" \
|
-p "dist/${PKG_NAME}.rpm" \
|
||||||
--architecture ${{ matrix.rpm }} \
|
--architecture ${{ matrix.rpm }} \
|
||||||
dist/sing-box=/usr/bin/sing-box
|
dist/sing-box=/usr/bin/sing-box
|
||||||
@@ -219,7 +222,7 @@ jobs:
|
|||||||
sudo gem install fpm
|
sudo gem install fpm
|
||||||
sudo apt-get install -y libarchive-tools
|
sudo apt-get install -y libarchive-tools
|
||||||
fpm -t pacman \
|
fpm -t pacman \
|
||||||
-v "${{ needs.calculate_version.outputs.version }}" \
|
-v "$PKG_VERSION" \
|
||||||
-p "dist/${PKG_NAME}.pkg.tar.zst" \
|
-p "dist/${PKG_NAME}.pkg.tar.zst" \
|
||||||
--architecture ${{ matrix.pacman }} \
|
--architecture ${{ matrix.pacman }} \
|
||||||
dist/sing-box=/usr/bin/sing-box
|
dist/sing-box=/usr/bin/sing-box
|
||||||
|
|||||||
9
.github/workflows/linux.yml
vendored
9
.github/workflows/linux.yml
vendored
@@ -109,6 +109,11 @@ jobs:
|
|||||||
if: contains(needs.calculate_version.outputs.version, '-')
|
if: contains(needs.calculate_version.outputs.version, '-')
|
||||||
run: |-
|
run: |-
|
||||||
echo "NAME=sing-box-beta" >> "$GITHUB_ENV"
|
echo "NAME=sing-box-beta" >> "$GITHUB_ENV"
|
||||||
|
- name: Set version
|
||||||
|
run: |-
|
||||||
|
PKG_VERSION="${{ needs.calculate_version.outputs.version }}"
|
||||||
|
PKG_VERSION="${PKG_VERSION//-/\~}"
|
||||||
|
echo "PKG_VERSION=${PKG_VERSION}" >> "${GITHUB_ENV}"
|
||||||
- name: Package DEB
|
- name: Package DEB
|
||||||
if: matrix.debian != ''
|
if: matrix.debian != ''
|
||||||
run: |
|
run: |
|
||||||
@@ -117,7 +122,7 @@ jobs:
|
|||||||
sudo apt-get install -y debsigs
|
sudo apt-get install -y debsigs
|
||||||
fpm -t deb \
|
fpm -t deb \
|
||||||
--name "${NAME}" \
|
--name "${NAME}" \
|
||||||
-v "${{ needs.calculate_version.outputs.version }}" \
|
-v "$PKG_VERSION" \
|
||||||
-p "dist/${NAME}_${{ needs.calculate_version.outputs.version }}_linux_${{ matrix.debian }}.deb" \
|
-p "dist/${NAME}_${{ needs.calculate_version.outputs.version }}_linux_${{ matrix.debian }}.deb" \
|
||||||
--architecture ${{ matrix.debian }} \
|
--architecture ${{ matrix.debian }} \
|
||||||
dist/sing-box=/usr/bin/sing-box
|
dist/sing-box=/usr/bin/sing-box
|
||||||
@@ -135,7 +140,7 @@ jobs:
|
|||||||
sudo gem install fpm
|
sudo gem install fpm
|
||||||
fpm -t rpm \
|
fpm -t rpm \
|
||||||
--name "${NAME}" \
|
--name "${NAME}" \
|
||||||
-v "${{ needs.calculate_version.outputs.version }}" \
|
-v "$PKG_VERSION" \
|
||||||
-p "dist/${NAME}_${{ needs.calculate_version.outputs.version }}_linux_${{ matrix.rpm }}.rpm" \
|
-p "dist/${NAME}_${{ needs.calculate_version.outputs.version }}_linux_${{ matrix.rpm }}.rpm" \
|
||||||
--architecture ${{ matrix.rpm }} \
|
--architecture ${{ matrix.rpm }} \
|
||||||
dist/sing-box=/usr/bin/sing-box
|
dist/sing-box=/usr/bin/sing-box
|
||||||
|
|||||||
@@ -10,9 +10,6 @@ import (
|
|||||||
type CertificateStore interface {
|
type CertificateStore interface {
|
||||||
LifecycleService
|
LifecycleService
|
||||||
Pool() *x509.CertPool
|
Pool() *x509.CertPool
|
||||||
TLSDecryptionEnabled() bool
|
|
||||||
TLSDecryptionCertificate() *x509.Certificate
|
|
||||||
TLSDecryptionPrivateKey() any
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func RootPoolFromContext(ctx context.Context) *x509.CertPool {
|
func RootPoolFromContext(ctx context.Context) *x509.CertPool {
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ package adapter
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
|
||||||
"net/http"
|
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -60,8 +58,6 @@ type InboundContext struct {
|
|||||||
Client string
|
Client string
|
||||||
SniffContext any
|
SniffContext any
|
||||||
PacketSniffError error
|
PacketSniffError error
|
||||||
HTTPRequest *http.Request
|
|
||||||
ClientHello *tls.ClientHelloInfo
|
|
||||||
|
|
||||||
// cache
|
// cache
|
||||||
|
|
||||||
@@ -78,7 +74,6 @@ type InboundContext struct {
|
|||||||
UDPTimeout time.Duration
|
UDPTimeout time.Duration
|
||||||
TLSFragment bool
|
TLSFragment bool
|
||||||
TLSFragmentFallbackDelay time.Duration
|
TLSFragmentFallbackDelay time.Duration
|
||||||
MITM *option.MITMRouteOptions
|
|
||||||
|
|
||||||
NetworkStrategy *C.NetworkStrategy
|
NetworkStrategy *C.NetworkStrategy
|
||||||
NetworkType []C.InterfaceType
|
NetworkType []C.InterfaceType
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package adapter
|
package adapter
|
||||||
|
|
||||||
import (
|
import E "github.com/sagernet/sing/common/exceptions"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
)
|
|
||||||
|
|
||||||
type StartStage uint8
|
type StartStage uint8
|
||||||
|
|
||||||
@@ -47,9 +45,6 @@ type LifecycleService interface {
|
|||||||
|
|
||||||
func Start(stage StartStage, services ...Lifecycle) error {
|
func Start(stage StartStage, services ...Lifecycle) error {
|
||||||
for _, service := range services {
|
for _, service := range services {
|
||||||
if service == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
err := service.Start(stage)
|
err := service.Start(stage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
package adapter
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net"
|
|
||||||
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
)
|
|
||||||
|
|
||||||
type MITMEngine interface {
|
|
||||||
Lifecycle
|
|
||||||
NewConnection(ctx context.Context, this N.Dialer, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc)
|
|
||||||
}
|
|
||||||
43
box.go
43
box.go
@@ -23,7 +23,6 @@ import (
|
|||||||
"github.com/sagernet/sing-box/experimental/cachefile"
|
"github.com/sagernet/sing-box/experimental/cachefile"
|
||||||
"github.com/sagernet/sing-box/experimental/libbox/platform"
|
"github.com/sagernet/sing-box/experimental/libbox/platform"
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
"github.com/sagernet/sing-box/mitm"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
"github.com/sagernet/sing-box/protocol/direct"
|
"github.com/sagernet/sing-box/protocol/direct"
|
||||||
"github.com/sagernet/sing-box/route"
|
"github.com/sagernet/sing-box/route"
|
||||||
@@ -49,7 +48,6 @@ type Box struct {
|
|||||||
dnsRouter *dns.Router
|
dnsRouter *dns.Router
|
||||||
connection *route.ConnectionManager
|
connection *route.ConnectionManager
|
||||||
router *route.Router
|
router *route.Router
|
||||||
mitm adapter.MITMEngine //*mitm.Engine
|
|
||||||
services []adapter.LifecycleService
|
services []adapter.LifecycleService
|
||||||
done chan struct{}
|
done chan struct{}
|
||||||
}
|
}
|
||||||
@@ -145,12 +143,18 @@ func New(options Options) (*Box, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var services []adapter.LifecycleService
|
var services []adapter.LifecycleService
|
||||||
certificateStore, err := certificate.NewStore(ctx, logFactory.NewLogger("certificate"), common.PtrValueOrDefault(options.Certificate))
|
certificateOptions := common.PtrValueOrDefault(options.Certificate)
|
||||||
if err != nil {
|
if C.IsAndroid || certificateOptions.Store != "" && certificateOptions.Store != C.CertificateStoreSystem ||
|
||||||
return nil, err
|
len(certificateOptions.Certificate) > 0 ||
|
||||||
|
len(certificateOptions.CertificatePath) > 0 ||
|
||||||
|
len(certificateOptions.CertificateDirectoryPath) > 0 {
|
||||||
|
certificateStore, err := certificate.NewStore(ctx, logFactory.NewLogger("certificate"), certificateOptions)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
service.MustRegister[adapter.CertificateStore](ctx, certificateStore)
|
||||||
|
services = append(services, certificateStore)
|
||||||
}
|
}
|
||||||
service.MustRegister[adapter.CertificateStore](ctx, certificateStore)
|
|
||||||
services = append(services, certificateStore)
|
|
||||||
|
|
||||||
routeOptions := common.PtrValueOrDefault(options.Route)
|
routeOptions := common.PtrValueOrDefault(options.Route)
|
||||||
dnsOptions := common.PtrValueOrDefault(options.DNS)
|
dnsOptions := common.PtrValueOrDefault(options.DNS)
|
||||||
@@ -169,7 +173,7 @@ func New(options Options) (*Box, error) {
|
|||||||
return nil, E.Cause(err, "initialize network manager")
|
return nil, E.Cause(err, "initialize network manager")
|
||||||
}
|
}
|
||||||
service.MustRegister[adapter.NetworkManager](ctx, networkManager)
|
service.MustRegister[adapter.NetworkManager](ctx, networkManager)
|
||||||
connectionManager := route.NewConnectionManager(ctx, logFactory.NewLogger("connection"))
|
connectionManager := route.NewConnectionManager(logFactory.NewLogger("connection"))
|
||||||
service.MustRegister[adapter.ConnectionManager](ctx, connectionManager)
|
service.MustRegister[adapter.ConnectionManager](ctx, connectionManager)
|
||||||
router := route.NewRouter(ctx, logFactory, routeOptions, dnsOptions)
|
router := route.NewRouter(ctx, logFactory, routeOptions, dnsOptions)
|
||||||
service.MustRegister[adapter.Router](ctx, router)
|
service.MustRegister[adapter.Router](ctx, router)
|
||||||
@@ -177,8 +181,8 @@ func New(options Options) (*Box, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Cause(err, "initialize router")
|
return nil, E.Cause(err, "initialize router")
|
||||||
}
|
}
|
||||||
var timeService *tls.TimeServiceWrapper
|
|
||||||
ntpOptions := common.PtrValueOrDefault(options.NTP)
|
ntpOptions := common.PtrValueOrDefault(options.NTP)
|
||||||
|
var timeService *tls.TimeServiceWrapper
|
||||||
if ntpOptions.Enabled {
|
if ntpOptions.Enabled {
|
||||||
timeService = new(tls.TimeServiceWrapper)
|
timeService = new(tls.TimeServiceWrapper)
|
||||||
service.MustRegister[ntp.TimeService](ctx, timeService)
|
service.MustRegister[ntp.TimeService](ctx, timeService)
|
||||||
@@ -341,16 +345,6 @@ func New(options Options) (*Box, error) {
|
|||||||
timeService.TimeService = ntpService
|
timeService.TimeService = ntpService
|
||||||
services = append(services, adapter.NewLifecycleService(ntpService, "ntp service"))
|
services = append(services, adapter.NewLifecycleService(ntpService, "ntp service"))
|
||||||
}
|
}
|
||||||
mitmOptions := common.PtrValueOrDefault(options.MITM)
|
|
||||||
var mitmEngine adapter.MITMEngine
|
|
||||||
if mitmOptions.Enabled {
|
|
||||||
engine, err := mitm.NewEngine(ctx, logFactory.NewLogger("mitm"), mitmOptions)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "create MITM engine")
|
|
||||||
}
|
|
||||||
service.MustRegister[adapter.MITMEngine](ctx, engine)
|
|
||||||
mitmEngine = engine
|
|
||||||
}
|
|
||||||
return &Box{
|
return &Box{
|
||||||
network: networkManager,
|
network: networkManager,
|
||||||
endpoint: endpointManager,
|
endpoint: endpointManager,
|
||||||
@@ -360,7 +354,6 @@ func New(options Options) (*Box, error) {
|
|||||||
dnsRouter: dnsRouter,
|
dnsRouter: dnsRouter,
|
||||||
connection: connectionManager,
|
connection: connectionManager,
|
||||||
router: router,
|
router: router,
|
||||||
mitm: mitmEngine,
|
|
||||||
createdAt: createdAt,
|
createdAt: createdAt,
|
||||||
logFactory: logFactory,
|
logFactory: logFactory,
|
||||||
logger: logFactory.Logger(),
|
logger: logFactory.Logger(),
|
||||||
@@ -419,11 +412,11 @@ func (s *Box) preStart() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = adapter.Start(adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.mitm, s.outbound, s.inbound, s.endpoint)
|
err = adapter.Start(adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = adapter.Start(adapter.StartStateStart, s.outbound, s.dnsTransport, s.dnsRouter, s.network, s.connection, s.router, s.mitm)
|
err = adapter.Start(adapter.StartStateStart, s.outbound, s.dnsTransport, s.dnsRouter, s.network, s.connection, s.router)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -447,7 +440,7 @@ func (s *Box) start() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = adapter.Start(adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.mitm, s.inbound, s.endpoint)
|
err = adapter.Start(adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.inbound, s.endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -455,7 +448,7 @@ func (s *Box) start() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = adapter.Start(adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.mitm, s.outbound, s.inbound, s.endpoint)
|
err = adapter.Start(adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -474,7 +467,7 @@ func (s *Box) Close() error {
|
|||||||
close(s.done)
|
close(s.done)
|
||||||
}
|
}
|
||||||
err := common.Close(
|
err := common.Close(
|
||||||
s.inbound, s.outbound, s.endpoint, s.mitm, s.router, s.connection, s.dnsRouter, s.dnsTransport, s.network,
|
s.inbound, s.outbound, s.endpoint, s.router, s.connection, s.dnsRouter, s.dnsTransport, s.network,
|
||||||
)
|
)
|
||||||
for _, lifecycleService := range s.services {
|
for _, lifecycleService := range s.services {
|
||||||
err = E.Append(err, lifecycleService.Close(), func(err error) error {
|
err = E.Append(err, lifecycleService.Close(), func(err error) error {
|
||||||
|
|||||||
Submodule clients/android updated: aefe3c0290...5659088bb3
@@ -27,11 +27,8 @@ func main() {
|
|||||||
)
|
)
|
||||||
if flagRunNightly {
|
if flagRunNightly {
|
||||||
var version badversion.Version
|
var version badversion.Version
|
||||||
version, err = build_shared.ReadTagVersionRev()
|
version, err = build_shared.ReadTagVersion()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if version.PreReleaseIdentifier == "" {
|
|
||||||
version.Patch++
|
|
||||||
}
|
|
||||||
versionStr = version.String()
|
versionStr = version.String()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,120 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/rsa"
|
|
||||||
"crypto/sha1"
|
|
||||||
"crypto/x509"
|
|
||||||
"crypto/x509/pkix"
|
|
||||||
"encoding/asn1"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/hex"
|
|
||||||
"encoding/pem"
|
|
||||||
"math/big"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/log"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
"github.com/sagernet/sing/common/json"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"software.sslmate.com/src/go-pkcs12"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
flagGenerateCAName string
|
|
||||||
flagGenerateCAPKCS12Password string
|
|
||||||
flagGenerateOutput string
|
|
||||||
)
|
|
||||||
|
|
||||||
var commandGenerateCAKeyPair = &cobra.Command{
|
|
||||||
Use: "ca-keypair",
|
|
||||||
Short: "Generate CA key pair",
|
|
||||||
Args: cobra.NoArgs,
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
err := generateCAKeyPair()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
commandGenerateCAKeyPair.Flags().StringVarP(&flagGenerateCAName, "name", "n", "", "Set custom CA name")
|
|
||||||
commandGenerateCAKeyPair.Flags().StringVarP(&flagGenerateCAPKCS12Password, "p12-password", "p", "", "Set custom PKCS12 password")
|
|
||||||
commandGenerateCAKeyPair.Flags().StringVarP(&flagGenerateOutput, "output", "o", ".", "Set output directory")
|
|
||||||
commandGenerate.AddCommand(commandGenerateCAKeyPair)
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateCAKeyPair() error {
|
|
||||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
|
||||||
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
spkiASN1, err := x509.MarshalPKIXPublicKey(privateKey.Public())
|
|
||||||
var spki struct {
|
|
||||||
Algorithm pkix.AlgorithmIdentifier
|
|
||||||
SubjectPublicKey asn1.BitString
|
|
||||||
}
|
|
||||||
_, err = asn1.Unmarshal(spkiASN1, &spki)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
skid := sha1.Sum(spki.SubjectPublicKey.Bytes)
|
|
||||||
var caName string
|
|
||||||
if flagGenerateCAName != "" {
|
|
||||||
caName = flagGenerateCAName
|
|
||||||
} else {
|
|
||||||
caName = "sing-box Generated CA " + strings.ToUpper(hex.EncodeToString(skid[:4]))
|
|
||||||
}
|
|
||||||
caTpl := &x509.Certificate{
|
|
||||||
SerialNumber: serialNumber,
|
|
||||||
Subject: pkix.Name{
|
|
||||||
Organization: []string{caName},
|
|
||||||
CommonName: caName,
|
|
||||||
},
|
|
||||||
SubjectKeyId: skid[:],
|
|
||||||
NotAfter: time.Now().AddDate(10, 0, 0),
|
|
||||||
NotBefore: time.Now(),
|
|
||||||
KeyUsage: x509.KeyUsageCertSign,
|
|
||||||
BasicConstraintsValid: true,
|
|
||||||
IsCA: true,
|
|
||||||
MaxPathLenZero: true,
|
|
||||||
}
|
|
||||||
publicDer, err := x509.CreateCertificate(rand.Reader, caTpl, caTpl, privateKey.Public(), privateKey)
|
|
||||||
var caPassword string
|
|
||||||
if flagGenerateCAPKCS12Password != "" {
|
|
||||||
caPassword = flagGenerateCAPKCS12Password
|
|
||||||
} else {
|
|
||||||
caPassword = strings.ToUpper(hex.EncodeToString(skid[:4]))
|
|
||||||
}
|
|
||||||
caTpl.Raw = publicDer
|
|
||||||
p12Bytes, err := pkcs12.Modern.Encode(privateKey, caTpl, nil, caPassword)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
privateDer, err := x509.MarshalPKCS8PrivateKey(privateKey)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
os.WriteFile(filepath.Join(flagGenerateOutput, caName+".pem"), []byte(string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: publicDer}))+string(pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privateDer}))), 0o644)
|
|
||||||
os.WriteFile(filepath.Join(flagGenerateOutput, caName+".crt"), publicDer, 0o644)
|
|
||||||
os.WriteFile(filepath.Join(flagGenerateOutput, caName+".p12"), p12Bytes, 0o644)
|
|
||||||
var tlsDecryptionOptions option.TLSDecryptionOptions
|
|
||||||
tlsDecryptionOptions.Enabled = true
|
|
||||||
tlsDecryptionOptions.KeyPair = base64.StdEncoding.EncodeToString(p12Bytes)
|
|
||||||
tlsDecryptionOptions.KeyPairPassword = caPassword
|
|
||||||
var certificateOptions option.CertificateOptions
|
|
||||||
certificateOptions.TLSDecryption = &tlsDecryptionOptions
|
|
||||||
encoder := json.NewEncoder(os.Stdout)
|
|
||||||
encoder.SetIndent("", " ")
|
|
||||||
return encoder.Encode(certificateOptions)
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,13 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -12,5 +19,36 @@ var commandTools = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
commandTools.PersistentFlags().StringVarP(&commandToolsFlagOutbound, "outbound", "o", "", "Use specified tag instead of default outbound")
|
||||||
mainCommand.AddCommand(commandTools)
|
mainCommand.AddCommand(commandTools)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func createPreStartedClient() (*box.Box, error) {
|
||||||
|
options, err := readConfigAndMerge()
|
||||||
|
if err != nil {
|
||||||
|
if !(errors.Is(err, os.ErrNotExist) && len(configDirectories) == 0 && len(configPaths) == 1) || configPaths[0] != "config.json" {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
instance, err := box.New(box.Options{Context: globalCtx, Options: options})
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "create service")
|
||||||
|
}
|
||||||
|
err = instance.PreStart()
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "start service")
|
||||||
|
}
|
||||||
|
return instance, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createDialer(instance *box.Box, outboundTag string) (N.Dialer, error) {
|
||||||
|
if outboundTag == "" {
|
||||||
|
return instance.Outbound().Default(), nil
|
||||||
|
} else {
|
||||||
|
outbound, loaded := instance.Outbound().Outbound(outboundTag)
|
||||||
|
if !loaded {
|
||||||
|
return nil, E.New("outbound not found: ", outboundTag)
|
||||||
|
}
|
||||||
|
return outbound, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
73
cmd/sing-box/cmd_tools_connect.go
Normal file
73
cmd/sing-box/cmd_tools_connect.go
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
"github.com/sagernet/sing/common/bufio"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
"github.com/sagernet/sing/common/task"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var commandConnectFlagNetwork string
|
||||||
|
|
||||||
|
var commandConnect = &cobra.Command{
|
||||||
|
Use: "connect <address>",
|
||||||
|
Short: "Connect to an address",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
err := connect(args[0])
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
commandConnect.Flags().StringVarP(&commandConnectFlagNetwork, "network", "n", "tcp", "network type")
|
||||||
|
commandTools.AddCommand(commandConnect)
|
||||||
|
}
|
||||||
|
|
||||||
|
func connect(address string) error {
|
||||||
|
switch N.NetworkName(commandConnectFlagNetwork) {
|
||||||
|
case N.NetworkTCP, N.NetworkUDP:
|
||||||
|
default:
|
||||||
|
return E.Cause(N.ErrUnknownNetwork, commandConnectFlagNetwork)
|
||||||
|
}
|
||||||
|
instance, err := createPreStartedClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer instance.Close()
|
||||||
|
dialer, err := createDialer(instance, commandToolsFlagOutbound)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
conn, err := dialer.DialContext(context.Background(), commandConnectFlagNetwork, M.ParseSocksaddr(address))
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "connect to server")
|
||||||
|
}
|
||||||
|
var group task.Group
|
||||||
|
group.Append("upload", func(ctx context.Context) error {
|
||||||
|
return common.Error(bufio.Copy(conn, os.Stdin))
|
||||||
|
})
|
||||||
|
group.Append("download", func(ctx context.Context) error {
|
||||||
|
return common.Error(bufio.Copy(os.Stdout, conn))
|
||||||
|
})
|
||||||
|
group.Cleanup(func() {
|
||||||
|
conn.Close()
|
||||||
|
})
|
||||||
|
err = group.Run(context.Background())
|
||||||
|
if E.IsClosed(err) {
|
||||||
|
log.Info(err)
|
||||||
|
} else {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
115
cmd/sing-box/cmd_tools_fetch.go
Normal file
115
cmd/sing-box/cmd_tools_fetch.go
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing/common/bufio"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var commandFetch = &cobra.Command{
|
||||||
|
Use: "fetch",
|
||||||
|
Short: "Fetch an URL",
|
||||||
|
Args: cobra.MinimumNArgs(1),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
err := fetch(args)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
commandTools.AddCommand(commandFetch)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
httpClient *http.Client
|
||||||
|
http3Client *http.Client
|
||||||
|
)
|
||||||
|
|
||||||
|
func fetch(args []string) error {
|
||||||
|
instance, err := createPreStartedClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer instance.Close()
|
||||||
|
httpClient = &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
|
dialer, err := createDialer(instance, commandToolsFlagOutbound)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
|
||||||
|
},
|
||||||
|
ForceAttemptHTTP2: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
defer httpClient.CloseIdleConnections()
|
||||||
|
if C.WithQUIC {
|
||||||
|
err = initializeHTTP3Client(instance)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer http3Client.CloseIdleConnections()
|
||||||
|
}
|
||||||
|
for _, urlString := range args {
|
||||||
|
var parsedURL *url.URL
|
||||||
|
parsedURL, err = url.Parse(urlString)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch parsedURL.Scheme {
|
||||||
|
case "":
|
||||||
|
parsedURL.Scheme = "http"
|
||||||
|
fallthrough
|
||||||
|
case "http", "https":
|
||||||
|
err = fetchHTTP(httpClient, parsedURL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case "http3":
|
||||||
|
if !C.WithQUIC {
|
||||||
|
return C.ErrQUICNotIncluded
|
||||||
|
}
|
||||||
|
parsedURL.Scheme = "https"
|
||||||
|
err = fetchHTTP(http3Client, parsedURL)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return E.New("unsupported scheme: ", parsedURL.Scheme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchHTTP(httpClient *http.Client, parsedURL *url.URL) error {
|
||||||
|
request, err := http.NewRequest("GET", parsedURL.String(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
request.Header.Add("User-Agent", "curl/7.88.0")
|
||||||
|
response, err := httpClient.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
_, err = bufio.Copy(os.Stdout, response.Body)
|
||||||
|
if errors.Is(err, io.EOF) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
36
cmd/sing-box/cmd_tools_fetch_http3.go
Normal file
36
cmd/sing-box/cmd_tools_fetch_http3.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
//go:build with_quic
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/sagernet/quic-go"
|
||||||
|
"github.com/sagernet/quic-go/http3"
|
||||||
|
box "github.com/sagernet/sing-box"
|
||||||
|
"github.com/sagernet/sing/common/bufio"
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
)
|
||||||
|
|
||||||
|
func initializeHTTP3Client(instance *box.Box) error {
|
||||||
|
dialer, err := createDialer(instance, commandToolsFlagOutbound)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
http3Client = &http.Client{
|
||||||
|
Transport: &http3.Transport{
|
||||||
|
Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
|
||||||
|
destination := M.ParseSocksaddr(addr)
|
||||||
|
udpConn, dErr := dialer.DialContext(ctx, N.NetworkUDP, destination)
|
||||||
|
if dErr != nil {
|
||||||
|
return nil, dErr
|
||||||
|
}
|
||||||
|
return quic.DialEarly(ctx, bufio.NewUnbindPacketConn(udpConn), udpConn.RemoteAddr(), tlsCfg, cfg)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
18
cmd/sing-box/cmd_tools_fetch_http3_stub.go
Normal file
18
cmd/sing-box/cmd_tools_fetch_http3_stub.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
//go:build !with_quic
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
box "github.com/sagernet/sing-box"
|
||||||
|
)
|
||||||
|
|
||||||
|
func initializeHTTP3Client(instance *box.Box) error {
|
||||||
|
return os.ErrInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchHTTP3(parsedURL *url.URL) error {
|
||||||
|
return os.ErrInvalid
|
||||||
|
}
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/pem"
|
|
||||||
"errors"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/log"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
"github.com/sagernet/sing/common/shell"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var commandInstallCACertificate = &cobra.Command{
|
|
||||||
Use: "install-ca <path to certificate>",
|
|
||||||
Short: "Install CA certificate to system",
|
|
||||||
Args: cobra.ExactArgs(1),
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
err := installCACertificate(args[0])
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
commandTools.AddCommand(commandInstallCACertificate)
|
|
||||||
}
|
|
||||||
|
|
||||||
func installCACertificate(path string) error {
|
|
||||||
switch runtime.GOOS {
|
|
||||||
case "windows":
|
|
||||||
return shell.Exec("powershell", "-Command", "Import-Certificate -FilePath \""+path+"\" -CertStoreLocation Cert:\\LocalMachine\\Root").Attach().Run()
|
|
||||||
case "darwin":
|
|
||||||
return shell.Exec("sudo", "security", "add-trusted-cert", "-d", "-r", "trustRoot", "-k", "/Library/Keychains/System.keychain", path).Attach().Run()
|
|
||||||
case "linux":
|
|
||||||
updateCertPath, updateCertPathNotFoundErr := exec.LookPath("update-ca-certificates")
|
|
||||||
if updateCertPathNotFoundErr == nil {
|
|
||||||
publicDer, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = os.MkdirAll("/usr/local/share/ca-certificates", 0o755)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, os.ErrPermission) {
|
|
||||||
log.Info("Try running with sudo")
|
|
||||||
return shell.Exec("sudo", os.Args...).Attach().Run()
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
fileName := filepath.Base(updateCertPath)
|
|
||||||
if !strings.HasSuffix(fileName, ".crt") {
|
|
||||||
fileName = fileName + ".crt"
|
|
||||||
}
|
|
||||||
filePath, _ := filepath.Abs(filepath.Join("/usr/local/share/ca-certificates", fileName))
|
|
||||||
err = os.WriteFile(filePath, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: publicDer}), 0o644)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, os.ErrPermission) {
|
|
||||||
log.Info("Try running with sudo")
|
|
||||||
return shell.Exec("sudo", os.Args...).Attach().Run()
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
log.Info("certificate written to " + filePath + "\n")
|
|
||||||
err = shell.Exec(updateCertPath).Attach().Run()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
log.Info("certificate installed")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
updateTrustPath, updateTrustPathNotFoundErr := exec.LookPath("update-ca-trust")
|
|
||||||
if updateTrustPathNotFoundErr == nil {
|
|
||||||
publicDer, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
fileName := filepath.Base(updateTrustPath)
|
|
||||||
fileExt := filepath.Ext(path)
|
|
||||||
if fileExt != "" {
|
|
||||||
fileName = fileName[:len(fileName)-len(fileExt)]
|
|
||||||
}
|
|
||||||
filePath, _ := filepath.Abs(filepath.Join("/etc/pki/ca-trust/source/anchors/", fileName+".pem"))
|
|
||||||
err = os.WriteFile(filePath, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: publicDer}), 0o644)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, os.ErrPermission) {
|
|
||||||
log.Info("Try running with sudo")
|
|
||||||
return shell.Exec("sudo", os.Args...).Attach().Run()
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
log.Info("certificate written to " + filePath + "\n")
|
|
||||||
err = shell.Exec(updateTrustPath, "extract").Attach().Run()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
log.Info("certificate installed")
|
|
||||||
}
|
|
||||||
return E.New("update-ca-certificates or update-ca-trust not found")
|
|
||||||
default:
|
|
||||||
return E.New("unsupported operating system: ", runtime.GOOS)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
"github.com/sagernet/sing/common/ntp"
|
"github.com/sagernet/sing/common/ntp"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -40,11 +39,20 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func syncTime() error {
|
func syncTime() error {
|
||||||
|
instance, err := createPreStartedClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dialer, err := createDialer(instance, commandToolsFlagOutbound)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer instance.Close()
|
||||||
serverAddress := M.ParseSocksaddr(commandSyncTimeFlagServer)
|
serverAddress := M.ParseSocksaddr(commandSyncTimeFlagServer)
|
||||||
if serverAddress.Port == 0 {
|
if serverAddress.Port == 0 {
|
||||||
serverAddress.Port = 123
|
serverAddress.Port = 123
|
||||||
}
|
}
|
||||||
response, err := ntp.Exchange(context.Background(), N.SystemDialer, serverAddress)
|
response, err := ntp.Exchange(context.Background(), dialer, serverAddress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package certificate
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/base64"
|
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -17,8 +16,6 @@ import (
|
|||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
"github.com/sagernet/sing/common/logger"
|
"github.com/sagernet/sing/common/logger"
|
||||||
"github.com/sagernet/sing/service"
|
"github.com/sagernet/sing/service"
|
||||||
|
|
||||||
"software.sslmate.com/src/go-pkcs12"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ adapter.CertificateStore = (*Store)(nil)
|
var _ adapter.CertificateStore = (*Store)(nil)
|
||||||
@@ -30,9 +27,6 @@ type Store struct {
|
|||||||
certificatePaths []string
|
certificatePaths []string
|
||||||
certificateDirectoryPaths []string
|
certificateDirectoryPaths []string
|
||||||
watcher *fswatch.Watcher
|
watcher *fswatch.Watcher
|
||||||
tlsDecryptionEnabled bool
|
|
||||||
tlsDecryptionPrivateKey any
|
|
||||||
tlsDecryptionCertificate *x509.Certificate
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewStore(ctx context.Context, logger logger.Logger, options option.CertificateOptions) (*Store, error) {
|
func NewStore(ctx context.Context, logger logger.Logger, options option.CertificateOptions) (*Store, error) {
|
||||||
@@ -96,19 +90,6 @@ func NewStore(ctx context.Context, logger logger.Logger, options option.Certific
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Cause(err, "initializing certificate store")
|
return nil, E.Cause(err, "initializing certificate store")
|
||||||
}
|
}
|
||||||
if options.TLSDecryption != nil && options.TLSDecryption.Enabled {
|
|
||||||
pfxBytes, err := base64.StdEncoding.DecodeString(options.TLSDecryption.KeyPair)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "decode key pair base64 bytes")
|
|
||||||
}
|
|
||||||
privateKey, certificate, err := pkcs12.Decode(pfxBytes, options.TLSDecryption.KeyPairPassword)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "decode key pair")
|
|
||||||
}
|
|
||||||
store.tlsDecryptionEnabled = true
|
|
||||||
store.tlsDecryptionPrivateKey = privateKey
|
|
||||||
store.tlsDecryptionCertificate = certificate
|
|
||||||
}
|
|
||||||
return store, nil
|
return store, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,15 +183,3 @@ func isSameDirSymlink(f fs.DirEntry, dir string) bool {
|
|||||||
target, err := os.Readlink(filepath.Join(dir, f.Name()))
|
target, err := os.Readlink(filepath.Join(dir, f.Name()))
|
||||||
return err == nil && !strings.Contains(target, "/")
|
return err == nil && !strings.Contains(target, "/")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) TLSDecryptionEnabled() bool {
|
|
||||||
return s.tlsDecryptionEnabled
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) TLSDecryptionCertificate() *x509.Certificate {
|
|
||||||
return s.tlsDecryptionCertificate
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) TLSDecryptionPrivateKey() any {
|
|
||||||
return s.tlsDecryptionPrivateKey
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ type Options struct {
|
|||||||
ResolverOnDetour bool
|
ResolverOnDetour bool
|
||||||
NewDialer bool
|
NewDialer bool
|
||||||
LegacyDNSDialer bool
|
LegacyDNSDialer bool
|
||||||
|
DirectOutbound bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: merge with NewWithOptions
|
// TODO: merge with NewWithOptions
|
||||||
@@ -102,13 +103,13 @@ func NewWithOptions(options Options) (N.Dialer, error) {
|
|||||||
}
|
}
|
||||||
dnsQueryOptions.Transport = transport
|
dnsQueryOptions.Transport = transport
|
||||||
resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay)
|
resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay)
|
||||||
} else if options.NewDialer {
|
|
||||||
return nil, E.New("missing domain resolver for domain server address")
|
|
||||||
} else {
|
} else {
|
||||||
transports := dnsTransport.Transports()
|
transports := dnsTransport.Transports()
|
||||||
if len(transports) < 2 {
|
if len(transports) < 2 {
|
||||||
dnsQueryOptions.Transport = dnsTransport.Default()
|
dnsQueryOptions.Transport = dnsTransport.Default()
|
||||||
} else {
|
} else if options.NewDialer {
|
||||||
|
return nil, E.New("missing domain resolver for domain server address")
|
||||||
|
} else if !options.DirectOutbound {
|
||||||
deprecated.Report(options.Context, deprecated.OptionMissingDomainResolver)
|
deprecated.Report(options.Context, deprecated.OptionMissingDomainResolver)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import (
|
|||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
"github.com/sagernet/sing/common/buf"
|
"github.com/sagernet/sing/common/buf"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
|
||||||
"github.com/sagernet/sing/common/task"
|
"github.com/sagernet/sing/common/task"
|
||||||
|
|
||||||
mDNS "github.com/miekg/dns"
|
mDNS "github.com/miekg/dns"
|
||||||
@@ -47,9 +46,6 @@ func DomainNameQuery(ctx context.Context, metadata *adapter.InboundContext, pack
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if len(msg.Question) == 0 || msg.Question[0].Qclass != mDNS.ClassINET || !M.IsDomainName(msg.Question[0].Name) {
|
|
||||||
return os.ErrInvalid
|
|
||||||
}
|
|
||||||
metadata.Protocol = C.ProtocolDNS
|
metadata.Protocol = C.ProtocolDNS
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
23
common/sniff/dns_test.go
Normal file
23
common/sniff/dns_test.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package sniff_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/hex"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/common/sniff"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSniffDNS(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
query, err := hex.DecodeString("740701000001000000000000012a06676f6f676c6503636f6d0000010001")
|
||||||
|
require.NoError(t, err)
|
||||||
|
var metadata adapter.InboundContext
|
||||||
|
err = sniff.DomainNameQuery(context.TODO(), &metadata, query)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, C.ProtocolDNS, metadata.Protocol)
|
||||||
|
}
|
||||||
@@ -18,6 +18,5 @@ func HTTPHost(_ context.Context, metadata *adapter.InboundContext, reader io.Rea
|
|||||||
}
|
}
|
||||||
metadata.Protocol = C.ProtocolHTTP
|
metadata.Protocol = C.ProtocolHTTP
|
||||||
metadata.Domain = M.ParseSocksaddr(request.Host).AddrString()
|
metadata.Domain = M.ParseSocksaddr(request.Host).AddrString()
|
||||||
metadata.HTTPRequest = request
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ func TLSClientHello(ctx context.Context, metadata *adapter.InboundContext, reade
|
|||||||
if clientHello != nil {
|
if clientHello != nil {
|
||||||
metadata.Protocol = C.ProtocolTLS
|
metadata.Protocol = C.ProtocolTLS
|
||||||
metadata.Domain = clientHello.ServerName
|
metadata.Domain = clientHello.ServerName
|
||||||
metadata.ClientHello = clientHello
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -8,10 +8,7 @@ import (
|
|||||||
"crypto/x509/pkix"
|
"crypto/x509/pkix"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"math/big"
|
"math/big"
|
||||||
"net"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func GenerateKeyPair(parent *x509.Certificate, parentKey any, timeFunc func() time.Time, serverName string) (*tls.Certificate, error) {
|
func GenerateKeyPair(parent *x509.Certificate, parentKey any, timeFunc func() time.Time, serverName string) (*tls.Certificate, error) {
|
||||||
@@ -38,30 +35,17 @@ func GenerateCertificate(parent *x509.Certificate, parentKey any, timeFunc func(
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var template *x509.Certificate
|
template := &x509.Certificate{
|
||||||
if serverAddress := M.ParseAddr(serverName); serverAddress.IsValid() {
|
SerialNumber: serialNumber,
|
||||||
template = &x509.Certificate{
|
NotBefore: timeFunc().Add(time.Hour * -1),
|
||||||
SerialNumber: serialNumber,
|
NotAfter: expire,
|
||||||
IPAddresses: []net.IP{serverAddress.AsSlice()},
|
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||||
NotBefore: timeFunc().Add(time.Hour * -1),
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||||
NotAfter: expire,
|
BasicConstraintsValid: true,
|
||||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
Subject: pkix.Name{
|
||||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
CommonName: serverName,
|
||||||
BasicConstraintsValid: true,
|
},
|
||||||
}
|
DNSNames: []string{serverName},
|
||||||
} else {
|
|
||||||
template = &x509.Certificate{
|
|
||||||
SerialNumber: serialNumber,
|
|
||||||
NotBefore: timeFunc().Add(time.Hour * -1),
|
|
||||||
NotAfter: expire,
|
|
||||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
|
||||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
|
||||||
BasicConstraintsValid: true,
|
|
||||||
Subject: pkix.Name{
|
|
||||||
CommonName: serverName,
|
|
||||||
},
|
|
||||||
DNSNames: []string{serverName},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if parent == nil {
|
if parent == nil {
|
||||||
parent = template
|
parent = template
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ func TruncateDNSMessage(request *dns.Msg, response *dns.Msg, headroom int) (*buf
|
|||||||
}
|
}
|
||||||
responseLen := response.Len()
|
responseLen := response.Len()
|
||||||
if responseLen > maxLen {
|
if responseLen > maxLen {
|
||||||
|
copyResponse := *response
|
||||||
|
response = ©Response
|
||||||
response.Truncate(maxLen)
|
response.Truncate(maxLen)
|
||||||
}
|
}
|
||||||
buffer := buf.NewSize(headroom*2 + 1 + responseLen)
|
buffer := buf.NewSize(headroom*2 + 1 + responseLen)
|
||||||
|
|||||||
@@ -140,12 +140,12 @@ func (t *Transport) exchange(ctx context.Context, message *mDNS.Msg, conn quic.C
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer stream.Close()
|
|
||||||
defer stream.CancelRead(0)
|
|
||||||
err = transport.WriteMessage(stream, 0, message)
|
err = transport.WriteMessage(stream, 0, message)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
stream.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
stream.Close()
|
||||||
return transport.ReadMessage(stream)
|
return transport.ReadMessage(stream)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,22 @@
|
|||||||
icon: material/alert-decagram
|
icon: material/alert-decagram
|
||||||
---
|
---
|
||||||
|
|
||||||
#### 1.12.0-alpha.20
|
#### 1.12.0-beta.1
|
||||||
|
|
||||||
|
* Improve `auto_redirect` **1**
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
**1**:
|
||||||
|
|
||||||
|
Now `auto_redirect` fixes compatibility issues between tun and Docker bridge networks,
|
||||||
|
see [Tun](/configuration/inbound/tun/#auto_redirect).
|
||||||
|
|
||||||
|
### 1.11.6
|
||||||
|
|
||||||
* Fixes and improvements
|
* Fixes and improvements
|
||||||
|
|
||||||
|
_We are temporarily unable to update sing-box apps on the App Store because the reviewer mistakenly found that we violated the rules (TestFlight users are not affected)._
|
||||||
|
|
||||||
#### 1.12.0-alpha.19
|
#### 1.12.0-alpha.19
|
||||||
|
|
||||||
* Update gVisor to 20250319.0
|
* Update gVisor to 20250319.0
|
||||||
|
|||||||
@@ -44,10 +44,10 @@ Default padding scheme:
|
|||||||
|
|
||||||
```
|
```
|
||||||
stop=8
|
stop=8
|
||||||
0=34-120
|
0=30-30
|
||||||
1=100-400
|
1=100-400
|
||||||
2=400-500,c,500-1000,c,400-500,c,500-1000,c,500-1000,c,400-500
|
2=400-500,c,500-1000,c,500-1000,c,500-1000,c,500-1000
|
||||||
3=500-1000
|
3=9-9,500-1000
|
||||||
4=500-1000
|
4=500-1000
|
||||||
5=500-1000
|
5=500-1000
|
||||||
6=500-1000
|
6=500-1000
|
||||||
|
|||||||
@@ -44,10 +44,10 @@ AnyTLS 填充方案行数组。
|
|||||||
|
|
||||||
```
|
```
|
||||||
stop=8
|
stop=8
|
||||||
0=34-120
|
0=30-30
|
||||||
1=100-400
|
1=100-400
|
||||||
2=400-500,c,500-1000,c,400-500,c,500-1000,c,500-1000,c,400-500
|
2=400-500,c,500-1000,c,500-1000,c,500-1000,c,500-1000
|
||||||
3=500-1000
|
3=9-9,500-1000
|
||||||
4=500-1000
|
4=500-1000
|
||||||
5=500-1000
|
5=500-1000
|
||||||
6=500-1000
|
6=500-1000
|
||||||
|
|||||||
@@ -211,6 +211,10 @@ Set the default route to the Tun.
|
|||||||
|
|
||||||
By default, VPN takes precedence over tun. To make tun go through VPN, enable `route.override_android_vpn`.
|
By default, VPN takes precedence over tun. To make tun go through VPN, enable `route.override_android_vpn`.
|
||||||
|
|
||||||
|
!!! note "Also enable `auto_redirect`"
|
||||||
|
|
||||||
|
`auto_redirect` is always recommended on Linux, it provides better routing, higher performance (better than tproxy), and avoids conflicts with Docker bridge networks.
|
||||||
|
|
||||||
#### iproute2_table_index
|
#### iproute2_table_index
|
||||||
|
|
||||||
!!! question "Since sing-box 1.10.0"
|
!!! question "Since sing-box 1.10.0"
|
||||||
@@ -237,6 +241,10 @@ Linux iproute2 rule start index generated by `auto_route`.
|
|||||||
|
|
||||||
Automatically configure iptables/nftables to redirect connections.
|
Automatically configure iptables/nftables to redirect connections.
|
||||||
|
|
||||||
|
Auto redirect is always recommended on Linux, it provides better routing,
|
||||||
|
higher performance (better than tproxy),
|
||||||
|
and avoids conflicts with Docker bridge networks.
|
||||||
|
|
||||||
*In Android*:
|
*In Android*:
|
||||||
|
|
||||||
Only local IPv4 connections are forwarded. To share your VPN connection over hotspot or repeater,
|
Only local IPv4 connections are forwarded. To share your VPN connection over hotspot or repeater,
|
||||||
@@ -246,11 +254,13 @@ use [VPNHotspot](https://github.com/Mygod/VPNHotspot).
|
|||||||
|
|
||||||
`auto_route` with `auto_redirect` works as expected on routers **without intervention**.
|
`auto_route` with `auto_redirect` works as expected on routers **without intervention**.
|
||||||
|
|
||||||
|
Conflict with `route.default_mark` and `[dialOptions].routing_mark`.
|
||||||
|
|
||||||
#### auto_redirect_input_mark
|
#### auto_redirect_input_mark
|
||||||
|
|
||||||
!!! question "Since sing-box 1.10.0"
|
!!! question "Since sing-box 1.10.0"
|
||||||
|
|
||||||
Connection input mark used by `route[_exclude]_address_set` with `auto_redirect`.
|
Connection input mark used by `auto_redirect`.
|
||||||
|
|
||||||
`0x2023` is used by default.
|
`0x2023` is used by default.
|
||||||
|
|
||||||
@@ -258,7 +268,7 @@ Connection input mark used by `route[_exclude]_address_set` with `auto_redirect`
|
|||||||
|
|
||||||
!!! question "Since sing-box 1.10.0"
|
!!! question "Since sing-box 1.10.0"
|
||||||
|
|
||||||
Connection input mark used by `route[_exclude]_address_set` with `auto_redirect`.
|
Connection output mark used by `auto_redirect`.
|
||||||
|
|
||||||
`0x2024` is used by default.
|
`0x2024` is used by default.
|
||||||
|
|
||||||
@@ -367,8 +377,6 @@ Exclude custom routes when `auto_route` is enabled.
|
|||||||
|
|
||||||
Add the destination IP CIDR rules in the specified rule-sets to the firewall.
|
Add the destination IP CIDR rules in the specified rule-sets to the firewall.
|
||||||
Matched traffic will bypass the sing-box routes.
|
Matched traffic will bypass the sing-box routes.
|
||||||
|
|
||||||
Conflict with `route.default_mark` and `[dialOptions].routing_mark`.
|
|
||||||
|
|
||||||
=== "Without `auto_redirect` enabled"
|
=== "Without `auto_redirect` enabled"
|
||||||
|
|
||||||
|
|||||||
@@ -215,6 +215,10 @@ tun 接口的 IPv6 前缀。
|
|||||||
|
|
||||||
VPN 默认优先于 tun。要使 tun 经过 VPN,启用 `route.override_android_vpn`。
|
VPN 默认优先于 tun。要使 tun 经过 VPN,启用 `route.override_android_vpn`。
|
||||||
|
|
||||||
|
!!! note "也启用 `auto_redirect`"
|
||||||
|
|
||||||
|
在 Linux 上始终推荐使用 `auto_redirect`,它提供更好的路由, 更高的性能(优于 tproxy), 并避免与 Docker 桥接网络冲突。
|
||||||
|
|
||||||
#### iproute2_table_index
|
#### iproute2_table_index
|
||||||
|
|
||||||
!!! question "自 sing-box 1.10.0 起"
|
!!! question "自 sing-box 1.10.0 起"
|
||||||
@@ -241,19 +245,23 @@ tun 接口的 IPv6 前缀。
|
|||||||
|
|
||||||
自动配置 iptables/nftables 以重定向连接。
|
自动配置 iptables/nftables 以重定向连接。
|
||||||
|
|
||||||
|
在 Linux 上始终推荐使用 auto redirect,它提供更好的路由, 更高的性能(优于 tproxy), 并避免与 Docker 桥接网络冲突。
|
||||||
|
|
||||||
*在 Android 中*:
|
*在 Android 中*:
|
||||||
|
|
||||||
仅转发本地 IPv4 连接。 要通过热点或中继共享您的 VPN 连接,请使用 [VPNHotspot](https://github.com/Mygod/VPNHotspot)。
|
仅转发本地 IPv4 连接。 要通过热点或中继共享您的 VPN 连接,请使用 [VPNHotspot](https://github.com/Mygod/VPNHotspot)。
|
||||||
|
|
||||||
*在 Linux 中*:
|
*在 Linux 中*:
|
||||||
|
|
||||||
带有 `auto_redirect `的 `auto_route` 可以在路由器上按预期工作,**无需干预**。
|
带有 `auto_redirect` 的 `auto_route` 在路由器上**无需干预**即可按预期工作。
|
||||||
|
|
||||||
|
与 `route.default_mark` 和 `[dialOptions].routing_mark` 冲突。
|
||||||
|
|
||||||
#### auto_redirect_input_mark
|
#### auto_redirect_input_mark
|
||||||
|
|
||||||
!!! question "自 sing-box 1.10.0 起"
|
!!! question "自 sing-box 1.10.0 起"
|
||||||
|
|
||||||
`route_address_set` 和 `route_exclude_address_set` 使用的连接输入标记。
|
`auto_redriect` 使用的连接输入标记。
|
||||||
|
|
||||||
默认使用 `0x2023`。
|
默认使用 `0x2023`。
|
||||||
|
|
||||||
@@ -261,7 +269,7 @@ tun 接口的 IPv6 前缀。
|
|||||||
|
|
||||||
!!! question "自 sing-box 1.10.0 起"
|
!!! question "自 sing-box 1.10.0 起"
|
||||||
|
|
||||||
`route_address_set` 和 `route_exclude_address_set` 使用的连接输出标记。
|
`auto_redriect` 使用的连接输出标记。
|
||||||
|
|
||||||
默认使用 `0x2024`。
|
默认使用 `0x2024`。
|
||||||
|
|
||||||
@@ -341,8 +349,6 @@ tun 接口的 IPv6 前缀。
|
|||||||
|
|
||||||
将指定规则集中的目标 IP CIDR 规则添加到防火墙。
|
将指定规则集中的目标 IP CIDR 规则添加到防火墙。
|
||||||
不匹配的流量将绕过 sing-box 路由。
|
不匹配的流量将绕过 sing-box 路由。
|
||||||
|
|
||||||
与 `route.default_mark` 和 `[dialOptions].routing_mark` 冲突。
|
|
||||||
|
|
||||||
=== "`auto_redirect` 未启用"
|
=== "`auto_redirect` 未启用"
|
||||||
|
|
||||||
|
|||||||
@@ -1,186 +0,0 @@
|
|||||||
package clashapi
|
|
||||||
|
|
||||||
import (
|
|
||||||
"archive/zip"
|
|
||||||
"context"
|
|
||||||
"crypto/x509"
|
|
||||||
"encoding/pem"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
"github.com/sagernet/sing/common"
|
|
||||||
"github.com/sagernet/sing/service"
|
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
|
||||||
"github.com/go-chi/render"
|
|
||||||
"github.com/gofrs/uuid/v5"
|
|
||||||
"howett.net/plist"
|
|
||||||
)
|
|
||||||
|
|
||||||
func mitmRouter(ctx context.Context) http.Handler {
|
|
||||||
r := chi.NewRouter()
|
|
||||||
r.Get("/mobileconfig", getMobileConfig(ctx))
|
|
||||||
r.Get("/crt", getCertificate(ctx))
|
|
||||||
r.Get("/pem", getCertificatePEM(ctx))
|
|
||||||
r.Get("/magisk", getMagiskModule(ctx))
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
func getMobileConfig(ctx context.Context) http.HandlerFunc {
|
|
||||||
return func(writer http.ResponseWriter, request *http.Request) {
|
|
||||||
store := service.FromContext[adapter.CertificateStore](ctx)
|
|
||||||
if !store.TLSDecryptionEnabled() {
|
|
||||||
http.NotFound(writer, request)
|
|
||||||
render.PlainText(writer, request, "TLS decryption not enabled")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
certificate := store.TLSDecryptionCertificate()
|
|
||||||
writer.Header().Set("Content-Type", "application/x-apple-aspen-config")
|
|
||||||
uuidGen := common.Must1(uuid.NewV4()).String()
|
|
||||||
mobileConfig := map[string]interface{}{
|
|
||||||
"PayloadContent": []interface{}{
|
|
||||||
map[string]interface{}{
|
|
||||||
"PayloadCertificateFileName": "Certificates.cer",
|
|
||||||
"PayloadContent": certificate.Raw,
|
|
||||||
"PayloadDescription": "Adds a root certificate",
|
|
||||||
"PayloadDisplayName": certificate.Subject.CommonName,
|
|
||||||
"PayloadIdentifier": "com.apple.security.root." + uuidGen,
|
|
||||||
"PayloadType": "com.apple.security.root",
|
|
||||||
"PayloadUUID": uuidGen,
|
|
||||||
"PayloadVersion": 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"PayloadDisplayName": certificate.Subject.CommonName,
|
|
||||||
"PayloadIdentifier": "io.nekohasekai.sfa.ca.profile." + uuidGen,
|
|
||||||
"PayloadRemovalDisallowed": false,
|
|
||||||
"PayloadType": "Configuration",
|
|
||||||
"PayloadUUID": uuidGen,
|
|
||||||
"PayloadVersion": 1,
|
|
||||||
}
|
|
||||||
encoder := plist.NewEncoder(writer)
|
|
||||||
encoder.Indent("\t")
|
|
||||||
encoder.Encode(mobileConfig)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getCertificate(ctx context.Context) http.HandlerFunc {
|
|
||||||
return func(writer http.ResponseWriter, request *http.Request) {
|
|
||||||
store := service.FromContext[adapter.CertificateStore](ctx)
|
|
||||||
if !store.TLSDecryptionEnabled() {
|
|
||||||
http.NotFound(writer, request)
|
|
||||||
render.PlainText(writer, request, "TLS decryption not enabled")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writer.Header().Set("Content-Type", "application/x-x509-ca-cert")
|
|
||||||
writer.Header().Set("Content-Disposition", "attachment; filename=Certificate.crt")
|
|
||||||
writer.Write(store.TLSDecryptionCertificate().Raw)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getCertificatePEM(ctx context.Context) http.HandlerFunc {
|
|
||||||
return func(writer http.ResponseWriter, request *http.Request) {
|
|
||||||
store := service.FromContext[adapter.CertificateStore](ctx)
|
|
||||||
if !store.TLSDecryptionEnabled() {
|
|
||||||
http.NotFound(writer, request)
|
|
||||||
render.PlainText(writer, request, "TLS decryption not enabled")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writer.Header().Set("Content-Type", "application/x-pem-file")
|
|
||||||
writer.Header().Set("Content-Disposition", "attachment; filename=Certificate.pem")
|
|
||||||
writer.Write(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: store.TLSDecryptionCertificate().Raw}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getMagiskModule(ctx context.Context) http.HandlerFunc {
|
|
||||||
return func(writer http.ResponseWriter, request *http.Request) {
|
|
||||||
store := service.FromContext[adapter.CertificateStore](ctx)
|
|
||||||
if !store.TLSDecryptionEnabled() {
|
|
||||||
http.NotFound(writer, request)
|
|
||||||
render.PlainText(writer, request, "TLS decryption not enabled")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writer.Header().Set("Content-Type", "application/zip")
|
|
||||||
writer.Header().Set("Content-Disposition", "attachment; filename="+store.TLSDecryptionCertificate().Subject.CommonName+".zip")
|
|
||||||
createMagiskModule(writer, store.TLSDecryptionCertificate())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func createMagiskModule(writer io.Writer, certificate *x509.Certificate) error {
|
|
||||||
zipWriter := zip.NewWriter(writer)
|
|
||||||
defer zipWriter.Close()
|
|
||||||
moduleProp, err := zipWriter.Create("module.prop")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = moduleProp.Write([]byte(`
|
|
||||||
id=sing-box-certificate
|
|
||||||
name=` + certificate.Subject.CommonName + `
|
|
||||||
version=v0.0.1
|
|
||||||
versionCode=1
|
|
||||||
author=sing-box
|
|
||||||
description=This module adds ` + certificate.Subject.CommonName + ` to the system trust store.
|
|
||||||
`))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
certificateFile, err := zipWriter.Create("system/etc/security/cacerts/" + certificate.Subject.CommonName + ".pem")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = pem.Encode(certificateFile, &pem.Block{Type: "CERTIFICATE", Bytes: certificate.Raw})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
updateBinary, err := zipWriter.Create("META-INF/com/google/android/update-binary")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = updateBinary.Write([]byte(`
|
|
||||||
#!/sbin/sh
|
|
||||||
|
|
||||||
#################
|
|
||||||
# Initialization
|
|
||||||
#################
|
|
||||||
|
|
||||||
umask 022
|
|
||||||
|
|
||||||
# echo before loading util_functions
|
|
||||||
ui_print() { echo "$1"; }
|
|
||||||
|
|
||||||
require_new_magisk() {
|
|
||||||
ui_print "*******************************"
|
|
||||||
ui_print " Please install Magisk v20.4+! "
|
|
||||||
ui_print "*******************************"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
#########################
|
|
||||||
# Load util_functions.sh
|
|
||||||
#########################
|
|
||||||
|
|
||||||
OUTFD=$2
|
|
||||||
ZIPFILE=$3
|
|
||||||
|
|
||||||
mount /data 2>/dev/null
|
|
||||||
|
|
||||||
[ -f /data/adb/magisk/util_functions.sh ] || require_new_magisk
|
|
||||||
. /data/adb/magisk/util_functions.sh
|
|
||||||
[ $MAGISK_VER_CODE -lt 20400 ] && require_new_magisk
|
|
||||||
|
|
||||||
install_module
|
|
||||||
exit 0
|
|
||||||
`))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
updaterScript, err := zipWriter.Create("META-INF/com/google/android/updater-script")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = updaterScript.Write([]byte("#MAGISK"))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -124,7 +124,6 @@ func NewServer(ctx context.Context, logFactory log.ObservableFactory, options op
|
|||||||
r.Mount("/profile", profileRouter())
|
r.Mount("/profile", profileRouter())
|
||||||
r.Mount("/cache", cacheRouter(ctx))
|
r.Mount("/cache", cacheRouter(ctx))
|
||||||
r.Mount("/dns", dnsRouter(s.dnsRouter))
|
r.Mount("/dns", dnsRouter(s.dnsRouter))
|
||||||
r.Mount("/mitm", mitmRouter(ctx))
|
|
||||||
|
|
||||||
s.setupMetaAPI(r)
|
s.setupMetaAPI(r)
|
||||||
})
|
})
|
||||||
|
|||||||
7
go.mod
7
go.mod
@@ -3,7 +3,7 @@ module github.com/sagernet/sing-box
|
|||||||
go 1.23.1
|
go 1.23.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/anytls/sing-anytls v0.0.6
|
github.com/anytls/sing-anytls v0.0.7
|
||||||
github.com/caddyserver/certmagic v0.21.7
|
github.com/caddyserver/certmagic v0.21.7
|
||||||
github.com/cloudflare/circl v1.6.0
|
github.com/cloudflare/circl v1.6.0
|
||||||
github.com/cretz/bine v0.2.0
|
github.com/cretz/bine v0.2.0
|
||||||
@@ -32,10 +32,10 @@ require (
|
|||||||
github.com/sagernet/sing-shadowsocks v0.2.7
|
github.com/sagernet/sing-shadowsocks v0.2.7
|
||||||
github.com/sagernet/sing-shadowsocks2 v0.2.0
|
github.com/sagernet/sing-shadowsocks2 v0.2.0
|
||||||
github.com/sagernet/sing-shadowtls v0.2.1-0.20250316154757-6f9e732e5056
|
github.com/sagernet/sing-shadowtls v0.2.1-0.20250316154757-6f9e732e5056
|
||||||
github.com/sagernet/sing-tun v0.6.2-0.20250319123703-35b5747b44ec
|
github.com/sagernet/sing-tun v0.6.2
|
||||||
github.com/sagernet/sing-vmess v0.2.0
|
github.com/sagernet/sing-vmess v0.2.0
|
||||||
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7
|
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7
|
||||||
github.com/sagernet/tailscale v1.80.3-mod.0
|
github.com/sagernet/tailscale v1.80.3-mod.2
|
||||||
github.com/sagernet/utls v1.6.7
|
github.com/sagernet/utls v1.6.7
|
||||||
github.com/sagernet/wireguard-go v0.0.1-beta.5
|
github.com/sagernet/wireguard-go v0.0.1-beta.5
|
||||||
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854
|
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854
|
||||||
@@ -53,7 +53,6 @@ require (
|
|||||||
google.golang.org/grpc v1.70.0
|
google.golang.org/grpc v1.70.0
|
||||||
google.golang.org/protobuf v1.36.5
|
google.golang.org/protobuf v1.36.5
|
||||||
howett.net/plist v1.0.1
|
howett.net/plist v1.0.1
|
||||||
software.sslmate.com/src/go-pkcs12 v0.4.0
|
|
||||||
)
|
)
|
||||||
|
|
||||||
//replace github.com/sagernet/sing => ../sing
|
//replace github.com/sagernet/sing => ../sing
|
||||||
|
|||||||
12
go.sum
12
go.sum
@@ -8,8 +8,8 @@ github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7V
|
|||||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||||
github.com/anytls/sing-anytls v0.0.6 h1:UatIjl/OvzWQGXQ1I2bAIkabL9WtihW0fA7G+DXGBUg=
|
github.com/anytls/sing-anytls v0.0.7 h1:0Q5dHNB2sqkFAWZCyK2vjQ/ckI5Iz3V/Frf3k7mBrGc=
|
||||||
github.com/anytls/sing-anytls v0.0.6/go.mod h1:7rjN6IukwysmdusYsrV51Fgu1uW6vsrdd6ctjnEAln8=
|
github.com/anytls/sing-anytls v0.0.7/go.mod h1:7rjN6IukwysmdusYsrV51Fgu1uW6vsrdd6ctjnEAln8=
|
||||||
github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE=
|
github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE=
|
||||||
github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||||
github.com/caddyserver/certmagic v0.21.7 h1:66KJioPFJwttL43KYSWk7ErSmE6LfaJgCQuhm8Sg6fg=
|
github.com/caddyserver/certmagic v0.21.7 h1:66KJioPFJwttL43KYSWk7ErSmE6LfaJgCQuhm8Sg6fg=
|
||||||
@@ -190,14 +190,14 @@ github.com/sagernet/sing-shadowsocks2 v0.2.0 h1:wpZNs6wKnR7mh1wV9OHwOyUr21VkS3wK
|
|||||||
github.com/sagernet/sing-shadowsocks2 v0.2.0/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ=
|
github.com/sagernet/sing-shadowsocks2 v0.2.0/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ=
|
||||||
github.com/sagernet/sing-shadowtls v0.2.1-0.20250316154757-6f9e732e5056 h1:GFNJQAHhSXqAfxAw1wDG/QWbdpGH5Na3k8qUynqWnEA=
|
github.com/sagernet/sing-shadowtls v0.2.1-0.20250316154757-6f9e732e5056 h1:GFNJQAHhSXqAfxAw1wDG/QWbdpGH5Na3k8qUynqWnEA=
|
||||||
github.com/sagernet/sing-shadowtls v0.2.1-0.20250316154757-6f9e732e5056/go.mod h1:HyacBPIFiKihJQR8LQp56FM4hBtd/7MZXnRxxQIOPsc=
|
github.com/sagernet/sing-shadowtls v0.2.1-0.20250316154757-6f9e732e5056/go.mod h1:HyacBPIFiKihJQR8LQp56FM4hBtd/7MZXnRxxQIOPsc=
|
||||||
github.com/sagernet/sing-tun v0.6.2-0.20250319123703-35b5747b44ec h1:9/OYGb9qDmUFIhqd3S+3eni62EKRQR1rSmRH18baA/M=
|
github.com/sagernet/sing-tun v0.6.2 h1:SoylB/8dA6bRWoUhi4GbFb4WkKL0SMCpmYcvumPndo0=
|
||||||
github.com/sagernet/sing-tun v0.6.2-0.20250319123703-35b5747b44ec/go.mod h1:fisFCbC4Vfb6HqQNcwPJi2CDK2bf0Xapyz3j3t4cnHE=
|
github.com/sagernet/sing-tun v0.6.2/go.mod h1:fisFCbC4Vfb6HqQNcwPJi2CDK2bf0Xapyz3j3t4cnHE=
|
||||||
github.com/sagernet/sing-vmess v0.2.0 h1:pCMGUXN2k7RpikQV65/rtXtDHzb190foTfF9IGTMZrI=
|
github.com/sagernet/sing-vmess v0.2.0 h1:pCMGUXN2k7RpikQV65/rtXtDHzb190foTfF9IGTMZrI=
|
||||||
github.com/sagernet/sing-vmess v0.2.0/go.mod h1:jDAZ0A0St1zVRkyvhAPRySOFfhC+4SQtO5VYyeFotgA=
|
github.com/sagernet/sing-vmess v0.2.0/go.mod h1:jDAZ0A0St1zVRkyvhAPRySOFfhC+4SQtO5VYyeFotgA=
|
||||||
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ=
|
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ=
|
||||||
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7/go.mod h1:FP9X2xjT/Az1EsG/orYYoC+5MojWnuI7hrffz8fGwwo=
|
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7/go.mod h1:FP9X2xjT/Az1EsG/orYYoC+5MojWnuI7hrffz8fGwwo=
|
||||||
github.com/sagernet/tailscale v1.80.3-mod.0 h1:oHIdivbR/yxoiA9d3a2rRlhYn2shY9XVF35Rr8jW508=
|
github.com/sagernet/tailscale v1.80.3-mod.2 h1:hT0CI74q727EuCcgQ+T4pvon8V0aoi4vTAxah7GsNMQ=
|
||||||
github.com/sagernet/tailscale v1.80.3-mod.0/go.mod h1:EBxXsWu4OH2ELbQLq32WoBeIubG8KgDrg4/Oaxjs6lI=
|
github.com/sagernet/tailscale v1.80.3-mod.2/go.mod h1:EBxXsWu4OH2ELbQLq32WoBeIubG8KgDrg4/Oaxjs6lI=
|
||||||
github.com/sagernet/utls v1.6.7 h1:Ep3+aJ8FUGGta+II2IEVNUc3EDhaRCZINWkj/LloIA8=
|
github.com/sagernet/utls v1.6.7 h1:Ep3+aJ8FUGGta+II2IEVNUc3EDhaRCZINWkj/LloIA8=
|
||||||
github.com/sagernet/utls v1.6.7/go.mod h1:Uua1TKO/FFuAhLr9rkaVnnrTmmiItzDjv1BUb2+ERwM=
|
github.com/sagernet/utls v1.6.7/go.mod h1:Uua1TKO/FFuAhLr9rkaVnnrTmmiItzDjv1BUb2+ERwM=
|
||||||
github.com/sagernet/wireguard-go v0.0.1-beta.5 h1:aBEsxJUMEONwOZqKPIkuAcv4zJV5p6XlzEN04CF0FXc=
|
github.com/sagernet/wireguard-go v0.0.1-beta.5 h1:aBEsxJUMEONwOZqKPIkuAcv4zJV5p6XlzEN04CF0FXc=
|
||||||
|
|||||||
@@ -10,10 +10,6 @@ import (
|
|||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
DefaultTimeFormat = "-0700 2006-01-02 15:04:05"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Options struct {
|
type Options struct {
|
||||||
Context context.Context
|
Context context.Context
|
||||||
Options option.LogOptions
|
Options option.LogOptions
|
||||||
@@ -51,7 +47,7 @@ func New(options Options) (Factory, error) {
|
|||||||
DisableColors: logOptions.DisableColor || logFilePath != "",
|
DisableColors: logOptions.DisableColor || logFilePath != "",
|
||||||
DisableTimestamp: !logOptions.Timestamp && logFilePath != "",
|
DisableTimestamp: !logOptions.Timestamp && logFilePath != "",
|
||||||
FullTimestamp: logOptions.Timestamp,
|
FullTimestamp: logOptions.Timestamp,
|
||||||
TimestampFormat: DefaultTimeFormat,
|
TimestampFormat: "-0700 2006-01-02 15:04:05",
|
||||||
}
|
}
|
||||||
factory := NewDefaultFactory(
|
factory := NewDefaultFactory(
|
||||||
options.Context,
|
options.Context,
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
package mitm
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/base64"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing/common"
|
|
||||||
)
|
|
||||||
|
|
||||||
var surgeTinyGif = common.OnceValue(func() []byte {
|
|
||||||
return common.Must1(base64.StdEncoding.DecodeString("R0lGODlhAQABAAAAACH5BAEAAAAALAAAAAABAAEAAAIBAAA="))
|
|
||||||
})
|
|
||||||
811
mitm/engine.go
811
mitm/engine.go
@@ -1,811 +0,0 @@
|
|||||||
package mitm
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"io"
|
|
||||||
"math"
|
|
||||||
"mime"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
"unicode"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
"github.com/sagernet/sing-box/common/dialer"
|
|
||||||
sTLS "github.com/sagernet/sing-box/common/tls"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
"github.com/sagernet/sing/common"
|
|
||||||
"github.com/sagernet/sing/common/atomic"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
F "github.com/sagernet/sing/common/format"
|
|
||||||
"github.com/sagernet/sing/common/logger"
|
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
"github.com/sagernet/sing/common/ntp"
|
|
||||||
sHTTP "github.com/sagernet/sing/protocol/http"
|
|
||||||
"github.com/sagernet/sing/service"
|
|
||||||
|
|
||||||
"golang.org/x/net/http2"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ adapter.MITMEngine = (*Engine)(nil)
|
|
||||||
|
|
||||||
type Engine struct {
|
|
||||||
ctx context.Context
|
|
||||||
logger logger.ContextLogger
|
|
||||||
connection adapter.ConnectionManager
|
|
||||||
certificate adapter.CertificateStore
|
|
||||||
timeFunc func() time.Time
|
|
||||||
http2Enabled bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewEngine(ctx context.Context, logger logger.ContextLogger, options option.MITMOptions) (*Engine, error) {
|
|
||||||
engine := &Engine{
|
|
||||||
ctx: ctx,
|
|
||||||
logger: logger,
|
|
||||||
http2Enabled: options.HTTP2Enabled,
|
|
||||||
}
|
|
||||||
return engine, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Engine) Start(stage adapter.StartStage) error {
|
|
||||||
switch stage {
|
|
||||||
case adapter.StartStateInitialize:
|
|
||||||
e.connection = service.FromContext[adapter.ConnectionManager](e.ctx)
|
|
||||||
e.certificate = service.FromContext[adapter.CertificateStore](e.ctx)
|
|
||||||
e.timeFunc = ntp.TimeFuncFromContext(e.ctx)
|
|
||||||
if e.timeFunc == nil {
|
|
||||||
e.timeFunc = time.Now
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Engine) Close() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Engine) NewConnection(ctx context.Context, this N.Dialer, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
|
|
||||||
if e.certificate.TLSDecryptionEnabled() && metadata.ClientHello != nil {
|
|
||||||
err := e.newTLS(ctx, this, conn, metadata, onClose)
|
|
||||||
if err != nil {
|
|
||||||
e.logger.ErrorContext(ctx, err)
|
|
||||||
} else {
|
|
||||||
e.logger.DebugContext(ctx, "connection closed")
|
|
||||||
}
|
|
||||||
if onClose != nil {
|
|
||||||
onClose(err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
} else if metadata.HTTPRequest != nil {
|
|
||||||
err := e.newHTTP1(ctx, this, conn, nil, metadata)
|
|
||||||
if err != nil {
|
|
||||||
e.logger.ErrorContext(ctx, err)
|
|
||||||
} else {
|
|
||||||
e.logger.DebugContext(ctx, "connection closed")
|
|
||||||
}
|
|
||||||
if onClose != nil {
|
|
||||||
onClose(err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
e.logger.DebugContext(ctx, "HTTP and TLS not detected, skipped")
|
|
||||||
}
|
|
||||||
metadata.MITM = nil
|
|
||||||
e.connection.NewConnection(ctx, this, conn, metadata, onClose)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Engine) newTLS(ctx context.Context, this N.Dialer, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) error {
|
|
||||||
acceptHTTP := len(metadata.ClientHello.SupportedProtos) == 0 || common.Contains(metadata.ClientHello.SupportedProtos, "http/1.1")
|
|
||||||
acceptH2 := e.http2Enabled && common.Contains(metadata.ClientHello.SupportedProtos, "h2")
|
|
||||||
if !acceptHTTP && !acceptH2 {
|
|
||||||
metadata.MITM = nil
|
|
||||||
e.logger.DebugContext(ctx, "unsupported application protocol: ", strings.Join(metadata.ClientHello.SupportedProtos, ","))
|
|
||||||
e.connection.NewConnection(ctx, this, conn, metadata, onClose)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
var nextProtos []string
|
|
||||||
if acceptH2 {
|
|
||||||
nextProtos = append(nextProtos, "h2")
|
|
||||||
} else if acceptHTTP {
|
|
||||||
nextProtos = append(nextProtos, "http/1.1")
|
|
||||||
}
|
|
||||||
var (
|
|
||||||
maxVersion uint16
|
|
||||||
minVersion uint16
|
|
||||||
)
|
|
||||||
for _, version := range metadata.ClientHello.SupportedVersions {
|
|
||||||
maxVersion = common.Max(maxVersion, version)
|
|
||||||
minVersion = common.Min(minVersion, version)
|
|
||||||
}
|
|
||||||
serverName := metadata.ClientHello.ServerName
|
|
||||||
if serverName == "" && metadata.Destination.IsIP() {
|
|
||||||
serverName = metadata.Destination.Addr.String()
|
|
||||||
}
|
|
||||||
tlsConfig := &tls.Config{
|
|
||||||
Time: e.timeFunc,
|
|
||||||
ServerName: serverName,
|
|
||||||
NextProtos: nextProtos,
|
|
||||||
MinVersion: minVersion,
|
|
||||||
MaxVersion: maxVersion,
|
|
||||||
GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
|
||||||
return sTLS.GenerateKeyPair(e.certificate.TLSDecryptionCertificate(), e.certificate.TLSDecryptionPrivateKey(), e.timeFunc, serverName)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
tlsConn := tls.Server(conn, tlsConfig)
|
|
||||||
err := tlsConn.HandshakeContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "TLS handshake failed for ", metadata.ClientHello.ServerName, ", ", strings.Join(metadata.ClientHello.SupportedProtos, ", "))
|
|
||||||
}
|
|
||||||
if tlsConn.ConnectionState().NegotiatedProtocol == "h2" {
|
|
||||||
return e.newHTTP2(ctx, this, tlsConn, tlsConfig, metadata, onClose)
|
|
||||||
} else {
|
|
||||||
return e.newHTTP1(ctx, this, tlsConn, tlsConfig, metadata)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Engine) newHTTP1(ctx context.Context, this N.Dialer, conn net.Conn, tlsConfig *tls.Config, metadata adapter.InboundContext) error {
|
|
||||||
options := metadata.MITM
|
|
||||||
defer conn.Close()
|
|
||||||
reader := bufio.NewReader(conn)
|
|
||||||
request, err := sHTTP.ReadRequest(reader)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "read HTTP request")
|
|
||||||
}
|
|
||||||
rawRequestURL := request.URL
|
|
||||||
if tlsConfig != nil {
|
|
||||||
rawRequestURL.Scheme = "https"
|
|
||||||
} else {
|
|
||||||
rawRequestURL.Scheme = "http"
|
|
||||||
}
|
|
||||||
if rawRequestURL.Host == "" {
|
|
||||||
rawRequestURL.Host = request.Host
|
|
||||||
}
|
|
||||||
requestURL := rawRequestURL.String()
|
|
||||||
request.RequestURI = ""
|
|
||||||
var requestMatch bool
|
|
||||||
var body []byte
|
|
||||||
if options.Print && request.ContentLength > 0 && request.ContentLength <= 131072 {
|
|
||||||
body, err = io.ReadAll(request.Body)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "read HTTP request body")
|
|
||||||
}
|
|
||||||
request.Body = io.NopCloser(bytes.NewReader(body))
|
|
||||||
}
|
|
||||||
if options.Print {
|
|
||||||
e.printRequest(ctx, request, body)
|
|
||||||
}
|
|
||||||
for i, rule := range options.SurgeURLRewrite {
|
|
||||||
if !rule.Pattern.MatchString(requestURL) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
e.logger.DebugContext(ctx, "match url_rewrite[", i, "] => ", rule.String())
|
|
||||||
if rule.Reject {
|
|
||||||
return E.New("request rejected by url_rewrite")
|
|
||||||
} else if rule.Redirect {
|
|
||||||
w := new(simpleResponseWriter)
|
|
||||||
http.Redirect(w, request, rule.Destination.String(), http.StatusFound)
|
|
||||||
err = w.Build(request).Write(conn)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "write url_rewrite 302 response")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
requestMatch = true
|
|
||||||
request.URL = rule.Destination
|
|
||||||
newDestination := M.ParseSocksaddrHostPortStr(rule.Destination.Hostname(), rule.Destination.Port())
|
|
||||||
if newDestination.Port == 0 {
|
|
||||||
newDestination.Port = metadata.Destination.Port
|
|
||||||
}
|
|
||||||
metadata.Destination = newDestination
|
|
||||||
if tlsConfig != nil {
|
|
||||||
tlsConfig.ServerName = rule.Destination.Hostname()
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
for i, rule := range options.SurgeHeaderRewrite {
|
|
||||||
if rule.Response {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !rule.Pattern.MatchString(requestURL) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
requestMatch = true
|
|
||||||
e.logger.DebugContext(ctx, "match header_rewrite[", i, "] => ", rule.String())
|
|
||||||
switch {
|
|
||||||
case rule.Add:
|
|
||||||
if strings.ToLower(rule.Key) == "host" {
|
|
||||||
request.Host = rule.Value
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
request.Header.Add(rule.Key, rule.Value)
|
|
||||||
case rule.Delete:
|
|
||||||
request.Header.Del(rule.Key)
|
|
||||||
case rule.Replace:
|
|
||||||
if request.Header.Get(rule.Key) != "" {
|
|
||||||
request.Header.Set(rule.Key, rule.Value)
|
|
||||||
}
|
|
||||||
case rule.ReplaceRegex:
|
|
||||||
if value := request.Header.Get(rule.Key); value != "" {
|
|
||||||
request.Header.Set(rule.Key, rule.Match.ReplaceAllString(value, rule.Value))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for i, rule := range options.SurgeBodyRewrite {
|
|
||||||
if rule.Response {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !rule.Pattern.MatchString(requestURL) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
requestMatch = true
|
|
||||||
e.logger.DebugContext(ctx, "match body_rewrite[", i, "] => ", rule.String())
|
|
||||||
if body == nil {
|
|
||||||
if request.ContentLength <= 0 {
|
|
||||||
e.logger.WarnContext(ctx, "body replace skipped due to non-fixed content length")
|
|
||||||
break
|
|
||||||
} else if request.ContentLength > 131072 {
|
|
||||||
e.logger.WarnContext(ctx, "body replace skipped due to large content length: ", request.ContentLength)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
body, err = io.ReadAll(request.Body)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "read HTTP request body")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for mi := 0; i < len(rule.Match); i++ {
|
|
||||||
body = rule.Match[mi].ReplaceAll(body, []byte(rule.Replace[i]))
|
|
||||||
}
|
|
||||||
request.Body = io.NopCloser(bytes.NewReader(body))
|
|
||||||
request.ContentLength = int64(len(body))
|
|
||||||
}
|
|
||||||
if !requestMatch {
|
|
||||||
for i, rule := range options.SurgeMapLocal {
|
|
||||||
if !rule.Pattern.MatchString(requestURL) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
requestMatch = true
|
|
||||||
e.logger.DebugContext(ctx, "match map_local[", i, "] => ", rule.String())
|
|
||||||
var (
|
|
||||||
statusCode = http.StatusOK
|
|
||||||
headers = make(http.Header)
|
|
||||||
)
|
|
||||||
if rule.StatusCode > 0 {
|
|
||||||
statusCode = rule.StatusCode
|
|
||||||
}
|
|
||||||
switch {
|
|
||||||
case rule.File:
|
|
||||||
resource, err := os.ReadFile(rule.Data)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "open map local source")
|
|
||||||
}
|
|
||||||
mimeType := mime.TypeByExtension(filepath.Ext(rule.Data))
|
|
||||||
if mimeType == "" {
|
|
||||||
mimeType = "application/octet-stream"
|
|
||||||
}
|
|
||||||
headers.Set("Content-Type", mimeType)
|
|
||||||
body = resource
|
|
||||||
case rule.Text:
|
|
||||||
headers.Set("Content-Type", "text/plain")
|
|
||||||
body = []byte(rule.Data)
|
|
||||||
case rule.TinyGif:
|
|
||||||
headers.Set("Content-Type", "image/gif")
|
|
||||||
body = surgeTinyGif()
|
|
||||||
case rule.Base64:
|
|
||||||
headers.Set("Content-Type", "application/octet-stream")
|
|
||||||
body = rule.Base64Data
|
|
||||||
}
|
|
||||||
response := &http.Response{
|
|
||||||
StatusCode: statusCode,
|
|
||||||
Status: http.StatusText(statusCode),
|
|
||||||
Proto: request.Proto,
|
|
||||||
ProtoMajor: request.ProtoMajor,
|
|
||||||
ProtoMinor: request.ProtoMinor,
|
|
||||||
Header: headers,
|
|
||||||
Body: io.NopCloser(bytes.NewReader(body)),
|
|
||||||
}
|
|
||||||
err = response.Write(conn)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "write map local response")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ctx = adapter.WithContext(ctx, &metadata)
|
|
||||||
var innerErr atomic.TypedValue[error]
|
|
||||||
httpClient := &http.Client{
|
|
||||||
Transport: &http.Transport{
|
|
||||||
DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
|
|
||||||
if len(metadata.DestinationAddresses) > 0 || metadata.Destination.IsIP() {
|
|
||||||
return dialer.DialSerialNetwork(ctx, this, N.NetworkTCP, metadata.Destination, metadata.DestinationAddresses, metadata.NetworkStrategy, metadata.NetworkType, metadata.FallbackNetworkType, metadata.FallbackDelay)
|
|
||||||
} else {
|
|
||||||
return this.DialContext(ctx, N.NetworkTCP, metadata.Destination)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
TLSClientConfig: tlsConfig,
|
|
||||||
},
|
|
||||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
|
||||||
return http.ErrUseLastResponse
|
|
||||||
},
|
|
||||||
}
|
|
||||||
defer httpClient.CloseIdleConnections()
|
|
||||||
requestCtx, cancel := context.WithCancel(ctx)
|
|
||||||
defer cancel()
|
|
||||||
response, err := httpClient.Do(request.WithContext(requestCtx))
|
|
||||||
if err != nil {
|
|
||||||
cancel()
|
|
||||||
return E.Errors(innerErr.Load(), err)
|
|
||||||
}
|
|
||||||
var responseMatch bool
|
|
||||||
var responseBody []byte
|
|
||||||
if options.Print && response.ContentLength > 0 && response.ContentLength <= 131072 {
|
|
||||||
responseBody, err = io.ReadAll(response.Body)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "read HTTP response body")
|
|
||||||
}
|
|
||||||
response.Body = io.NopCloser(bytes.NewReader(responseBody))
|
|
||||||
}
|
|
||||||
if options.Print {
|
|
||||||
e.printResponse(ctx, request, response, responseBody)
|
|
||||||
}
|
|
||||||
for i, rule := range options.SurgeHeaderRewrite {
|
|
||||||
if !rule.Response {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !rule.Pattern.MatchString(requestURL) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
responseMatch = true
|
|
||||||
e.logger.DebugContext(ctx, "match header_rewrite[", i, "] => ", rule.String())
|
|
||||||
switch {
|
|
||||||
case rule.Add:
|
|
||||||
response.Header.Add(rule.Key, rule.Value)
|
|
||||||
case rule.Delete:
|
|
||||||
response.Header.Del(rule.Key)
|
|
||||||
case rule.Replace:
|
|
||||||
if response.Header.Get(rule.Key) != "" {
|
|
||||||
response.Header.Set(rule.Key, rule.Value)
|
|
||||||
}
|
|
||||||
case rule.ReplaceRegex:
|
|
||||||
if value := response.Header.Get(rule.Key); value != "" {
|
|
||||||
response.Header.Set(rule.Key, rule.Match.ReplaceAllString(value, rule.Value))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for i, rule := range options.SurgeBodyRewrite {
|
|
||||||
if !rule.Response {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !rule.Pattern.MatchString(requestURL) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
responseMatch = true
|
|
||||||
e.logger.DebugContext(ctx, "match body_rewrite[", i, "] => ", rule.String())
|
|
||||||
if responseBody == nil {
|
|
||||||
if response.ContentLength <= 0 {
|
|
||||||
e.logger.WarnContext(ctx, "body replace skipped due to non-fixed content length")
|
|
||||||
break
|
|
||||||
} else if response.ContentLength > 131072 {
|
|
||||||
e.logger.WarnContext(ctx, "body replace skipped due to large content length: ", request.ContentLength)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
responseBody, err = io.ReadAll(response.Body)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "read HTTP request body")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for mi := 0; i < len(rule.Match); i++ {
|
|
||||||
responseBody = rule.Match[mi].ReplaceAll(responseBody, []byte(rule.Replace[i]))
|
|
||||||
}
|
|
||||||
response.Body = io.NopCloser(bytes.NewReader(responseBody))
|
|
||||||
response.ContentLength = int64(len(responseBody))
|
|
||||||
}
|
|
||||||
if !options.Print && !requestMatch && !responseMatch {
|
|
||||||
e.logger.WarnContext(ctx, "request not modified")
|
|
||||||
}
|
|
||||||
err = response.Write(conn)
|
|
||||||
if err != nil {
|
|
||||||
return E.Errors(E.Cause(err, "write HTTP response"), innerErr.Load())
|
|
||||||
} else if innerErr.Load() != nil {
|
|
||||||
return E.Cause(innerErr.Load(), "write HTTP response")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Engine) newHTTP2(ctx context.Context, this N.Dialer, conn net.Conn, tlsConfig *tls.Config, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) error {
|
|
||||||
httpTransport := &http.Transport{
|
|
||||||
ForceAttemptHTTP2: true,
|
|
||||||
DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
|
|
||||||
ctx = adapter.WithContext(ctx, &metadata)
|
|
||||||
if len(metadata.DestinationAddresses) > 0 || metadata.Destination.IsIP() {
|
|
||||||
return dialer.DialSerialNetwork(ctx, this, N.NetworkTCP, metadata.Destination, metadata.DestinationAddresses, metadata.NetworkStrategy, metadata.NetworkType, metadata.FallbackNetworkType, metadata.FallbackDelay)
|
|
||||||
} else {
|
|
||||||
return this.DialContext(ctx, N.NetworkTCP, metadata.Destination)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
TLSClientConfig: tlsConfig,
|
|
||||||
}
|
|
||||||
err := http2.ConfigureTransport(httpTransport)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "configure HTTP/2 transport")
|
|
||||||
}
|
|
||||||
handler := &engineHandler{
|
|
||||||
Engine: e,
|
|
||||||
conn: conn,
|
|
||||||
tlsConfig: tlsConfig,
|
|
||||||
dialer: this,
|
|
||||||
metadata: metadata,
|
|
||||||
httpClient: &http.Client{
|
|
||||||
Transport: httpTransport,
|
|
||||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
|
||||||
return http.ErrUseLastResponse
|
|
||||||
},
|
|
||||||
},
|
|
||||||
onClose: onClose,
|
|
||||||
}
|
|
||||||
http2Server := &http2.Server{
|
|
||||||
MaxReadFrameSize: math.MaxUint32,
|
|
||||||
}
|
|
||||||
http2Server.ServeConn(conn, &http2.ServeConnOpts{
|
|
||||||
Context: ctx,
|
|
||||||
Handler: handler,
|
|
||||||
})
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type engineHandler struct {
|
|
||||||
*Engine
|
|
||||||
conn net.Conn
|
|
||||||
tlsConfig *tls.Config
|
|
||||||
dialer N.Dialer
|
|
||||||
metadata adapter.InboundContext
|
|
||||||
onClose N.CloseHandlerFunc
|
|
||||||
httpClient *http.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *engineHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
|
||||||
err := e.serveHTTP(request.Context(), writer, request)
|
|
||||||
if err != nil {
|
|
||||||
if E.IsClosedOrCanceled(err) {
|
|
||||||
e.logger.DebugContext(request.Context(), E.Cause(err, "connection closed"))
|
|
||||||
} else {
|
|
||||||
e.logger.ErrorContext(request.Context(), err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *engineHandler) serveHTTP(ctx context.Context, writer http.ResponseWriter, request *http.Request) error {
|
|
||||||
options := e.metadata.MITM
|
|
||||||
rawRequestURL := request.URL
|
|
||||||
rawRequestURL.Scheme = "https"
|
|
||||||
if rawRequestURL.Host == "" {
|
|
||||||
rawRequestURL.Host = request.Host
|
|
||||||
}
|
|
||||||
requestURL := rawRequestURL.String()
|
|
||||||
request.RequestURI = ""
|
|
||||||
var requestMatch bool
|
|
||||||
var (
|
|
||||||
body []byte
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
if options.Print && request.ContentLength > 0 && request.ContentLength <= 131072 {
|
|
||||||
body, err = io.ReadAll(request.Body)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "read HTTP request body")
|
|
||||||
}
|
|
||||||
request.Body.Close()
|
|
||||||
request.Body = io.NopCloser(bytes.NewReader(body))
|
|
||||||
}
|
|
||||||
if options.Print {
|
|
||||||
e.printRequest(ctx, request, body)
|
|
||||||
}
|
|
||||||
for i, rule := range options.SurgeURLRewrite {
|
|
||||||
if !rule.Pattern.MatchString(requestURL) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
e.logger.DebugContext(ctx, "match url_rewrite[", i, "] => ", rule.String())
|
|
||||||
if rule.Reject {
|
|
||||||
return E.New("request rejected by url_rewrite")
|
|
||||||
} else if rule.Redirect {
|
|
||||||
http.Redirect(writer, request, rule.Destination.String(), http.StatusFound)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
requestMatch = true
|
|
||||||
request.URL = rule.Destination
|
|
||||||
newDestination := M.ParseSocksaddrHostPortStr(rule.Destination.Hostname(), rule.Destination.Port())
|
|
||||||
if newDestination.Port == 0 {
|
|
||||||
newDestination.Port = e.metadata.Destination.Port
|
|
||||||
}
|
|
||||||
e.metadata.Destination = newDestination
|
|
||||||
e.tlsConfig.ServerName = rule.Destination.Hostname()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
for i, rule := range options.SurgeHeaderRewrite {
|
|
||||||
if rule.Response {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !rule.Pattern.MatchString(requestURL) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
requestMatch = true
|
|
||||||
e.logger.DebugContext(ctx, "match header_rewrite[", i, "] => ", rule.String())
|
|
||||||
switch {
|
|
||||||
case rule.Add:
|
|
||||||
if strings.ToLower(rule.Key) == "host" {
|
|
||||||
request.Host = rule.Value
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
request.Header.Add(rule.Key, rule.Value)
|
|
||||||
case rule.Delete:
|
|
||||||
request.Header.Del(rule.Key)
|
|
||||||
case rule.Replace:
|
|
||||||
if request.Header.Get(rule.Key) != "" {
|
|
||||||
request.Header.Set(rule.Key, rule.Value)
|
|
||||||
}
|
|
||||||
case rule.ReplaceRegex:
|
|
||||||
if value := request.Header.Get(rule.Key); value != "" {
|
|
||||||
request.Header.Set(rule.Key, rule.Match.ReplaceAllString(value, rule.Value))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for i, rule := range options.SurgeBodyRewrite {
|
|
||||||
if rule.Response {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !rule.Pattern.MatchString(requestURL) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
requestMatch = true
|
|
||||||
e.logger.DebugContext(ctx, "match body_rewrite[", i, "] => ", rule.String())
|
|
||||||
var body []byte
|
|
||||||
if request.ContentLength <= 0 {
|
|
||||||
e.logger.WarnContext(ctx, "body replace skipped due to non-fixed content length")
|
|
||||||
break
|
|
||||||
} else if request.ContentLength > 131072 {
|
|
||||||
e.logger.WarnContext(ctx, "body replace skipped due to large content length: ", request.ContentLength)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
body, err := io.ReadAll(request.Body)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "read HTTP request body")
|
|
||||||
}
|
|
||||||
request.Body.Close()
|
|
||||||
for mi := 0; i < len(rule.Match); i++ {
|
|
||||||
body = rule.Match[mi].ReplaceAll(body, []byte(rule.Replace[i]))
|
|
||||||
}
|
|
||||||
request.Body = io.NopCloser(bytes.NewReader(body))
|
|
||||||
request.ContentLength = int64(len(body))
|
|
||||||
}
|
|
||||||
if !requestMatch {
|
|
||||||
for i, rule := range options.SurgeMapLocal {
|
|
||||||
if !rule.Pattern.MatchString(requestURL) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
requestMatch = true
|
|
||||||
e.logger.DebugContext(ctx, "match map_local[", i, "] => ", rule.String())
|
|
||||||
go func() {
|
|
||||||
io.Copy(io.Discard, request.Body)
|
|
||||||
request.Body.Close()
|
|
||||||
}()
|
|
||||||
var (
|
|
||||||
statusCode = http.StatusOK
|
|
||||||
headers = make(http.Header)
|
|
||||||
body []byte
|
|
||||||
)
|
|
||||||
if rule.StatusCode > 0 {
|
|
||||||
statusCode = rule.StatusCode
|
|
||||||
}
|
|
||||||
switch {
|
|
||||||
case rule.File:
|
|
||||||
resource, err := os.ReadFile(rule.Data)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "open map local source")
|
|
||||||
}
|
|
||||||
mimeType := mime.TypeByExtension(filepath.Ext(rule.Data))
|
|
||||||
if mimeType == "" {
|
|
||||||
mimeType = "application/octet-stream"
|
|
||||||
}
|
|
||||||
headers.Set("Content-Type", mimeType)
|
|
||||||
body = resource
|
|
||||||
case rule.Text:
|
|
||||||
headers.Set("Content-Type", "text/plain")
|
|
||||||
body = []byte(rule.Data)
|
|
||||||
case rule.TinyGif:
|
|
||||||
headers.Set("Content-Type", "image/gif")
|
|
||||||
body = surgeTinyGif()
|
|
||||||
case rule.Base64:
|
|
||||||
headers.Set("Content-Type", "application/octet-stream")
|
|
||||||
body = rule.Base64Data
|
|
||||||
}
|
|
||||||
for key, values := range headers {
|
|
||||||
writer.Header()[key] = values
|
|
||||||
}
|
|
||||||
writer.WriteHeader(statusCode)
|
|
||||||
_, err = writer.Write(body)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "write map local response")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
requestCtx, cancel := context.WithCancel(ctx)
|
|
||||||
defer cancel()
|
|
||||||
response, err := e.httpClient.Do(request.WithContext(requestCtx))
|
|
||||||
if err != nil {
|
|
||||||
cancel()
|
|
||||||
return E.Cause(err, "exchange request")
|
|
||||||
}
|
|
||||||
var responseMatch bool
|
|
||||||
var responseBody []byte
|
|
||||||
if options.Print && response.ContentLength > 0 && response.ContentLength <= 131072 {
|
|
||||||
responseBody, err = io.ReadAll(response.Body)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "read HTTP response body")
|
|
||||||
}
|
|
||||||
response.Body.Close()
|
|
||||||
response.Body = io.NopCloser(bytes.NewReader(responseBody))
|
|
||||||
}
|
|
||||||
if options.Print {
|
|
||||||
e.printResponse(ctx, request, response, responseBody)
|
|
||||||
}
|
|
||||||
for i, rule := range options.SurgeHeaderRewrite {
|
|
||||||
if !rule.Response {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !rule.Pattern.MatchString(requestURL) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
responseMatch = true
|
|
||||||
e.logger.DebugContext(ctx, "match header_rewrite[", i, "] => ", rule.String())
|
|
||||||
switch {
|
|
||||||
case rule.Add:
|
|
||||||
response.Header.Add(rule.Key, rule.Value)
|
|
||||||
case rule.Delete:
|
|
||||||
response.Header.Del(rule.Key)
|
|
||||||
case rule.Replace:
|
|
||||||
if response.Header.Get(rule.Key) != "" {
|
|
||||||
response.Header.Set(rule.Key, rule.Value)
|
|
||||||
}
|
|
||||||
case rule.ReplaceRegex:
|
|
||||||
if value := response.Header.Get(rule.Key); value != "" {
|
|
||||||
response.Header.Set(rule.Key, rule.Match.ReplaceAllString(value, rule.Value))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for i, rule := range options.SurgeBodyRewrite {
|
|
||||||
if !rule.Response {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !rule.Pattern.MatchString(requestURL) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
responseMatch = true
|
|
||||||
e.logger.DebugContext(ctx, "match body_rewrite[", i, "] => ", rule.String())
|
|
||||||
if responseBody == nil {
|
|
||||||
if response.ContentLength <= 0 {
|
|
||||||
e.logger.WarnContext(ctx, "body replace skipped due to non-fixed content length")
|
|
||||||
break
|
|
||||||
} else if response.ContentLength > 131072 {
|
|
||||||
e.logger.WarnContext(ctx, "body replace skipped due to large content length: ", request.ContentLength)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
responseBody, err = io.ReadAll(response.Body)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "read HTTP request body")
|
|
||||||
}
|
|
||||||
response.Body.Close()
|
|
||||||
}
|
|
||||||
for mi := 0; i < len(rule.Match); i++ {
|
|
||||||
responseBody = rule.Match[mi].ReplaceAll(responseBody, []byte(rule.Replace[i]))
|
|
||||||
}
|
|
||||||
response.Body = io.NopCloser(bytes.NewReader(responseBody))
|
|
||||||
response.ContentLength = int64(len(responseBody))
|
|
||||||
}
|
|
||||||
if !options.Print && !requestMatch && !responseMatch {
|
|
||||||
e.logger.WarnContext(ctx, "request not modified")
|
|
||||||
}
|
|
||||||
for key, values := range response.Header {
|
|
||||||
writer.Header()[key] = values
|
|
||||||
}
|
|
||||||
writer.WriteHeader(response.StatusCode)
|
|
||||||
_, err = io.Copy(writer, response.Body)
|
|
||||||
response.Body.Close()
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "write HTTP response")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Engine) printRequest(ctx context.Context, request *http.Request, body []byte) {
|
|
||||||
var builder strings.Builder
|
|
||||||
builder.WriteString(F.ToString(request.Proto, " ", request.Method, " ", request.URL))
|
|
||||||
builder.WriteString("\n")
|
|
||||||
if request.URL.Hostname() != "" && request.URL.Hostname() != request.Host {
|
|
||||||
builder.WriteString("Host: ")
|
|
||||||
builder.WriteString(request.Host)
|
|
||||||
builder.WriteString("\n")
|
|
||||||
}
|
|
||||||
for key, values := range request.Header {
|
|
||||||
for _, value := range values {
|
|
||||||
builder.WriteString(key)
|
|
||||||
builder.WriteString(": ")
|
|
||||||
builder.WriteString(value)
|
|
||||||
builder.WriteString("\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(body) > 0 {
|
|
||||||
builder.WriteString("\n")
|
|
||||||
if !bytes.ContainsFunc(body, func(r rune) bool {
|
|
||||||
return !unicode.IsPrint(r) && !unicode.IsSpace(r)
|
|
||||||
}) {
|
|
||||||
builder.Write(body)
|
|
||||||
} else {
|
|
||||||
builder.WriteString("(body not printable)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
e.logger.InfoContext(ctx, "request: ", builder.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Engine) printResponse(ctx context.Context, request *http.Request, response *http.Response, body []byte) {
|
|
||||||
var builder strings.Builder
|
|
||||||
builder.WriteString(F.ToString(response.Proto, " ", response.Status, " ", request.URL))
|
|
||||||
builder.WriteString("\n")
|
|
||||||
for key, values := range response.Header {
|
|
||||||
for _, value := range values {
|
|
||||||
builder.WriteString(key)
|
|
||||||
builder.WriteString(": ")
|
|
||||||
builder.WriteString(value)
|
|
||||||
builder.WriteString("\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(body) > 0 {
|
|
||||||
builder.WriteString("\n")
|
|
||||||
if !bytes.ContainsFunc(body, func(r rune) bool {
|
|
||||||
return !unicode.IsPrint(r) && !unicode.IsSpace(r)
|
|
||||||
}) {
|
|
||||||
builder.Write(body)
|
|
||||||
} else {
|
|
||||||
builder.WriteString("(body not printable)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
e.logger.InfoContext(ctx, "response: ", builder.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
type simpleResponseWriter struct {
|
|
||||||
statusCode int
|
|
||||||
header http.Header
|
|
||||||
body bytes.Buffer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *simpleResponseWriter) Build(request *http.Request) *http.Response {
|
|
||||||
return &http.Response{
|
|
||||||
StatusCode: w.statusCode,
|
|
||||||
Status: http.StatusText(w.statusCode),
|
|
||||||
Proto: request.Proto,
|
|
||||||
ProtoMajor: request.ProtoMajor,
|
|
||||||
ProtoMinor: request.ProtoMinor,
|
|
||||||
Header: w.header,
|
|
||||||
Body: io.NopCloser(&w.body),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *simpleResponseWriter) Header() http.Header {
|
|
||||||
if w.header == nil {
|
|
||||||
w.header = make(http.Header)
|
|
||||||
}
|
|
||||||
return w.header
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *simpleResponseWriter) Write(b []byte) (int, error) {
|
|
||||||
return w.body.Write(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *simpleResponseWriter) WriteHeader(statusCode int) {
|
|
||||||
w.statusCode = statusCode
|
|
||||||
}
|
|
||||||
@@ -11,13 +11,6 @@ type _CertificateOptions struct {
|
|||||||
Certificate badoption.Listable[string] `json:"certificate,omitempty"`
|
Certificate badoption.Listable[string] `json:"certificate,omitempty"`
|
||||||
CertificatePath badoption.Listable[string] `json:"certificate_path,omitempty"`
|
CertificatePath badoption.Listable[string] `json:"certificate_path,omitempty"`
|
||||||
CertificateDirectoryPath badoption.Listable[string] `json:"certificate_directory_path,omitempty"`
|
CertificateDirectoryPath badoption.Listable[string] `json:"certificate_directory_path,omitempty"`
|
||||||
TLSDecryption *TLSDecryptionOptions `json:"tls_decryption,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type TLSDecryptionOptions struct {
|
|
||||||
Enabled bool `json:"enabled,omitempty"`
|
|
||||||
KeyPair string `json:"key_pair_p12,omitempty"`
|
|
||||||
KeyPairPassword string `json:"key_pair_p12_password,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type CertificateOptions _CertificateOptions
|
type CertificateOptions _CertificateOptions
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
package option
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/sagernet/sing/common/json/badoption"
|
|
||||||
)
|
|
||||||
|
|
||||||
type MITMOptions struct {
|
|
||||||
Enabled bool `json:"enabled,omitempty"`
|
|
||||||
HTTP2Enabled bool `json:"http2_enabled,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type MITMRouteOptions struct {
|
|
||||||
Enabled bool `json:"enabled,omitempty"`
|
|
||||||
Print bool `json:"print,omitempty"`
|
|
||||||
SurgeURLRewrite badoption.Listable[SurgeURLRewriteLine] `json:"surge_url_rewrite,omitempty"`
|
|
||||||
SurgeHeaderRewrite badoption.Listable[SurgeHeaderRewriteLine] `json:"surge_header_rewrite,omitempty"`
|
|
||||||
SurgeBodyRewrite badoption.Listable[SurgeBodyRewriteLine] `json:"surge_body_rewrite,omitempty"`
|
|
||||||
SurgeMapLocal badoption.Listable[SurgeMapLocalLine] `json:"surge_map_local,omitempty"`
|
|
||||||
}
|
|
||||||
@@ -1,449 +0,0 @@
|
|||||||
package option
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/base64"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"regexp"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"unicode"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing/common"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
F "github.com/sagernet/sing/common/format"
|
|
||||||
"github.com/sagernet/sing/common/json"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SurgeURLRewriteLine struct {
|
|
||||||
Pattern *regexp.Regexp
|
|
||||||
Destination *url.URL
|
|
||||||
Redirect bool
|
|
||||||
Reject bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l SurgeURLRewriteLine) String() string {
|
|
||||||
var fields []string
|
|
||||||
fields = append(fields, l.Pattern.String())
|
|
||||||
if l.Reject {
|
|
||||||
fields = append(fields, "_")
|
|
||||||
} else {
|
|
||||||
fields = append(fields, l.Destination.String())
|
|
||||||
}
|
|
||||||
switch {
|
|
||||||
case l.Redirect:
|
|
||||||
fields = append(fields, "302")
|
|
||||||
case l.Reject:
|
|
||||||
fields = append(fields, "reject")
|
|
||||||
default:
|
|
||||||
fields = append(fields, "header")
|
|
||||||
}
|
|
||||||
return encodeSurgeKeys(fields)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l SurgeURLRewriteLine) MarshalJSON() ([]byte, error) {
|
|
||||||
return json.Marshal(l.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *SurgeURLRewriteLine) UnmarshalJSON(bytes []byte) error {
|
|
||||||
var stringValue string
|
|
||||||
err := json.Unmarshal(bytes, &stringValue)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
fields, err := surgeFields(stringValue)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "invalid surge_url_rewrite line: ", stringValue)
|
|
||||||
} else if len(fields) < 2 || len(fields) > 3 {
|
|
||||||
return E.New("invalid surge_url_rewrite line: ", stringValue)
|
|
||||||
}
|
|
||||||
pattern, err := regexp.Compile(fields[0].Key)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "invalid surge_url_rewrite line: invalid pattern: ", stringValue)
|
|
||||||
}
|
|
||||||
l.Pattern = pattern
|
|
||||||
l.Destination, err = url.Parse(fields[1].Key)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "invalid surge_url_rewrite line: invalid destination: ", stringValue)
|
|
||||||
}
|
|
||||||
if len(fields) == 3 {
|
|
||||||
switch fields[2].Key {
|
|
||||||
case "header":
|
|
||||||
case "302":
|
|
||||||
l.Redirect = true
|
|
||||||
case "reject":
|
|
||||||
l.Reject = true
|
|
||||||
default:
|
|
||||||
return E.New("invalid surge_url_rewrite line: invalid action: ", stringValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type SurgeHeaderRewriteLine struct {
|
|
||||||
Response bool
|
|
||||||
Pattern *regexp.Regexp
|
|
||||||
Add bool
|
|
||||||
Delete bool
|
|
||||||
Replace bool
|
|
||||||
ReplaceRegex bool
|
|
||||||
Key string
|
|
||||||
Match *regexp.Regexp
|
|
||||||
Value string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l SurgeHeaderRewriteLine) String() string {
|
|
||||||
var fields []string
|
|
||||||
if !l.Response {
|
|
||||||
fields = append(fields, "http-request")
|
|
||||||
} else {
|
|
||||||
fields = append(fields, "http-response")
|
|
||||||
}
|
|
||||||
fields = append(fields, l.Pattern.String())
|
|
||||||
if l.Add {
|
|
||||||
fields = append(fields, "header-add")
|
|
||||||
} else if l.Delete {
|
|
||||||
fields = append(fields, "header-del")
|
|
||||||
} else if l.Replace {
|
|
||||||
fields = append(fields, "header-replace")
|
|
||||||
} else if l.ReplaceRegex {
|
|
||||||
fields = append(fields, "header-replace-regex")
|
|
||||||
}
|
|
||||||
fields = append(fields, l.Key)
|
|
||||||
if l.Add || l.Replace {
|
|
||||||
fields = append(fields, l.Value)
|
|
||||||
} else if l.ReplaceRegex {
|
|
||||||
fields = append(fields, l.Match.String(), l.Value)
|
|
||||||
}
|
|
||||||
return encodeSurgeKeys(fields)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l SurgeHeaderRewriteLine) MarshalJSON() ([]byte, error) {
|
|
||||||
return json.Marshal(l.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *SurgeHeaderRewriteLine) UnmarshalJSON(bytes []byte) error {
|
|
||||||
var stringValue string
|
|
||||||
err := json.Unmarshal(bytes, &stringValue)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
fields, err := surgeFields(stringValue)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "invalid surge_header_rewrite line: ", stringValue)
|
|
||||||
} else if len(fields) < 4 {
|
|
||||||
return E.New("invalid surge_header_rewrite line: ", stringValue)
|
|
||||||
}
|
|
||||||
switch fields[0].Key {
|
|
||||||
case "http-request":
|
|
||||||
case "http-response":
|
|
||||||
l.Response = true
|
|
||||||
default:
|
|
||||||
return E.New("invalid surge_header_rewrite line: invalid type: ", stringValue)
|
|
||||||
}
|
|
||||||
l.Pattern, err = regexp.Compile(fields[1].Key)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "invalid surge_header_rewrite line: invalid pattern: ", stringValue)
|
|
||||||
}
|
|
||||||
switch fields[2].Key {
|
|
||||||
case "header-add":
|
|
||||||
l.Add = true
|
|
||||||
if len(fields) != 5 {
|
|
||||||
return E.New("invalid surge_header_rewrite line: " + stringValue)
|
|
||||||
}
|
|
||||||
l.Key = fields[3].Key
|
|
||||||
l.Value = fields[4].Key
|
|
||||||
case "header-del":
|
|
||||||
l.Delete = true
|
|
||||||
l.Key = fields[3].Key
|
|
||||||
case "header-replace":
|
|
||||||
l.Replace = true
|
|
||||||
if len(fields) != 5 {
|
|
||||||
return E.New("invalid surge_header_rewrite line: " + stringValue)
|
|
||||||
}
|
|
||||||
l.Key = fields[3].Key
|
|
||||||
l.Value = fields[4].Key
|
|
||||||
case "header-replace-regex":
|
|
||||||
l.ReplaceRegex = true
|
|
||||||
if len(fields) != 6 {
|
|
||||||
return E.New("invalid surge_header_rewrite line: " + stringValue)
|
|
||||||
}
|
|
||||||
l.Key = fields[3].Key
|
|
||||||
l.Match, err = regexp.Compile(fields[4].Key)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "invalid surge_header_rewrite line: invalid match: ", stringValue)
|
|
||||||
}
|
|
||||||
l.Value = fields[5].Key
|
|
||||||
default:
|
|
||||||
return E.New("invalid surge_header_rewrite line: invalid action: ", stringValue)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type SurgeBodyRewriteLine struct {
|
|
||||||
Response bool
|
|
||||||
Pattern *regexp.Regexp
|
|
||||||
Match []*regexp.Regexp
|
|
||||||
Replace []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l SurgeBodyRewriteLine) String() string {
|
|
||||||
var fields []string
|
|
||||||
if !l.Response {
|
|
||||||
fields = append(fields, "http-request")
|
|
||||||
} else {
|
|
||||||
fields = append(fields, "http-response")
|
|
||||||
}
|
|
||||||
for i := 0; i < len(l.Match); i += 2 {
|
|
||||||
fields = append(fields, l.Match[i].String(), l.Replace[i])
|
|
||||||
}
|
|
||||||
return strings.Join(fields, " ")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l SurgeBodyRewriteLine) MarshalJSON() ([]byte, error) {
|
|
||||||
return json.Marshal(l.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *SurgeBodyRewriteLine) UnmarshalJSON(bytes []byte) error {
|
|
||||||
var stringValue string
|
|
||||||
err := json.Unmarshal(bytes, &stringValue)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
fields, err := surgeFields(stringValue)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "invalid surge_body_rewrite line: ", stringValue)
|
|
||||||
} else if len(fields) < 4 {
|
|
||||||
return E.New("invalid surge_body_rewrite line: ", stringValue)
|
|
||||||
} else if len(fields)%2 != 0 {
|
|
||||||
return E.New("invalid surge_body_rewrite line: ", stringValue)
|
|
||||||
}
|
|
||||||
switch fields[0].Key {
|
|
||||||
case "http-request":
|
|
||||||
case "http-response":
|
|
||||||
l.Response = true
|
|
||||||
default:
|
|
||||||
return E.New("invalid surge_body_rewrite line: invalid type: ", stringValue)
|
|
||||||
}
|
|
||||||
l.Pattern, err = regexp.Compile(fields[1].Key)
|
|
||||||
for i := 2; i < len(fields); i += 2 {
|
|
||||||
var match *regexp.Regexp
|
|
||||||
match, err = regexp.Compile(fields[i].Key)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "invalid surge_body_rewrite line: invalid match: ", stringValue)
|
|
||||||
}
|
|
||||||
l.Match = append(l.Match, match)
|
|
||||||
l.Replace = append(l.Replace, fields[i+1].Key)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type SurgeMapLocalLine struct {
|
|
||||||
Pattern *regexp.Regexp
|
|
||||||
StatusCode int
|
|
||||||
File bool
|
|
||||||
Text bool
|
|
||||||
TinyGif bool
|
|
||||||
Base64 bool
|
|
||||||
Data string
|
|
||||||
Base64Data []byte
|
|
||||||
Headers http.Header
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l SurgeMapLocalLine) String() string {
|
|
||||||
var fields []surgeField
|
|
||||||
fields = append(fields, surgeField{Key: l.Pattern.String()})
|
|
||||||
if l.File {
|
|
||||||
fields = append(fields, surgeField{Key: "data-type", Value: "file"})
|
|
||||||
fields = append(fields, surgeField{Key: "data", Value: l.Data})
|
|
||||||
} else if l.Text {
|
|
||||||
fields = append(fields, surgeField{Key: "data-type", Value: "text"})
|
|
||||||
fields = append(fields, surgeField{Key: "data", Value: l.Data})
|
|
||||||
} else if l.TinyGif {
|
|
||||||
fields = append(fields, surgeField{Key: "data-type", Value: "tiny-gif"})
|
|
||||||
} else if l.Base64 {
|
|
||||||
fields = append(fields, surgeField{Key: "data-type", Value: "base64"})
|
|
||||||
fields = append(fields, surgeField{Key: "data-type", Value: base64.StdEncoding.EncodeToString(l.Base64Data)})
|
|
||||||
}
|
|
||||||
if l.StatusCode != 0 {
|
|
||||||
fields = append(fields, surgeField{Key: "status-code", Value: F.ToString(l.StatusCode), ValueSet: true})
|
|
||||||
}
|
|
||||||
if len(l.Headers) > 0 {
|
|
||||||
var headers []string
|
|
||||||
for key, values := range l.Headers {
|
|
||||||
for _, value := range values {
|
|
||||||
headers = append(headers, key+":"+value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fields = append(fields, surgeField{Key: "headers", Value: strings.Join(headers, "|")})
|
|
||||||
}
|
|
||||||
return encodeSurgeFields(fields)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l SurgeMapLocalLine) MarshalJSON() ([]byte, error) {
|
|
||||||
return json.Marshal(l.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *SurgeMapLocalLine) UnmarshalJSON(bytes []byte) error {
|
|
||||||
var stringValue string
|
|
||||||
err := json.Unmarshal(bytes, &stringValue)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
fields, err := surgeFields(stringValue)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "invalid surge_map_local line: ", stringValue)
|
|
||||||
} else if len(fields) < 1 {
|
|
||||||
return E.New("invalid surge_map_local line: ", stringValue)
|
|
||||||
}
|
|
||||||
l.Pattern, err = regexp.Compile(fields[0].Key)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "invalid surge_map_local line: invalid pattern: ", stringValue)
|
|
||||||
}
|
|
||||||
dataTypeField := common.Find(fields, func(it surgeField) bool {
|
|
||||||
return it.Key == "data-type"
|
|
||||||
})
|
|
||||||
if !dataTypeField.ValueSet {
|
|
||||||
return E.New("invalid surge_map_local line: missing data-type: ", stringValue)
|
|
||||||
}
|
|
||||||
switch dataTypeField.Value {
|
|
||||||
case "file":
|
|
||||||
l.File = true
|
|
||||||
case "text":
|
|
||||||
l.Text = true
|
|
||||||
case "tiny-gif":
|
|
||||||
l.TinyGif = true
|
|
||||||
case "base64":
|
|
||||||
l.Base64 = true
|
|
||||||
default:
|
|
||||||
return E.New("unsupported data-type ", dataTypeField.Value)
|
|
||||||
}
|
|
||||||
for i := 1; i < len(fields); i++ {
|
|
||||||
switch fields[i].Key {
|
|
||||||
case "data-type":
|
|
||||||
continue
|
|
||||||
case "data":
|
|
||||||
if l.File {
|
|
||||||
l.Data = fields[i].Value
|
|
||||||
} else if l.Text {
|
|
||||||
l.Data = fields[i].Value
|
|
||||||
} else if l.Base64 {
|
|
||||||
l.Base64Data, err = base64.StdEncoding.DecodeString(fields[i].Value)
|
|
||||||
if err != nil {
|
|
||||||
return E.New("invalid surge_map_local line: invalid base64 data: ", stringValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case "status-code":
|
|
||||||
statusCode, err := strconv.ParseInt(fields[i].Value, 10, 16)
|
|
||||||
if err != nil {
|
|
||||||
return E.New("invalid surge_map_local line: invalid status code: ", stringValue)
|
|
||||||
}
|
|
||||||
l.StatusCode = int(statusCode)
|
|
||||||
case "header":
|
|
||||||
headers := make(http.Header)
|
|
||||||
for _, headerLine := range strings.Split(fields[i].Value, "|") {
|
|
||||||
if !strings.Contains(headerLine, ":") {
|
|
||||||
return E.New("invalid surge_map_local line: headers: missing `:` in item: ", stringValue, ": ", headerLine)
|
|
||||||
}
|
|
||||||
headers.Add(common.SubstringBefore(headerLine, ":"), common.SubstringAfter(headerLine, ":"))
|
|
||||||
}
|
|
||||||
l.Headers = headers
|
|
||||||
default:
|
|
||||||
return E.New("invalid surge_map_local line: unknown options: ", fields[i].Key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type surgeField struct {
|
|
||||||
Key string
|
|
||||||
Value string
|
|
||||||
ValueSet bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func encodeSurgeKeys(keys []string) string {
|
|
||||||
keys = common.Map(keys, func(it string) string {
|
|
||||||
if strings.ContainsFunc(it, unicode.IsSpace) {
|
|
||||||
return "\"" + it + "\""
|
|
||||||
} else {
|
|
||||||
return it
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return strings.Join(keys, " ")
|
|
||||||
}
|
|
||||||
|
|
||||||
func encodeSurgeFields(fields []surgeField) string {
|
|
||||||
return strings.Join(common.Map(fields, func(it surgeField) string {
|
|
||||||
if !it.ValueSet {
|
|
||||||
if strings.ContainsFunc(it.Key, unicode.IsSpace) {
|
|
||||||
return "\"" + it.Key + "\""
|
|
||||||
} else {
|
|
||||||
return it.Key
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if strings.ContainsFunc(it.Value, unicode.IsSpace) {
|
|
||||||
return it.Key + "=\"" + it.Value + "\""
|
|
||||||
} else {
|
|
||||||
return it.Key + "=" + it.Value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}), " ")
|
|
||||||
}
|
|
||||||
|
|
||||||
func surgeFields(s string) ([]surgeField, error) {
|
|
||||||
var (
|
|
||||||
fields []surgeField
|
|
||||||
currentField *surgeField
|
|
||||||
)
|
|
||||||
for _, field := range strings.Fields(s) {
|
|
||||||
if currentField != nil {
|
|
||||||
field = " " + field
|
|
||||||
if strings.HasSuffix(field, "\"") {
|
|
||||||
field = field[:len(field)-1]
|
|
||||||
if !currentField.ValueSet {
|
|
||||||
currentField.Key += field
|
|
||||||
} else {
|
|
||||||
currentField.Value += field
|
|
||||||
}
|
|
||||||
fields = append(fields, *currentField)
|
|
||||||
currentField = nil
|
|
||||||
} else {
|
|
||||||
if !currentField.ValueSet {
|
|
||||||
currentField.Key += field
|
|
||||||
} else {
|
|
||||||
currentField.Value += field
|
|
||||||
}
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !strings.Contains(field, "=") {
|
|
||||||
if strings.HasPrefix(field, "\"") {
|
|
||||||
field = field[1:]
|
|
||||||
if strings.HasSuffix(field, "\"") {
|
|
||||||
field = field[:len(field)-1]
|
|
||||||
} else {
|
|
||||||
currentField = &surgeField{Key: field}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fields = append(fields, surgeField{Key: field})
|
|
||||||
} else {
|
|
||||||
key := common.SubstringBefore(field, "=")
|
|
||||||
value := common.SubstringAfter(field, "=")
|
|
||||||
if strings.HasPrefix(value, "\"") {
|
|
||||||
value = value[1:]
|
|
||||||
if strings.HasSuffix(field, "\"") {
|
|
||||||
value = value[:len(value)-1]
|
|
||||||
} else {
|
|
||||||
currentField = &surgeField{Key: key, Value: value, ValueSet: true}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fields = append(fields, surgeField{Key: key, Value: value, ValueSet: true})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if currentField != nil {
|
|
||||||
return nil, E.New("invalid surge fields line: ", s)
|
|
||||||
}
|
|
||||||
return fields, nil
|
|
||||||
}
|
|
||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
"github.com/sagernet/sing/common/json"
|
"github.com/sagernet/sing/common/json"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -12,14 +13,13 @@ type _Options struct {
|
|||||||
Schema string `json:"$schema,omitempty"`
|
Schema string `json:"$schema,omitempty"`
|
||||||
Log *LogOptions `json:"log,omitempty"`
|
Log *LogOptions `json:"log,omitempty"`
|
||||||
DNS *DNSOptions `json:"dns,omitempty"`
|
DNS *DNSOptions `json:"dns,omitempty"`
|
||||||
|
NTP *NTPOptions `json:"ntp,omitempty"`
|
||||||
|
Certificate *CertificateOptions `json:"certificate,omitempty"`
|
||||||
Endpoints []Endpoint `json:"endpoints,omitempty"`
|
Endpoints []Endpoint `json:"endpoints,omitempty"`
|
||||||
Inbounds []Inbound `json:"inbounds,omitempty"`
|
Inbounds []Inbound `json:"inbounds,omitempty"`
|
||||||
Outbounds []Outbound `json:"outbounds,omitempty"`
|
Outbounds []Outbound `json:"outbounds,omitempty"`
|
||||||
Route *RouteOptions `json:"route,omitempty"`
|
Route *RouteOptions `json:"route,omitempty"`
|
||||||
Experimental *ExperimentalOptions `json:"experimental,omitempty"`
|
Experimental *ExperimentalOptions `json:"experimental,omitempty"`
|
||||||
NTP *NTPOptions `json:"ntp,omitempty"`
|
|
||||||
Certificate *CertificateOptions `json:"certificate,omitempty"`
|
|
||||||
MITM *MITMOptions `json:"mitm,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Options _Options
|
type Options _Options
|
||||||
@@ -32,7 +32,7 @@ func (o *Options) UnmarshalJSONContext(ctx context.Context, content []byte) erro
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
o.RawMessage = content
|
o.RawMessage = content
|
||||||
return nil
|
return checkOptions(o)
|
||||||
}
|
}
|
||||||
|
|
||||||
type LogOptions struct {
|
type LogOptions struct {
|
||||||
@@ -44,3 +44,52 @@ type LogOptions struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type StubOptions struct{}
|
type StubOptions struct{}
|
||||||
|
|
||||||
|
func checkOptions(options *Options) error {
|
||||||
|
err := checkInbounds(options.Inbounds)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = checkOutbounds(options.Outbounds, options.Endpoints)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkInbounds(inbounds []Inbound) error {
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
for _, inbound := range inbounds {
|
||||||
|
if inbound.Tag == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if seen[inbound.Tag] {
|
||||||
|
return E.New("duplicate inbound tag: ", inbound.Tag)
|
||||||
|
}
|
||||||
|
seen[inbound.Tag] = true
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkOutbounds(outbounds []Outbound, endpoints []Endpoint) error {
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
for _, outbound := range outbounds {
|
||||||
|
if outbound.Tag == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if seen[outbound.Tag] {
|
||||||
|
return E.New("duplicate outbound/endpoint tag: ", outbound.Tag)
|
||||||
|
}
|
||||||
|
seen[outbound.Tag] = true
|
||||||
|
}
|
||||||
|
for _, endpoint := range endpoints {
|
||||||
|
if endpoint.Tag == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if seen[endpoint.Tag] {
|
||||||
|
return E.New("duplicate outbound/endpoint tag: ", endpoint.Tag)
|
||||||
|
}
|
||||||
|
seen[endpoint.Tag] = true
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -158,8 +158,6 @@ type RawRouteOptionsActionOptions struct {
|
|||||||
|
|
||||||
TLSFragment bool `json:"tls_fragment,omitempty"`
|
TLSFragment bool `json:"tls_fragment,omitempty"`
|
||||||
TLSFragmentFallbackDelay badoption.Duration `json:"tls_fragment_fallback_delay,omitempty"`
|
TLSFragmentFallbackDelay badoption.Duration `json:"tls_fragment_fallback_delay,omitempty"`
|
||||||
|
|
||||||
MITM *MITMRouteOptions `json:"mitm,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type RouteOptionsActionOptions RawRouteOptionsActionOptions
|
type RouteOptionsActionOptions RawRouteOptionsActionOptions
|
||||||
|
|||||||
@@ -48,7 +48,12 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
|
|||||||
if options.Detour != "" {
|
if options.Detour != "" {
|
||||||
return nil, E.New("`detour` is not supported in direct context")
|
return nil, E.New("`detour` is not supported in direct context")
|
||||||
}
|
}
|
||||||
outboundDialer, err := dialer.New(ctx, options.DialerOptions, true)
|
outboundDialer, err := dialer.NewWithOptions(dialer.Options{
|
||||||
|
Context: ctx,
|
||||||
|
Options: options.DialerOptions,
|
||||||
|
RemoteIsDomain: true,
|
||||||
|
DirectOutbound: true,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ package tailscale
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"net/http"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
@@ -147,6 +149,17 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL
|
|||||||
return dnsRouter.Lookup(ctx, host, outboundDialer.(dialer.ResolveDialer).QueryOptions())
|
return dnsRouter.Lookup(ctx, host, outboundDialer.(dialer.ResolveDialer).QueryOptions())
|
||||||
},
|
},
|
||||||
DNS: &dnsConfigurtor{},
|
DNS: &dnsConfigurtor{},
|
||||||
|
HTTPClient: &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
ForceAttemptHTTP2: true,
|
||||||
|
DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||||
|
return outboundDialer.DialContext(ctx, network, M.ParseSocksaddr(address))
|
||||||
|
},
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
RootCAs: adapter.RootPoolFromContext(ctx),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
return &Endpoint{
|
return &Endpoint{
|
||||||
Adapter: endpoint.NewAdapter(C.TypeTailscale, tag, []string{N.NetworkTCP, N.NetworkUDP}, nil),
|
Adapter: endpoint.NewAdapter(C.TypeTailscale, tag, []string{N.NetworkTCP, N.NetworkUDP}, nil),
|
||||||
@@ -446,6 +459,10 @@ func (t *Endpoint) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn,
|
|||||||
t.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
|
t.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *Endpoint) Server() *tsnet.Server {
|
||||||
|
return t.server
|
||||||
|
}
|
||||||
|
|
||||||
func addressFromAddr(destination netip.Addr) tcpip.Address {
|
func addressFromAddr(destination netip.Addr) tcpip.Address {
|
||||||
if destination.Is6() {
|
if destination.Is6() {
|
||||||
return tcpip.AddrFrom16(destination.As16())
|
return tcpip.AddrFrom16(destination.As16())
|
||||||
|
|||||||
@@ -245,7 +245,7 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Cause(err, "initialize auto-redirect")
|
return nil, E.Cause(err, "initialize auto-redirect")
|
||||||
}
|
}
|
||||||
if !C.IsAndroid && (len(inbound.routeRuleSet) > 0 || len(inbound.routeExcludeRuleSet) > 0) {
|
if !C.IsAndroid {
|
||||||
inbound.tunOptions.AutoRedirectMarkMode = true
|
inbound.tunOptions.AutoRedirectMarkMode = true
|
||||||
err = networkManager.RegisterAutoRedirectOutputMark(inbound.tunOptions.AutoRedirectOutputMark)
|
err = networkManager.RegisterAutoRedirectOutputMark(inbound.tunOptions.AutoRedirectOutputMark)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -24,31 +24,23 @@ import (
|
|||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
"github.com/sagernet/sing/common/x/list"
|
"github.com/sagernet/sing/common/x/list"
|
||||||
"github.com/sagernet/sing/service"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ adapter.ConnectionManager = (*ConnectionManager)(nil)
|
var _ adapter.ConnectionManager = (*ConnectionManager)(nil)
|
||||||
|
|
||||||
type ConnectionManager struct {
|
type ConnectionManager struct {
|
||||||
ctx context.Context
|
|
||||||
logger logger.ContextLogger
|
logger logger.ContextLogger
|
||||||
mitm adapter.MITMEngine
|
|
||||||
access sync.Mutex
|
access sync.Mutex
|
||||||
connections list.List[io.Closer]
|
connections list.List[io.Closer]
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConnectionManager(ctx context.Context, logger logger.ContextLogger) *ConnectionManager {
|
func NewConnectionManager(logger logger.ContextLogger) *ConnectionManager {
|
||||||
return &ConnectionManager{
|
return &ConnectionManager{
|
||||||
ctx: ctx,
|
|
||||||
logger: logger,
|
logger: logger,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *ConnectionManager) Start(stage adapter.StartStage) error {
|
func (m *ConnectionManager) Start(stage adapter.StartStage) error {
|
||||||
switch stage {
|
|
||||||
case adapter.StartStateInitialize:
|
|
||||||
m.mitm = service.FromContext[adapter.MITMEngine](m.ctx)
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,14 +55,6 @@ func (m *ConnectionManager) Close() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *ConnectionManager) NewConnection(ctx context.Context, this N.Dialer, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
|
func (m *ConnectionManager) NewConnection(ctx context.Context, this N.Dialer, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
|
||||||
if metadata.MITM != nil && metadata.MITM.Enabled {
|
|
||||||
if m.mitm == nil {
|
|
||||||
m.logger.WarnContext(ctx, "MITM disabled")
|
|
||||||
} else {
|
|
||||||
m.mitm.NewConnection(ctx, this, conn, metadata, onClose)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ctx = adapter.WithContext(ctx, &metadata)
|
ctx = adapter.WithContext(ctx, &metadata)
|
||||||
var (
|
var (
|
||||||
remoteConn net.Conn
|
remoteConn net.Conn
|
||||||
|
|||||||
@@ -458,9 +458,6 @@ match:
|
|||||||
metadata.TLSFragment = true
|
metadata.TLSFragment = true
|
||||||
metadata.TLSFragmentFallbackDelay = routeOptions.TLSFragmentFallbackDelay
|
metadata.TLSFragmentFallbackDelay = routeOptions.TLSFragmentFallbackDelay
|
||||||
}
|
}
|
||||||
if routeOptions.MITM != nil && routeOptions.MITM.Enabled {
|
|
||||||
metadata.MITM = routeOptions.MITM
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
switch action := currentRule.Action().(type) {
|
switch action := currentRule.Action().(type) {
|
||||||
case *rule.RuleActionSniff:
|
case *rule.RuleActionSniff:
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ func NewRuleAction(ctx context.Context, logger logger.ContextLogger, action opti
|
|||||||
UDPConnect: action.RouteOptions.UDPConnect,
|
UDPConnect: action.RouteOptions.UDPConnect,
|
||||||
TLSFragment: action.RouteOptions.TLSFragment,
|
TLSFragment: action.RouteOptions.TLSFragment,
|
||||||
TLSFragmentFallbackDelay: time.Duration(action.RouteOptions.TLSFragmentFallbackDelay),
|
TLSFragmentFallbackDelay: time.Duration(action.RouteOptions.TLSFragmentFallbackDelay),
|
||||||
MITM: action.RouteOptions.MITM,
|
|
||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
case C.RuleActionTypeRouteOptions:
|
case C.RuleActionTypeRouteOptions:
|
||||||
@@ -54,7 +53,6 @@ func NewRuleAction(ctx context.Context, logger logger.ContextLogger, action opti
|
|||||||
UDPTimeout: time.Duration(action.RouteOptionsOptions.UDPTimeout),
|
UDPTimeout: time.Duration(action.RouteOptionsOptions.UDPTimeout),
|
||||||
TLSFragment: action.RouteOptionsOptions.TLSFragment,
|
TLSFragment: action.RouteOptionsOptions.TLSFragment,
|
||||||
TLSFragmentFallbackDelay: time.Duration(action.RouteOptionsOptions.TLSFragmentFallbackDelay),
|
TLSFragmentFallbackDelay: time.Duration(action.RouteOptionsOptions.TLSFragmentFallbackDelay),
|
||||||
MITM: action.RouteOptionsOptions.MITM,
|
|
||||||
}, nil
|
}, nil
|
||||||
case C.RuleActionTypeDirect:
|
case C.RuleActionTypeDirect:
|
||||||
directDialer, err := dialer.New(ctx, option.DialerOptions(action.DirectOptions), false)
|
directDialer, err := dialer.New(ctx, option.DialerOptions(action.DirectOptions), false)
|
||||||
@@ -154,7 +152,15 @@ func (r *RuleActionRoute) Type() string {
|
|||||||
func (r *RuleActionRoute) String() string {
|
func (r *RuleActionRoute) String() string {
|
||||||
var descriptions []string
|
var descriptions []string
|
||||||
descriptions = append(descriptions, r.Outbound)
|
descriptions = append(descriptions, r.Outbound)
|
||||||
descriptions = append(descriptions, r.Descriptions()...)
|
if r.UDPDisableDomainUnmapping {
|
||||||
|
descriptions = append(descriptions, "udp-disable-domain-unmapping")
|
||||||
|
}
|
||||||
|
if r.UDPConnect {
|
||||||
|
descriptions = append(descriptions, "udp-connect")
|
||||||
|
}
|
||||||
|
if r.TLSFragment {
|
||||||
|
descriptions = append(descriptions, "tls-fragment")
|
||||||
|
}
|
||||||
return F.ToString("route(", strings.Join(descriptions, ","), ")")
|
return F.ToString("route(", strings.Join(descriptions, ","), ")")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,14 +176,13 @@ type RuleActionRouteOptions struct {
|
|||||||
UDPTimeout time.Duration
|
UDPTimeout time.Duration
|
||||||
TLSFragment bool
|
TLSFragment bool
|
||||||
TLSFragmentFallbackDelay time.Duration
|
TLSFragmentFallbackDelay time.Duration
|
||||||
MITM *option.MITMRouteOptions
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RuleActionRouteOptions) Type() string {
|
func (r *RuleActionRouteOptions) Type() string {
|
||||||
return C.RuleActionTypeRouteOptions
|
return C.RuleActionTypeRouteOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RuleActionRouteOptions) Descriptions() []string {
|
func (r *RuleActionRouteOptions) String() string {
|
||||||
var descriptions []string
|
var descriptions []string
|
||||||
if r.OverrideAddress.IsValid() {
|
if r.OverrideAddress.IsValid() {
|
||||||
descriptions = append(descriptions, F.ToString("override-address=", r.OverrideAddress.AddrString()))
|
descriptions = append(descriptions, F.ToString("override-address=", r.OverrideAddress.AddrString()))
|
||||||
@@ -204,22 +209,9 @@ func (r *RuleActionRouteOptions) Descriptions() []string {
|
|||||||
descriptions = append(descriptions, "udp-connect")
|
descriptions = append(descriptions, "udp-connect")
|
||||||
}
|
}
|
||||||
if r.UDPTimeout > 0 {
|
if r.UDPTimeout > 0 {
|
||||||
descriptions = append(descriptions, F.ToString("udp-timeout=", r.UDPTimeout))
|
descriptions = append(descriptions, "udp-timeout")
|
||||||
}
|
}
|
||||||
if r.TLSFragment {
|
return F.ToString("route-options(", strings.Join(descriptions, ","), ")")
|
||||||
descriptions = append(descriptions, "tls-fragment")
|
|
||||||
if r.TLSFragmentFallbackDelay > 0 {
|
|
||||||
descriptions = append(descriptions, F.ToString("tls-fragment-fallbac-delay=", r.TLSFragmentFallbackDelay.String()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if r.MITM != nil && r.MITM.Enabled {
|
|
||||||
descriptions = append(descriptions, "mitm")
|
|
||||||
}
|
|
||||||
return descriptions
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RuleActionRouteOptions) String() string {
|
|
||||||
return F.ToString("route-options(", strings.Join(r.Descriptions(), ","), ")")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type RuleActionDNSRoute struct {
|
type RuleActionDNSRoute struct {
|
||||||
|
|||||||
Reference in New Issue
Block a user