diff --git a/adapter/outbound/mieru.go b/adapter/outbound/mieru.go
new file mode 100644
index 0000000000..0d32ca4126
--- /dev/null
+++ b/adapter/outbound/mieru.go
@@ -0,0 +1,268 @@
+package outbound
+
+import (
+	"context"
+	"fmt"
+	"net"
+	"runtime"
+	"strconv"
+	"sync"
+
+	mieruclient "github.com/enfein/mieru/v3/apis/client"
+	mierumodel "github.com/enfein/mieru/v3/apis/model"
+	mierupb "github.com/enfein/mieru/v3/pkg/appctl/appctlpb"
+	"github.com/metacubex/mihomo/component/dialer"
+	"github.com/metacubex/mihomo/component/proxydialer"
+	C "github.com/metacubex/mihomo/constant"
+	"google.golang.org/protobuf/proto"
+)
+
+type Mieru struct {
+	*Base
+	option *MieruOption
+	client mieruclient.Client
+	mu     sync.Mutex
+}
+
+type MieruOption struct {
+	BasicOption
+	Name      string `proxy:"name"`
+	Server    string `proxy:"server"`
+	Port      int    `proxy:"port,omitempty"`
+	PortRange string `proxy:"port-range,omitempty"`
+	Transport string `proxy:"transport"`
+	UserName  string `proxy:"username"`
+	Password  string `proxy:"password"`
+}
+
+// DialContext implements C.ProxyAdapter
+func (m *Mieru) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
+	if err := m.ensureClientIsRunning(opts...); err != nil {
+		return nil, err
+	}
+	addr := metadataToMieruNetAddrSpec(metadata)
+	c, err := m.client.DialContext(ctx, addr)
+	if err != nil {
+		return nil, fmt.Errorf("dial to %s failed: %w", addr, err)
+	}
+	return NewConn(c, m), nil
+}
+
+// ProxyInfo implements C.ProxyAdapter
+func (m *Mieru) ProxyInfo() C.ProxyInfo {
+	info := m.Base.ProxyInfo()
+	info.DialerProxy = m.option.DialerProxy
+	return info
+}
+
+func (m *Mieru) ensureClientIsRunning(opts ...dialer.Option) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	if m.client.IsRunning() {
+		return nil
+	}
+
+	// Create a dialer and add it to the client config, before starting the client.
+	var dialer C.Dialer = dialer.NewDialer(m.Base.DialOptions(opts...)...)
+	var err error
+	if len(m.option.DialerProxy) > 0 {
+		dialer, err = proxydialer.NewByName(m.option.DialerProxy, dialer)
+		if err != nil {
+			return err
+		}
+	}
+	config, err := m.client.Load()
+	if err != nil {
+		return err
+	}
+	config.Dialer = dialer
+	if err := m.client.Store(config); err != nil {
+		return err
+	}
+
+	if err := m.client.Start(); err != nil {
+		return fmt.Errorf("failed to start mieru client: %w", err)
+	}
+	return nil
+}
+
+func NewMieru(option MieruOption) (*Mieru, error) {
+	config, err := buildMieruClientConfig(option)
+	if err != nil {
+		return nil, fmt.Errorf("failed to build mieru client config: %w", err)
+	}
+	c := mieruclient.NewClient()
+	if err := c.Store(config); err != nil {
+		return nil, fmt.Errorf("failed to store mieru client config: %w", err)
+	}
+	// Client is started lazily on the first use.
+
+	var addr string
+	if option.Port != 0 {
+		addr = net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
+	} else {
+		beginPort, _, _ := beginAndEndPortFromPortRange(option.PortRange)
+		addr = net.JoinHostPort(option.Server, strconv.Itoa(beginPort))
+	}
+	outbound := &Mieru{
+		Base: &Base{
+			name:   option.Name,
+			addr:   addr,
+			iface:  option.Interface,
+			tp:     C.Mieru,
+			udp:    false,
+			xudp:   false,
+			rmark:  option.RoutingMark,
+			prefer: C.NewDNSPrefer(option.IPVersion),
+		},
+		option: &option,
+		client: c,
+	}
+	runtime.SetFinalizer(outbound, closeMieru)
+	return outbound, nil
+}
+
+func closeMieru(m *Mieru) {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+	if m.client != nil && m.client.IsRunning() {
+		m.client.Stop()
+	}
+}
+
+func metadataToMieruNetAddrSpec(metadata *C.Metadata) mierumodel.NetAddrSpec {
+	if metadata.Host != "" {
+		return mierumodel.NetAddrSpec{
+			AddrSpec: mierumodel.AddrSpec{
+				FQDN: metadata.Host,
+				Port: int(metadata.DstPort),
+			},
+			Net: "tcp",
+		}
+	} else {
+		return mierumodel.NetAddrSpec{
+			AddrSpec: mierumodel.AddrSpec{
+				IP:   metadata.DstIP.AsSlice(),
+				Port: int(metadata.DstPort),
+			},
+			Net: "tcp",
+		}
+	}
+}
+
+func buildMieruClientConfig(option MieruOption) (*mieruclient.ClientConfig, error) {
+	if err := validateMieruOption(option); err != nil {
+		return nil, fmt.Errorf("failed to validate mieru option: %w", err)
+	}
+
+	transportProtocol := mierupb.TransportProtocol_TCP.Enum()
+	var server *mierupb.ServerEndpoint
+	if net.ParseIP(option.Server) != nil {
+		// server is an IP address
+		if option.PortRange != "" {
+			server = &mierupb.ServerEndpoint{
+				IpAddress: proto.String(option.Server),
+				PortBindings: []*mierupb.PortBinding{
+					{
+						PortRange: proto.String(option.PortRange),
+						Protocol:  transportProtocol,
+					},
+				},
+			}
+		} else {
+			server = &mierupb.ServerEndpoint{
+				IpAddress: proto.String(option.Server),
+				PortBindings: []*mierupb.PortBinding{
+					{
+						Port:     proto.Int32(int32(option.Port)),
+						Protocol: transportProtocol,
+					},
+				},
+			}
+		}
+	} else {
+		// server is a domain name
+		if option.PortRange != "" {
+			server = &mierupb.ServerEndpoint{
+				DomainName: proto.String(option.Server),
+				PortBindings: []*mierupb.PortBinding{
+					{
+						PortRange: proto.String(option.PortRange),
+						Protocol:  transportProtocol,
+					},
+				},
+			}
+		} else {
+			server = &mierupb.ServerEndpoint{
+				DomainName: proto.String(option.Server),
+				PortBindings: []*mierupb.PortBinding{
+					{
+						Port:     proto.Int32(int32(option.Port)),
+						Protocol: transportProtocol,
+					},
+				},
+			}
+		}
+	}
+	return &mieruclient.ClientConfig{
+		Profile: &mierupb.ClientProfile{
+			ProfileName: proto.String(option.Name),
+			User: &mierupb.User{
+				Name:     proto.String(option.UserName),
+				Password: proto.String(option.Password),
+			},
+			Servers: []*mierupb.ServerEndpoint{server},
+		},
+	}, nil
+}
+
+func validateMieruOption(option MieruOption) error {
+	if option.Name == "" {
+		return fmt.Errorf("name is empty")
+	}
+	if option.Server == "" {
+		return fmt.Errorf("server is empty")
+	}
+	if option.Port == 0 && option.PortRange == "" {
+		return fmt.Errorf("either port or port-range must be set")
+	}
+	if option.Port != 0 && option.PortRange != "" {
+		return fmt.Errorf("port and port-range cannot be set at the same time")
+	}
+	if option.Port != 0 && (option.Port < 1 || option.Port > 65535) {
+		return fmt.Errorf("port must be between 1 and 65535")
+	}
+	if option.PortRange != "" {
+		begin, end, err := beginAndEndPortFromPortRange(option.PortRange)
+		if err != nil {
+			return fmt.Errorf("invalid port-range format")
+		}
+		if begin < 1 || begin > 65535 {
+			return fmt.Errorf("begin port must be between 1 and 65535")
+		}
+		if end < 1 || end > 65535 {
+			return fmt.Errorf("end port must be between 1 and 65535")
+		}
+		if begin > end {
+			return fmt.Errorf("begin port must be less than or equal to end port")
+		}
+	}
+
+	if option.Transport != "TCP" {
+		return fmt.Errorf("transport must be TCP")
+	}
+	if option.UserName == "" {
+		return fmt.Errorf("username is empty")
+	}
+	if option.Password == "" {
+		return fmt.Errorf("password is empty")
+	}
+	return nil
+}
+
+func beginAndEndPortFromPortRange(portRange string) (int, int, error) {
+	var begin, end int
+	_, err := fmt.Sscanf(portRange, "%d-%d", &begin, &end)
+	return begin, end, err
+}
diff --git a/adapter/outbound/mieru_test.go b/adapter/outbound/mieru_test.go
new file mode 100644
index 0000000000..086b791044
--- /dev/null
+++ b/adapter/outbound/mieru_test.go
@@ -0,0 +1,92 @@
+package outbound
+
+import "testing"
+
+func TestNewMieru(t *testing.T) {
+	testCases := []struct {
+		option       MieruOption
+		wantBaseAddr string
+	}{
+		{
+			option: MieruOption{
+				Name:      "test",
+				Server:    "1.2.3.4",
+				Port:      10000,
+				Transport: "TCP",
+				UserName:  "test",
+				Password:  "test",
+			},
+			wantBaseAddr: "1.2.3.4:10000",
+		},
+		{
+			option: MieruOption{
+				Name:      "test",
+				Server:    "2001:db8::1",
+				PortRange: "10001-10002",
+				Transport: "TCP",
+				UserName:  "test",
+				Password:  "test",
+			},
+			wantBaseAddr: "[2001:db8::1]:10001",
+		},
+		{
+			option: MieruOption{
+				Name:      "test",
+				Server:    "example.com",
+				Port:      10003,
+				Transport: "TCP",
+				UserName:  "test",
+				Password:  "test",
+			},
+			wantBaseAddr: "example.com:10003",
+		},
+	}
+
+	for _, testCase := range testCases {
+		mieru, err := NewMieru(testCase.option)
+		if err != nil {
+			t.Error(err)
+		}
+		if mieru.addr != testCase.wantBaseAddr {
+			t.Errorf("got addr %q, want %q", mieru.addr, testCase.wantBaseAddr)
+		}
+	}
+}
+
+func TestBeginAndEndPortFromPortRange(t *testing.T) {
+	testCases := []struct {
+		input  string
+		begin  int
+		end    int
+		hasErr bool
+	}{
+		{"1-10", 1, 10, false},
+		{"1000-2000", 1000, 2000, false},
+		{"65535-65535", 65535, 65535, false},
+		{"1", 0, 0, true},
+		{"1-", 0, 0, true},
+		{"-10", 0, 0, true},
+		{"a-b", 0, 0, true},
+		{"1-b", 0, 0, true},
+		{"a-10", 0, 0, true},
+	}
+
+	for _, testCase := range testCases {
+		begin, end, err := beginAndEndPortFromPortRange(testCase.input)
+		if testCase.hasErr {
+			if err == nil {
+				t.Errorf("beginAndEndPortFromPortRange(%s) should return an error", testCase.input)
+			}
+		} else {
+			if err != nil {
+				t.Errorf("beginAndEndPortFromPortRange(%s) should not return an error, but got %v", testCase.input, err)
+			}
+			if begin != testCase.begin {
+				t.Errorf("beginAndEndPortFromPortRange(%s) begin port mismatch, got %d, want %d", testCase.input, begin, testCase.begin)
+			}
+			if end != testCase.end {
+				t.Errorf("beginAndEndPortFromPortRange(%s) end port mismatch, got %d, want %d", testCase.input, end, testCase.end)
+			}
+		}
+	}
+}
diff --git a/adapter/parser.go b/adapter/parser.go
index c64ee13a48..ce4e91d5e9 100644
--- a/adapter/parser.go
+++ b/adapter/parser.go
@@ -141,6 +141,13 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) {
 			break
 		}
 		proxy, err = outbound.NewSsh(*sshOption)
