mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-04-12 01:57:18 +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}"
|
||||
echo "DIR_NAME=${DIR_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
|
||||
if: matrix.debian != ''
|
||||
run: |
|
||||
@@ -183,7 +186,7 @@ jobs:
|
||||
sudo gem install fpm
|
||||
sudo apt-get install -y debsigs
|
||||
fpm -t deb \
|
||||
-v "${{ needs.calculate_version.outputs.version }}" \
|
||||
-v "$PKG_VERSION" \
|
||||
-p "dist/${PKG_NAME}.deb" \
|
||||
--architecture ${{ matrix.debian }} \
|
||||
dist/sing-box=/usr/bin/sing-box
|
||||
@@ -200,7 +203,7 @@ jobs:
|
||||
set -xeuo pipefail
|
||||
sudo gem install fpm
|
||||
fpm -t rpm \
|
||||
-v "${{ needs.calculate_version.outputs.version }}" \
|
||||
-v "$PKG_VERSION" \
|
||||
-p "dist/${PKG_NAME}.rpm" \
|
||||
--architecture ${{ matrix.rpm }} \
|
||||
dist/sing-box=/usr/bin/sing-box
|
||||
@@ -219,7 +222,7 @@ jobs:
|
||||
sudo gem install fpm
|
||||
sudo apt-get install -y libarchive-tools
|
||||
fpm -t pacman \
|
||||
-v "${{ needs.calculate_version.outputs.version }}" \
|
||||
-v "$PKG_VERSION" \
|
||||
-p "dist/${PKG_NAME}.pkg.tar.zst" \
|
||||
--architecture ${{ matrix.pacman }} \
|
||||
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, '-')
|
||||
run: |-
|
||||
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
|
||||
if: matrix.debian != ''
|
||||
run: |
|
||||
@@ -117,7 +122,7 @@ jobs:
|
||||
sudo apt-get install -y debsigs
|
||||
fpm -t deb \
|
||||
--name "${NAME}" \
|
||||
-v "${{ needs.calculate_version.outputs.version }}" \
|
||||
-v "$PKG_VERSION" \
|
||||
-p "dist/${NAME}_${{ needs.calculate_version.outputs.version }}_linux_${{ matrix.debian }}.deb" \
|
||||
--architecture ${{ matrix.debian }} \
|
||||
dist/sing-box=/usr/bin/sing-box
|
||||
@@ -135,7 +140,7 @@ jobs:
|
||||
sudo gem install fpm
|
||||
fpm -t rpm \
|
||||
--name "${NAME}" \
|
||||
-v "${{ needs.calculate_version.outputs.version }}" \
|
||||
-v "$PKG_VERSION" \
|
||||
-p "dist/${NAME}_${{ needs.calculate_version.outputs.version }}_linux_${{ matrix.rpm }}.rpm" \
|
||||
--architecture ${{ matrix.rpm }} \
|
||||
dist/sing-box=/usr/bin/sing-box
|
||||
|
||||
@@ -10,9 +10,6 @@ import (
|
||||
type CertificateStore interface {
|
||||
LifecycleService
|
||||
Pool() *x509.CertPool
|
||||
TLSDecryptionEnabled() bool
|
||||
TLSDecryptionCertificate() *x509.Certificate
|
||||
TLSDecryptionPrivateKey() any
|
||||
}
|
||||
|
||||
func RootPoolFromContext(ctx context.Context) *x509.CertPool {
|
||||
|
||||
@@ -2,8 +2,6 @@ package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
@@ -60,8 +58,6 @@ type InboundContext struct {
|
||||
Client string
|
||||
SniffContext any
|
||||
PacketSniffError error
|
||||
HTTPRequest *http.Request
|
||||
ClientHello *tls.ClientHelloInfo
|
||||
|
||||
// cache
|
||||
|
||||
@@ -78,7 +74,6 @@ type InboundContext struct {
|
||||
UDPTimeout time.Duration
|
||||
TLSFragment bool
|
||||
TLSFragmentFallbackDelay time.Duration
|
||||
MITM *option.MITMRouteOptions
|
||||
|
||||
NetworkStrategy *C.NetworkStrategy
|
||||
NetworkType []C.InterfaceType
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
import E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
type StartStage uint8
|
||||
|
||||
@@ -47,9 +45,6 @@ type LifecycleService interface {
|
||||
|
||||
func Start(stage StartStage, services ...Lifecycle) error {
|
||||
for _, service := range services {
|
||||
if service == nil {
|
||||
continue
|
||||
}
|
||||
err := service.Start(stage)
|
||||
if err != nil {
|
||||
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/libbox/platform"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/mitm"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-box/protocol/direct"
|
||||
"github.com/sagernet/sing-box/route"
|
||||
@@ -49,7 +48,6 @@ type Box struct {
|
||||
dnsRouter *dns.Router
|
||||
connection *route.ConnectionManager
|
||||
router *route.Router
|
||||
mitm adapter.MITMEngine //*mitm.Engine
|
||||
services []adapter.LifecycleService
|
||||
done chan struct{}
|
||||
}
|
||||
@@ -145,12 +143,18 @@ func New(options Options) (*Box, error) {
|
||||
}
|
||||
|
||||
var services []adapter.LifecycleService
|
||||
certificateStore, err := certificate.NewStore(ctx, logFactory.NewLogger("certificate"), common.PtrValueOrDefault(options.Certificate))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
certificateOptions := common.PtrValueOrDefault(options.Certificate)
|
||||
if C.IsAndroid || certificateOptions.Store != "" && certificateOptions.Store != C.CertificateStoreSystem ||
|
||||
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)
|
||||
dnsOptions := common.PtrValueOrDefault(options.DNS)
|
||||
@@ -169,7 +173,7 @@ func New(options Options) (*Box, error) {
|
||||
return nil, E.Cause(err, "initialize network manager")
|
||||
}
|
||||
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)
|
||||
router := route.NewRouter(ctx, logFactory, routeOptions, dnsOptions)
|
||||
service.MustRegister[adapter.Router](ctx, router)
|
||||
@@ -177,8 +181,8 @@ func New(options Options) (*Box, error) {
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initialize router")
|
||||
}
|
||||
var timeService *tls.TimeServiceWrapper
|
||||
ntpOptions := common.PtrValueOrDefault(options.NTP)
|
||||
var timeService *tls.TimeServiceWrapper
|
||||
if ntpOptions.Enabled {
|
||||
timeService = new(tls.TimeServiceWrapper)
|
||||
service.MustRegister[ntp.TimeService](ctx, timeService)
|
||||
@@ -341,16 +345,6 @@ func New(options Options) (*Box, error) {
|
||||
timeService.TimeService = ntpService
|
||||
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{
|
||||
network: networkManager,
|
||||
endpoint: endpointManager,
|
||||
@@ -360,7 +354,6 @@ func New(options Options) (*Box, error) {
|
||||
dnsRouter: dnsRouter,
|
||||
connection: connectionManager,
|
||||
router: router,
|
||||
mitm: mitmEngine,
|
||||
createdAt: createdAt,
|
||||
logFactory: logFactory,
|
||||
logger: logFactory.Logger(),
|
||||
@@ -419,11 +412,11 @@ func (s *Box) preStart() error {
|
||||
if err != nil {
|
||||
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 {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
@@ -447,7 +440,7 @@ func (s *Box) start() error {
|
||||
if err != nil {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
@@ -455,7 +448,7 @@ func (s *Box) start() error {
|
||||
if err != nil {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
@@ -474,7 +467,7 @@ func (s *Box) Close() error {
|
||||
close(s.done)
|
||||
}
|
||||
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 {
|
||||
err = E.Append(err, lifecycleService.Close(), func(err error) error {
|
||||
|
||||
Submodule clients/android updated: aefe3c0290...5659088bb3
@@ -27,11 +27,8 @@ func main() {
|
||||
)
|
||||
if flagRunNightly {
|
||||
var version badversion.Version
|
||||
version, err = build_shared.ReadTagVersionRev()
|
||||
version, err = build_shared.ReadTagVersion()
|
||||
if err == nil {
|
||||
if version.PreReleaseIdentifier == "" {
|
||||
version.Patch++
|
||||
}
|
||||
versionStr = version.String()
|
||||
}
|
||||
} 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
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
@@ -12,5 +19,36 @@ var commandTools = &cobra.Command{
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandTools.PersistentFlags().StringVarP(&commandToolsFlagOutbound, "outbound", "o", "", "Use specified tag instead of default outbound")
|
||||
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"
|
||||
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/ntp"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
@@ -40,11 +39,20 @@ func init() {
|
||||
}
|
||||
|
||||
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)
|
||||
if serverAddress.Port == 0 {
|
||||
serverAddress.Port = 123
|
||||
}
|
||||
response, err := ntp.Exchange(context.Background(), N.SystemDialer, serverAddress)
|
||||
response, err := ntp.Exchange(context.Background(), dialer, serverAddress)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package certificate
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -17,8 +16,6 @@ import (
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
"github.com/sagernet/sing/service"
|
||||
|
||||
"software.sslmate.com/src/go-pkcs12"
|
||||
)
|
||||
|
||||
var _ adapter.CertificateStore = (*Store)(nil)
|
||||
@@ -30,9 +27,6 @@ type Store struct {
|
||||
certificatePaths []string
|
||||
certificateDirectoryPaths []string
|
||||
watcher *fswatch.Watcher
|
||||
tlsDecryptionEnabled bool
|
||||
tlsDecryptionPrivateKey any
|
||||
tlsDecryptionCertificate *x509.Certificate
|
||||
}
|
||||
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -202,15 +183,3 @@ func isSameDirSymlink(f fs.DirEntry, dir string) bool {
|
||||
target, err := os.Readlink(filepath.Join(dir, f.Name()))
|
||||
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
|
||||
NewDialer bool
|
||||
LegacyDNSDialer bool
|
||||
DirectOutbound bool
|
||||
}
|
||||
|
||||
// TODO: merge with NewWithOptions
|
||||
@@ -102,13 +103,13 @@ func NewWithOptions(options Options) (N.Dialer, error) {
|
||||
}
|
||||
dnsQueryOptions.Transport = transport
|
||||
resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay)
|
||||
} else if options.NewDialer {
|
||||
return nil, E.New("missing domain resolver for domain server address")
|
||||
} else {
|
||||
transports := dnsTransport.Transports()
|
||||
if len(transports) < 2 {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
"github.com/sagernet/sing/common/task"
|
||||
|
||||
mDNS "github.com/miekg/dns"
|
||||
@@ -47,9 +46,6 @@ func DomainNameQuery(ctx context.Context, metadata *adapter.InboundContext, pack
|
||||
if err != nil {
|
||||
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
|
||||
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.Domain = M.ParseSocksaddr(request.Host).AddrString()
|
||||
metadata.HTTPRequest = request
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ func TLSClientHello(ctx context.Context, metadata *adapter.InboundContext, reade
|
||||
if clientHello != nil {
|
||||
metadata.Protocol = C.ProtocolTLS
|
||||
metadata.Domain = clientHello.ServerName
|
||||
metadata.ClientHello = clientHello
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
|
||||
@@ -8,10 +8,7 @@ import (
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"math/big"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
)
|
||||
|
||||
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 {
|
||||
return
|
||||
}
|
||||
var template *x509.Certificate
|
||||
if serverAddress := M.ParseAddr(serverName); serverAddress.IsValid() {
|
||||
template = &x509.Certificate{
|
||||
SerialNumber: serialNumber,
|
||||
IPAddresses: []net.IP{serverAddress.AsSlice()},
|
||||
NotBefore: timeFunc().Add(time.Hour * -1),
|
||||
NotAfter: expire,
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
BasicConstraintsValid: true,
|
||||
}
|
||||
} 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},
|
||||
}
|
||||
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 {
|
||||
parent = template
|
||||
|
||||
@@ -15,6 +15,8 @@ func TruncateDNSMessage(request *dns.Msg, response *dns.Msg, headroom int) (*buf
|
||||
}
|
||||
responseLen := response.Len()
|
||||
if responseLen > maxLen {
|
||||
copyResponse := *response
|
||||
response = ©Response
|
||||
response.Truncate(maxLen)
|
||||
}
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
defer stream.Close()
|
||||
defer stream.CancelRead(0)
|
||||
err = transport.WriteMessage(stream, 0, message)
|
||||
if err != nil {
|
||||
stream.Close()
|
||||
return nil, err
|
||||
}
|
||||
stream.Close()
|
||||
return transport.ReadMessage(stream)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,22 @@
|
||||
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
|
||||
|
||||
_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
|
||||
|
||||
* Update gVisor to 20250319.0
|
||||
|
||||
@@ -44,10 +44,10 @@ Default padding scheme:
|
||||
|
||||
```
|
||||
stop=8
|
||||
0=34-120
|
||||
0=30-30
|
||||
1=100-400
|
||||
2=400-500,c,500-1000,c,400-500,c,500-1000,c,500-1000,c,400-500
|
||||
3=500-1000
|
||||
2=400-500,c,500-1000,c,500-1000,c,500-1000,c,500-1000
|
||||
3=9-9,500-1000
|
||||
4=500-1000
|
||||
5=500-1000
|
||||
6=500-1000
|
||||
|
||||
@@ -44,10 +44,10 @@ AnyTLS 填充方案行数组。
|
||||
|
||||
```
|
||||
stop=8
|
||||
0=34-120
|
||||
0=30-30
|
||||
1=100-400
|
||||
2=400-500,c,500-1000,c,400-500,c,500-1000,c,500-1000,c,400-500
|
||||
3=500-1000
|
||||
2=400-500,c,500-1000,c,500-1000,c,500-1000,c,500-1000
|
||||
3=9-9,500-1000
|
||||
4=500-1000
|
||||
5=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`.
|
||||
|
||||
!!! 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
|
||||
|
||||
!!! 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.
|
||||
|
||||
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*:
|
||||
|
||||
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**.
|
||||
|
||||
Conflict with `route.default_mark` and `[dialOptions].routing_mark`.
|
||||
|
||||
#### auto_redirect_input_mark
|
||||
|
||||
!!! 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.
|
||||
|
||||
@@ -258,7 +268,7 @@ Connection input mark used by `route[_exclude]_address_set` with `auto_redirect`
|
||||
|
||||
!!! 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.
|
||||
|
||||
@@ -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.
|
||||
Matched traffic will bypass the sing-box routes.
|
||||
|
||||
Conflict with `route.default_mark` and `[dialOptions].routing_mark`.
|
||||
|
||||
=== "Without `auto_redirect` enabled"
|
||||
|
||||
|
||||
@@ -215,6 +215,10 @@ tun 接口的 IPv6 前缀。
|
||||
|
||||
VPN 默认优先于 tun。要使 tun 经过 VPN,启用 `route.override_android_vpn`。
|
||||
|
||||
!!! note "也启用 `auto_redirect`"
|
||||
|
||||
在 Linux 上始终推荐使用 `auto_redirect`,它提供更好的路由, 更高的性能(优于 tproxy), 并避免与 Docker 桥接网络冲突。
|
||||
|
||||
#### iproute2_table_index
|
||||
|
||||
!!! question "自 sing-box 1.10.0 起"
|
||||
@@ -241,19 +245,23 @@ tun 接口的 IPv6 前缀。
|
||||
|
||||
自动配置 iptables/nftables 以重定向连接。
|
||||
|
||||
在 Linux 上始终推荐使用 auto redirect,它提供更好的路由, 更高的性能(优于 tproxy), 并避免与 Docker 桥接网络冲突。
|
||||
|
||||
*在 Android 中*:
|
||||
|
||||
仅转发本地 IPv4 连接。 要通过热点或中继共享您的 VPN 连接,请使用 [VPNHotspot](https://github.com/Mygod/VPNHotspot)。
|
||||
|
||||
*在 Linux 中*:
|
||||
|
||||
带有 `auto_redirect `的 `auto_route` 可以在路由器上按预期工作,**无需干预**。
|
||||
带有 `auto_redirect` 的 `auto_route` 在路由器上**无需干预**即可按预期工作。
|
||||
|
||||
与 `route.default_mark` 和 `[dialOptions].routing_mark` 冲突。
|
||||
|
||||
#### auto_redirect_input_mark
|
||||
|
||||
!!! question "自 sing-box 1.10.0 起"
|
||||
|
||||
`route_address_set` 和 `route_exclude_address_set` 使用的连接输入标记。
|
||||
`auto_redriect` 使用的连接输入标记。
|
||||
|
||||
默认使用 `0x2023`。
|
||||
|
||||
@@ -261,7 +269,7 @@ tun 接口的 IPv6 前缀。
|
||||
|
||||
!!! question "自 sing-box 1.10.0 起"
|
||||
|
||||
`route_address_set` 和 `route_exclude_address_set` 使用的连接输出标记。
|
||||
`auto_redriect` 使用的连接输出标记。
|
||||
|
||||
默认使用 `0x2024`。
|
||||
|
||||
@@ -341,8 +349,6 @@ tun 接口的 IPv6 前缀。
|
||||
|
||||
将指定规则集中的目标 IP CIDR 规则添加到防火墙。
|
||||
不匹配的流量将绕过 sing-box 路由。
|
||||
|
||||
与 `route.default_mark` 和 `[dialOptions].routing_mark` 冲突。
|
||||
|
||||
=== "`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("/cache", cacheRouter(ctx))
|
||||
r.Mount("/dns", dnsRouter(s.dnsRouter))
|
||||
r.Mount("/mitm", mitmRouter(ctx))
|
||||
|
||||
s.setupMetaAPI(r)
|
||||
})
|
||||
|
||||
7
go.mod
7
go.mod
@@ -3,7 +3,7 @@ module github.com/sagernet/sing-box
|
||||
go 1.23.1
|
||||
|
||||
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/cloudflare/circl v1.6.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-shadowsocks2 v0.2.0
|
||||
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/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/wireguard-go v0.0.1-beta.5
|
||||
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854
|
||||
@@ -53,7 +53,6 @@ require (
|
||||
google.golang.org/grpc v1.70.0
|
||||
google.golang.org/protobuf v1.36.5
|
||||
howett.net/plist v1.0.1
|
||||
software.sslmate.com/src/go-pkcs12 v0.4.0
|
||||
)
|
||||
|
||||
//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/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||
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.6/go.mod h1:7rjN6IukwysmdusYsrV51Fgu1uW6vsrdd6ctjnEAln8=
|
||||
github.com/anytls/sing-anytls v0.0.7 h1:0Q5dHNB2sqkFAWZCyK2vjQ/ckI5Iz3V/Frf3k7mBrGc=
|
||||
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/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
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-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-tun v0.6.2-0.20250319123703-35b5747b44ec h1:9/OYGb9qDmUFIhqd3S+3eni62EKRQR1rSmRH18baA/M=
|
||||
github.com/sagernet/sing-tun v0.6.2-0.20250319123703-35b5747b44ec/go.mod h1:fisFCbC4Vfb6HqQNcwPJi2CDK2bf0Xapyz3j3t4cnHE=
|
||||
github.com/sagernet/sing-tun v0.6.2 h1:SoylB/8dA6bRWoUhi4GbFb4WkKL0SMCpmYcvumPndo0=
|
||||
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/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/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.0/go.mod h1:EBxXsWu4OH2ELbQLq32WoBeIubG8KgDrg4/Oaxjs6lI=
|
||||
github.com/sagernet/tailscale v1.80.3-mod.2 h1:hT0CI74q727EuCcgQ+T4pvon8V0aoi4vTAxah7GsNMQ=
|
||||
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/go.mod h1:Uua1TKO/FFuAhLr9rkaVnnrTmmiItzDjv1BUb2+ERwM=
|
||||
github.com/sagernet/wireguard-go v0.0.1-beta.5 h1:aBEsxJUMEONwOZqKPIkuAcv4zJV5p6XlzEN04CF0FXc=
|
||||
|
||||
@@ -10,10 +10,6 @@ import (
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultTimeFormat = "-0700 2006-01-02 15:04:05"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
Context context.Context
|
||||
Options option.LogOptions
|
||||
@@ -51,7 +47,7 @@ func New(options Options) (Factory, error) {
|
||||
DisableColors: logOptions.DisableColor || logFilePath != "",
|
||||
DisableTimestamp: !logOptions.Timestamp && logFilePath != "",
|
||||
FullTimestamp: logOptions.Timestamp,
|
||||
TimestampFormat: DefaultTimeFormat,
|
||||
TimestampFormat: "-0700 2006-01-02 15:04:05",
|
||||
}
|
||||
factory := NewDefaultFactory(
|
||||
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"`
|
||||
CertificatePath badoption.Listable[string] `json:"certificate_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
|
||||
|
||||
@@ -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"
|
||||
"context"
|
||||
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/json"
|
||||
)
|
||||
|
||||
@@ -12,14 +13,13 @@ type _Options struct {
|
||||
Schema string `json:"$schema,omitempty"`
|
||||
Log *LogOptions `json:"log,omitempty"`
|
||||
DNS *DNSOptions `json:"dns,omitempty"`
|
||||
NTP *NTPOptions `json:"ntp,omitempty"`
|
||||
Certificate *CertificateOptions `json:"certificate,omitempty"`
|
||||
Endpoints []Endpoint `json:"endpoints,omitempty"`
|
||||
Inbounds []Inbound `json:"inbounds,omitempty"`
|
||||
Outbounds []Outbound `json:"outbounds,omitempty"`
|
||||
Route *RouteOptions `json:"route,omitempty"`
|
||||
Experimental *ExperimentalOptions `json:"experimental,omitempty"`
|
||||
NTP *NTPOptions `json:"ntp,omitempty"`
|
||||
Certificate *CertificateOptions `json:"certificate,omitempty"`
|
||||
MITM *MITMOptions `json:"mitm,omitempty"`
|
||||
}
|
||||
|
||||
type Options _Options
|
||||
@@ -32,7 +32,7 @@ func (o *Options) UnmarshalJSONContext(ctx context.Context, content []byte) erro
|
||||
return err
|
||||
}
|
||||
o.RawMessage = content
|
||||
return nil
|
||||
return checkOptions(o)
|
||||
}
|
||||
|
||||
type LogOptions struct {
|
||||
@@ -44,3 +44,52 @@ type LogOptions 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"`
|
||||
TLSFragmentFallbackDelay badoption.Duration `json:"tls_fragment_fallback_delay,omitempty"`
|
||||
|
||||
MITM *MITMRouteOptions `json:"mitm,omitempty"`
|
||||
}
|
||||
|
||||
type RouteOptionsActionOptions RawRouteOptionsActionOptions
|
||||
|
||||
@@ -48,7 +48,12 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
|
||||
if options.Detour != "" {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -2,8 +2,10 @@ package tailscale
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"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())
|
||||
},
|
||||
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{
|
||||
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)
|
||||
}
|
||||
|
||||
func (t *Endpoint) Server() *tsnet.Server {
|
||||
return t.server
|
||||
}
|
||||
|
||||
func addressFromAddr(destination netip.Addr) tcpip.Address {
|
||||
if destination.Is6() {
|
||||
return tcpip.AddrFrom16(destination.As16())
|
||||
|
||||
@@ -245,7 +245,7 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
|
||||
if err != nil {
|
||||
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
|
||||
err = networkManager.RegisterAutoRedirectOutputMark(inbound.tunOptions.AutoRedirectOutputMark)
|
||||
if err != nil {
|
||||
|
||||
@@ -24,31 +24,23 @@ import (
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/common/x/list"
|
||||
"github.com/sagernet/sing/service"
|
||||
)
|
||||
|
||||
var _ adapter.ConnectionManager = (*ConnectionManager)(nil)
|
||||
|
||||
type ConnectionManager struct {
|
||||
ctx context.Context
|
||||
logger logger.ContextLogger
|
||||
mitm adapter.MITMEngine
|
||||
access sync.Mutex
|
||||
connections list.List[io.Closer]
|
||||
}
|
||||
|
||||
func NewConnectionManager(ctx context.Context, logger logger.ContextLogger) *ConnectionManager {
|
||||
func NewConnectionManager(logger logger.ContextLogger) *ConnectionManager {
|
||||
return &ConnectionManager{
|
||||
ctx: ctx,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ConnectionManager) Start(stage adapter.StartStage) error {
|
||||
switch stage {
|
||||
case adapter.StartStateInitialize:
|
||||
m.mitm = service.FromContext[adapter.MITMEngine](m.ctx)
|
||||
}
|
||||
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) {
|
||||
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)
|
||||
var (
|
||||
remoteConn net.Conn
|
||||
|
||||
@@ -458,9 +458,6 @@ match:
|
||||
metadata.TLSFragment = true
|
||||
metadata.TLSFragmentFallbackDelay = routeOptions.TLSFragmentFallbackDelay
|
||||
}
|
||||
if routeOptions.MITM != nil && routeOptions.MITM.Enabled {
|
||||
metadata.MITM = routeOptions.MITM
|
||||
}
|
||||
}
|
||||
switch action := currentRule.Action().(type) {
|
||||
case *rule.RuleActionSniff:
|
||||
|
||||
@@ -40,7 +40,6 @@ func NewRuleAction(ctx context.Context, logger logger.ContextLogger, action opti
|
||||
UDPConnect: action.RouteOptions.UDPConnect,
|
||||
TLSFragment: action.RouteOptions.TLSFragment,
|
||||
TLSFragmentFallbackDelay: time.Duration(action.RouteOptions.TLSFragmentFallbackDelay),
|
||||
MITM: action.RouteOptions.MITM,
|
||||
},
|
||||
}, nil
|
||||
case C.RuleActionTypeRouteOptions:
|
||||
@@ -54,7 +53,6 @@ func NewRuleAction(ctx context.Context, logger logger.ContextLogger, action opti
|
||||
UDPTimeout: time.Duration(action.RouteOptionsOptions.UDPTimeout),
|
||||
TLSFragment: action.RouteOptionsOptions.TLSFragment,
|
||||
TLSFragmentFallbackDelay: time.Duration(action.RouteOptionsOptions.TLSFragmentFallbackDelay),
|
||||
MITM: action.RouteOptionsOptions.MITM,
|
||||
}, nil
|
||||
case C.RuleActionTypeDirect:
|
||||
directDialer, err := dialer.New(ctx, option.DialerOptions(action.DirectOptions), false)
|
||||
@@ -154,7 +152,15 @@ func (r *RuleActionRoute) Type() string {
|
||||
func (r *RuleActionRoute) String() string {
|
||||
var descriptions []string
|
||||
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, ","), ")")
|
||||
}
|
||||
|
||||
@@ -170,14 +176,13 @@ type RuleActionRouteOptions struct {
|
||||
UDPTimeout time.Duration
|
||||
TLSFragment bool
|
||||
TLSFragmentFallbackDelay time.Duration
|
||||
MITM *option.MITMRouteOptions
|
||||
}
|
||||
|
||||
func (r *RuleActionRouteOptions) Type() string {
|
||||
return C.RuleActionTypeRouteOptions
|
||||
}
|
||||
|
||||
func (r *RuleActionRouteOptions) Descriptions() []string {
|
||||
func (r *RuleActionRouteOptions) String() string {
|
||||
var descriptions []string
|
||||
if r.OverrideAddress.IsValid() {
|
||||
descriptions = append(descriptions, F.ToString("override-address=", r.OverrideAddress.AddrString()))
|
||||
@@ -204,22 +209,9 @@ func (r *RuleActionRouteOptions) Descriptions() []string {
|
||||
descriptions = append(descriptions, "udp-connect")
|
||||
}
|
||||
if r.UDPTimeout > 0 {
|
||||
descriptions = append(descriptions, F.ToString("udp-timeout=", r.UDPTimeout))
|
||||
descriptions = append(descriptions, "udp-timeout")
|
||||
}
|
||||
if r.TLSFragment {
|
||||
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(), ","), ")")
|
||||
return F.ToString("route-options(", strings.Join(descriptions, ","), ")")
|
||||
}
|
||||
|
||||
type RuleActionDNSRoute struct {
|
||||
|
||||
Reference in New Issue
Block a user