+	case "mieru":
+		mieruOption := &outbound.MieruOption{}
+		err = decoder.Decode(mapping, mieruOption)
+		if err != nil {
+			break
+		}
+		proxy, err = outbound.NewMieru(*mieruOption)
 	default:
 		return nil, fmt.Errorf("unsupport proxy type: %s", proxyType)
 	}
diff --git a/constant/adapters.go b/constant/adapters.go
index 664054d750..420a797fa6 100644
--- a/constant/adapters.go
+++ b/constant/adapters.go
@@ -42,6 +42,7 @@ const (
 	WireGuard
 	Tuic
 	Ssh
+	Mieru
 )
 
 const (
@@ -226,7 +227,8 @@ func (at AdapterType) String() string {
 		return "Tuic"
 	case Ssh:
 		return "Ssh"
-
+	case Mieru:
+		return "Mieru"
 	case Relay:
 		return "Relay"
 	case Selector:
diff --git a/docs/config.yaml b/docs/config.yaml
index 5e83eea376..23acb78fd9 100644
--- a/docs/config.yaml
+++ b/docs/config.yaml
@@ -846,6 +846,16 @@ proxies: # socks5
     password: password
     privateKey: path
 
+  # mieru
+  - name: mieru
+    type: mieru
+    server: 1.2.3.4
+    port: 2999
+    # port-range: 2090-2099 #(不可同时填写 port 和 port-range)
+    transport: TCP # 只支持 TCP
+    username: user
+    password: password
+
 # dns 出站会将请求劫持到内部 dns 模块,所有请求均在内部处理
   - name: "dns-out"
     type: dns
@@ -1202,4 +1212,4 @@ listeners:
 #  authentication-timeout: 1000
 #  alpn:
 #    - h3
-#  max-udp-relay-packet-size: 1500
\ No newline at end of file
+#  max-udp-relay-packet-size: 1500
diff --git a/go.mod b/go.mod
index 905b1597fe..1ae7c26da9 100644
--- a/go.mod
+++ b/go.mod
@@ -7,6 +7,7 @@ require (
 	github.com/bahlo/generic-list-go v0.2.0
 	github.com/coreos/go-iptables v0.8.0
 	github.com/dlclark/regexp2 v1.11.4
+	github.com/enfein/mieru/v3 v3.8.3
 	github.com/go-chi/chi/v5 v5.1.0
 	github.com/go-chi/render v1.0.3
 	github.com/gobwas/ws v1.4.0
@@ -114,6 +115,8 @@ require (
 	golang.org/x/text v0.20.0 // indirect
 	golang.org/x/time v0.7.0 // indirect
 	golang.org/x/tools v0.24.0 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20240610135401-a8a62080eff3 // indirect
+	google.golang.org/grpc v1.64.1 // indirect
 )
 
 replace github.com/sagernet/sing => github.com/metacubex/sing v0.0.0-20241121030428-33b6ebc52000
diff --git a/go.sum b/go.sum
index 7845d2949a..7aac462805 100644
--- a/go.sum
+++ b/go.sum
@@ -27,6 +27,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
 github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
+github.com/enfein/mieru/v3 v3.8.3 h1:s4K0hMFDg6LHltokR8/nBTVCq15XnnxPsvc1LrHwpoo=
+github.com/enfein/mieru/v3 v3.8.3/go.mod h1:YtU00qjAEt54mCBQu4WZPCey6cBdB1BUtXjvrHLEUNQ=
 github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 h1:/5RkVc9Rc81XmMyVqawCiDyrBHZbLAZgTTCqou4mwj8=
 github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I=
 github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g=
@@ -59,7 +61,7 @@ github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakr
 github.com/gofrs/uuid/v5 v5.3.0 h1:m0mUMr+oVYUdxpMLgSYCZiXe7PuVPnI94+OMeVBNedk=
 github.com/gofrs/uuid/v5 v5.3.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
 github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
 github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
 github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
 github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
@@ -274,6 +276,10 @@ golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
 golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240610135401-a8a62080eff3 h1:9Xyg6I9IWQZhRVfCWjKK+l6kI0jHcPesVlMnT//aHNo=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240610135401-a8a62080eff3/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0=
+google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA=
+google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0=
 google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
 google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=