diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ab046bd17f..dd76733df2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -58,7 +58,7 @@ jobs: # 48042aa09c2f878c4faa576948b07fe625c4707a: "syscall: remove Windows 7 console handle workaround" # a17d959debdb04cd550016a3501dd09d50cd62e7: "runtime: always use LoadLibraryEx to load system libraries" - name: Revert Golang1.25 commit for Windows7/8 - if: ${{ matrix.jobs.goos == 'windows' && matrix.jobs.goversion == '1.25' }} + if: ${{ runner.os == 'Windows' && matrix.go-version == '1.25' }} run: | alias curl='curl -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}"' cd $(go env GOROOT) diff --git a/adapter/adapter.go b/adapter/adapter.go index d1f37863c6..4f127bcf6a 100644 --- a/adapter/adapter.go +++ b/adapter/adapter.go @@ -2,7 +2,6 @@ package adapter import ( "context" - "crypto/tls" "encoding/json" "fmt" "net" @@ -236,6 +235,11 @@ func (p *Proxy) URLTest(ctx context.Context, url string, expectedStatus utils.In } req = req.WithContext(ctx) + tlsConfig, err := ca.GetTLSConfig(ca.Option{}) + if err != nil { + return + } + transport := &http.Transport{ DialContext: func(context.Context, string, string) (net.Conn, error) { return instance, nil @@ -245,7 +249,7 @@ func (p *Proxy) URLTest(ctx context.Context, url string, expectedStatus utils.In IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, - TLSClientConfig: ca.GetGlobalTLSConfig(&tls.Config{}), + TLSClientConfig: tlsConfig, } client := http.Client{ diff --git a/adapter/outbound/anytls.go b/adapter/outbound/anytls.go index 02541f23d9..78b1e40cb7 100644 --- a/adapter/outbound/anytls.go +++ b/adapter/outbound/anytls.go @@ -36,6 +36,8 @@ type AnyTLSOption struct { ClientFingerprint string `proxy:"client-fingerprint,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` Fingerprint string `proxy:"fingerprint,omitempty"` + Certificate string `proxy:"certificate,omitempty"` + PrivateKey string `proxy:"private-key,omitempty"` UDP bool `proxy:"udp,omitempty"` IdleSessionCheckInterval int `proxy:"idle-session-check-interval,omitempty"` IdleSessionTimeout int `proxy:"idle-session-timeout,omitempty"` @@ -120,6 +122,8 @@ func NewAnyTLS(option AnyTLSOption) (*AnyTLS, error) { SkipCertVerify: option.SkipCertVerify, NextProtos: option.ALPN, FingerPrint: option.Fingerprint, + Certificate: option.Certificate, + PrivateKey: option.PrivateKey, ClientFingerprint: option.ClientFingerprint, ECH: echConfig, } diff --git a/adapter/outbound/http.go b/adapter/outbound/http.go index f02308b920..7b898e2eb6 100644 --- a/adapter/outbound/http.go +++ b/adapter/outbound/http.go @@ -37,6 +37,8 @@ type HttpOption struct { SNI string `proxy:"sni,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` Fingerprint string `proxy:"fingerprint,omitempty"` + Certificate string `proxy:"certificate,omitempty"` + PrivateKey string `proxy:"private-key,omitempty"` Headers map[string]string `proxy:"headers,omitempty"` } @@ -167,10 +169,15 @@ func NewHttp(option HttpOption) (*Http, error) { sni = option.SNI } var err error - tlsConfig, err = ca.GetSpecifiedFingerprintTLSConfig(&tls.Config{ - InsecureSkipVerify: option.SkipCertVerify, - ServerName: sni, - }, option.Fingerprint) + tlsConfig, err = ca.GetTLSConfig(ca.Option{ + TLSConfig: &tls.Config{ + InsecureSkipVerify: option.SkipCertVerify, + ServerName: sni, + }, + Fingerprint: option.Fingerprint, + Certificate: option.Certificate, + PrivateKey: option.PrivateKey, + }) if err != nil { return nil, err } diff --git a/adapter/outbound/hysteria.go b/adapter/outbound/hysteria.go index 383184595d..9ba118f39b 100644 --- a/adapter/outbound/hysteria.go +++ b/adapter/outbound/hysteria.go @@ -125,9 +125,9 @@ type HysteriaOption struct { ECHOpts ECHOptions `proxy:"ech-opts,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` Fingerprint string `proxy:"fingerprint,omitempty"` + Certificate string `proxy:"certificate,omitempty"` + PrivateKey string `proxy:"private-key,omitempty"` ALPN []string `proxy:"alpn,omitempty"` - CustomCA string `proxy:"ca,omitempty"` - CustomCAString string `proxy:"ca-str,omitempty"` ReceiveWindowConn int `proxy:"recv-window-conn,omitempty"` ReceiveWindow int `proxy:"recv-window,omitempty"` DisableMTUDiscovery bool `proxy:"disable-mtu-discovery,omitempty"` @@ -160,14 +160,16 @@ func NewHysteria(option HysteriaOption) (*Hysteria, error) { serverName = option.SNI } - tlsConfig := &tls.Config{ - ServerName: serverName, - InsecureSkipVerify: option.SkipCertVerify, - MinVersion: tls.VersionTLS13, - } - - var err error - tlsConfig, err = ca.GetTLSConfig(tlsConfig, option.Fingerprint, option.CustomCA, option.CustomCAString) + tlsConfig, err := ca.GetTLSConfig(ca.Option{ + TLSConfig: &tls.Config{ + ServerName: serverName, + InsecureSkipVerify: option.SkipCertVerify, + MinVersion: tls.VersionTLS13, + }, + Fingerprint: option.Fingerprint, + Certificate: option.Certificate, + PrivateKey: option.PrivateKey, + }) if err != nil { return nil, err } diff --git a/adapter/outbound/hysteria2.go b/adapter/outbound/hysteria2.go index e7b9f0b5b0..bc203366f0 100644 --- a/adapter/outbound/hysteria2.go +++ b/adapter/outbound/hysteria2.go @@ -55,9 +55,9 @@ type Hysteria2Option struct { ECHOpts ECHOptions `proxy:"ech-opts,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` Fingerprint string `proxy:"fingerprint,omitempty"` + Certificate string `proxy:"certificate,omitempty"` + PrivateKey string `proxy:"private-key,omitempty"` ALPN []string `proxy:"alpn,omitempty"` - CustomCA string `proxy:"ca,omitempty"` - CustomCAString string `proxy:"ca-str,omitempty"` CWND int `proxy:"cwnd,omitempty"` UdpMTU int `proxy:"udp-mtu,omitempty"` @@ -141,14 +141,16 @@ func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) { serverName = option.SNI } - tlsConfig := &tls.Config{ - ServerName: serverName, - InsecureSkipVerify: option.SkipCertVerify, - MinVersion: tls.VersionTLS13, - } - - var err error - tlsConfig, err = ca.GetTLSConfig(tlsConfig, option.Fingerprint, option.CustomCA, option.CustomCAString) + tlsConfig, err := ca.GetTLSConfig(ca.Option{ + TLSConfig: &tls.Config{ + ServerName: serverName, + InsecureSkipVerify: option.SkipCertVerify, + MinVersion: tls.VersionTLS13, + }, + Fingerprint: option.Fingerprint, + Certificate: option.Certificate, + PrivateKey: option.PrivateKey, + }) if err != nil { return nil, err } diff --git a/adapter/outbound/mieru.go b/adapter/outbound/mieru.go index 8ef9cfd758..bfdf0e519f 100644 --- a/adapter/outbound/mieru.go +++ b/adapter/outbound/mieru.go @@ -5,6 +5,7 @@ import ( "fmt" "net" "strconv" + "strings" "sync" CN "github.com/metacubex/mihomo/common/net" @@ -30,8 +31,8 @@ type MieruOption struct { BasicOption Name string `proxy:"name"` Server string `proxy:"server"` - Port int `proxy:"port,omitempty"` - PortRange string `proxy:"port-range,omitempty"` + Port string `proxy:"port,omitempty"` + PortRange string `proxy:"port-range,omitempty"` // deprecated Transport string `proxy:"transport"` UDP bool `proxy:"udp,omitempty"` UserName string `proxy:"username"` @@ -123,13 +124,19 @@ func NewMieru(option MieruOption) (*Mieru, error) { } // Client is started lazily on the first use. + // Use the first port to construct the address. var addr string - if option.Port != 0 { - addr = net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) + var portStr string + if option.Port != "" { + portStr = option.Port } else { - beginPort, _, _ := beginAndEndPortFromPortRange(option.PortRange) - addr = net.JoinHostPort(option.Server, strconv.Itoa(beginPort)) + portStr = option.PortRange } + firstPort, err := getFirstPort(portStr) + if err != nil { + return nil, fmt.Errorf("failed to get first port from port string %q: %w", portStr, err) + } + addr = net.JoinHostPort(option.Server, strconv.Itoa(firstPort)) outbound := &Mieru{ Base: &Base{ name: option.Name, @@ -183,54 +190,62 @@ func buildMieruClientConfig(option MieruOption) (*mieruclient.ClientConfig, erro } 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), + + portBindings := make([]*mierupb.PortBinding, 0) + if option.Port != "" { + parts := strings.Split(option.Port, ",") + for _, part := range parts { + part = strings.TrimSpace(part) + if strings.Contains(part, "-") { + _, _, err := beginAndEndPortFromPortRange(part) + if err == nil { + portBindings = append(portBindings, &mierupb.PortBinding{ + PortRange: proto.String(part), Protocol: transportProtocol, - }, - }, + }) + } else { + return nil, err + } + } else { + p, err := strconv.Atoi(part) + if err != nil { + return nil, fmt.Errorf("invalid port value: %s", part) + } + portBindings = append(portBindings, &mierupb.PortBinding{ + Port: proto.Int32(int32(p)), + Protocol: transportProtocol, + }) } - } else { - server = &mierupb.ServerEndpoint{ - IpAddress: proto.String(option.Server), - PortBindings: []*mierupb.PortBinding{ - { - Port: proto.Int32(int32(option.Port)), - Protocol: transportProtocol, - }, - }, + } + } + if option.PortRange != "" { + parts := strings.Split(option.PortRange, ",") + for _, part := range parts { + part = strings.TrimSpace(part) + if _, _, err := beginAndEndPortFromPortRange(part); err == nil { + portBindings = append(portBindings, &mierupb.PortBinding{ + PortRange: proto.String(part), + Protocol: transportProtocol, + }) } } + } + + var server *mierupb.ServerEndpoint + if net.ParseIP(option.Server) != nil { + // server is an IP address + server = &mierupb.ServerEndpoint{ + IpAddress: proto.String(option.Server), + PortBindings: portBindings, + } } 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, - }, - }, - } + server = &mierupb.ServerEndpoint{ + DomainName: proto.String(option.Server), + PortBindings: portBindings, } } + config := &mieruclient.ClientConfig{ Profile: &mierupb.ClientProfile{ ProfileName: proto.String(option.Name), @@ -259,31 +274,9 @@ func validateMieruOption(option MieruOption) error { 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.Port == "" && option.PortRange == "" { + return fmt.Errorf("port must be set") } - 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") } @@ -306,8 +299,36 @@ func validateMieruOption(option MieruOption) error { return nil } +func getFirstPort(portStr string) (int, error) { + if portStr == "" { + return 0, fmt.Errorf("port string is empty") + } + parts := strings.Split(portStr, ",") + firstPart := parts[0] + + if strings.Contains(firstPart, "-") { + begin, _, err := beginAndEndPortFromPortRange(firstPart) + if err != nil { + return 0, err + } + return begin, nil + } + + port, err := strconv.Atoi(firstPart) + if err != nil { + return 0, fmt.Errorf("invalid port format: %s", firstPart) + } + return port, nil +} + func beginAndEndPortFromPortRange(portRange string) (int, int, error) { var begin, end int _, err := fmt.Sscanf(portRange, "%d-%d", &begin, &end) + if err != nil { + return 0, 0, fmt.Errorf("invalid port range format: %w", err) + } + if begin > end { + return 0, 0, fmt.Errorf("begin port is greater than end port: %s", portRange) + } return begin, end, err } diff --git a/adapter/outbound/mieru_test.go b/adapter/outbound/mieru_test.go index 086b791044..2b7976e4c7 100644 --- a/adapter/outbound/mieru_test.go +++ b/adapter/outbound/mieru_test.go @@ -1,22 +1,51 @@ package outbound -import "testing" +import ( + "reflect" + "testing" + + mieruclient "github.com/enfein/mieru/v3/apis/client" + mierupb "github.com/enfein/mieru/v3/pkg/appctl/appctlpb" + "google.golang.org/protobuf/proto" +) func TestNewMieru(t *testing.T) { + transportProtocol := mierupb.TransportProtocol_TCP.Enum() testCases := []struct { option MieruOption wantBaseAddr string + wantConfig *mieruclient.ClientConfig }{ { option: MieruOption{ Name: "test", Server: "1.2.3.4", - Port: 10000, + Port: "10000", Transport: "TCP", UserName: "test", Password: "test", }, wantBaseAddr: "1.2.3.4:10000", + wantConfig: &mieruclient.ClientConfig{ + Profile: &mierupb.ClientProfile{ + ProfileName: proto.String("test"), + User: &mierupb.User{ + Name: proto.String("test"), + Password: proto.String("test"), + }, + Servers: []*mierupb.ServerEndpoint{ + { + IpAddress: proto.String("1.2.3.4"), + PortBindings: []*mierupb.PortBinding{ + { + Port: proto.Int32(10000), + Protocol: transportProtocol, + }, + }, + }, + }, + }, + }, }, { option: MieruOption{ @@ -28,28 +57,212 @@ func TestNewMieru(t *testing.T) { Password: "test", }, wantBaseAddr: "[2001:db8::1]:10001", + wantConfig: &mieruclient.ClientConfig{ + Profile: &mierupb.ClientProfile{ + ProfileName: proto.String("test"), + User: &mierupb.User{ + Name: proto.String("test"), + Password: proto.String("test"), + }, + Servers: []*mierupb.ServerEndpoint{ + { + IpAddress: proto.String("2001:db8::1"), + PortBindings: []*mierupb.PortBinding{ + { + PortRange: proto.String("10001-10002"), + Protocol: transportProtocol, + }, + }, + }, + }, + }, + }, }, { option: MieruOption{ Name: "test", Server: "example.com", - Port: 10003, + Port: "10003", Transport: "TCP", UserName: "test", Password: "test", }, wantBaseAddr: "example.com:10003", + wantConfig: &mieruclient.ClientConfig{ + Profile: &mierupb.ClientProfile{ + ProfileName: proto.String("test"), + User: &mierupb.User{ + Name: proto.String("test"), + Password: proto.String("test"), + }, + Servers: []*mierupb.ServerEndpoint{ + { + DomainName: proto.String("example.com"), + PortBindings: []*mierupb.PortBinding{ + { + Port: proto.Int32(10003), + Protocol: transportProtocol, + }, + }, + }, + }, + }, + }, + }, + { + option: MieruOption{ + Name: "test", + Server: "example.com", + Port: "10004,10005", + Transport: "TCP", + UserName: "test", + Password: "test", + }, + wantBaseAddr: "example.com:10004", + wantConfig: &mieruclient.ClientConfig{ + Profile: &mierupb.ClientProfile{ + ProfileName: proto.String("test"), + User: &mierupb.User{ + Name: proto.String("test"), + Password: proto.String("test"), + }, + Servers: []*mierupb.ServerEndpoint{ + { + DomainName: proto.String("example.com"), + PortBindings: []*mierupb.PortBinding{ + { + Port: proto.Int32(10004), + Protocol: transportProtocol, + }, + { + Port: proto.Int32(10005), + Protocol: transportProtocol, + }, + }, + }, + }, + }, + }, + }, + { + option: MieruOption{ + Name: "test", + Server: "example.com", + Port: "10006-10007,11000", + Transport: "TCP", + UserName: "test", + Password: "test", + }, + wantBaseAddr: "example.com:10006", + wantConfig: &mieruclient.ClientConfig{ + Profile: &mierupb.ClientProfile{ + ProfileName: proto.String("test"), + User: &mierupb.User{ + Name: proto.String("test"), + Password: proto.String("test"), + }, + Servers: []*mierupb.ServerEndpoint{ + { + DomainName: proto.String("example.com"), + PortBindings: []*mierupb.PortBinding{ + { + PortRange: proto.String("10006-10007"), + Protocol: transportProtocol, + }, + { + Port: proto.Int32(11000), + Protocol: transportProtocol, + }, + }, + }, + }, + }, + }, + }, + { + option: MieruOption{ + Name: "test", + Server: "example.com", + Port: "10008", + PortRange: "10009-10010", + Transport: "TCP", + UserName: "test", + Password: "test", + }, + wantBaseAddr: "example.com:10008", + wantConfig: &mieruclient.ClientConfig{ + Profile: &mierupb.ClientProfile{ + ProfileName: proto.String("test"), + User: &mierupb.User{ + Name: proto.String("test"), + Password: proto.String("test"), + }, + Servers: []*mierupb.ServerEndpoint{ + { + DomainName: proto.String("example.com"), + PortBindings: []*mierupb.PortBinding{ + { + Port: proto.Int32(10008), + Protocol: transportProtocol, + }, + { + PortRange: proto.String("10009-10010"), + Protocol: transportProtocol, + }, + }, + }, + }, + }, + }, }, } for _, testCase := range testCases { mieru, err := NewMieru(testCase.option) if err != nil { - t.Error(err) + t.Fatal(err) + } + config, err := mieru.client.Load() + if err != nil { + t.Fatal(err) } + config.Dialer = nil if mieru.addr != testCase.wantBaseAddr { t.Errorf("got addr %q, want %q", mieru.addr, testCase.wantBaseAddr) } + if !reflect.DeepEqual(config, testCase.wantConfig) { + t.Errorf("got config %+v, want %+v", config, testCase.wantConfig) + } + } +} + +func TestNewMieruError(t *testing.T) { + testCases := []MieruOption{ + { + Name: "test", + Server: "example.com", + Port: "invalid", + PortRange: "invalid", + Transport: "TCP", + UserName: "test", + Password: "test", + }, + { + Name: "test", + Server: "example.com", + Port: "", + PortRange: "", + Transport: "TCP", + UserName: "test", + Password: "test", + }, + } + + for _, option := range testCases { + _, err := NewMieru(option) + if err == nil { + t.Errorf("expected error for option %+v, but got nil", option) + } } } @@ -63,6 +276,7 @@ func TestBeginAndEndPortFromPortRange(t *testing.T) { {"1-10", 1, 10, false}, {"1000-2000", 1000, 2000, false}, {"65535-65535", 65535, 65535, false}, + {"2000-1000", 0, 0, true}, {"1", 0, 0, true}, {"1-", 0, 0, true}, {"-10", 0, 0, true}, diff --git a/adapter/outbound/shadowsocks.go b/adapter/outbound/shadowsocks.go index 595ef0e5e8..c6cfa9147b 100644 --- a/adapter/outbound/shadowsocks.go +++ b/adapter/outbound/shadowsocks.go @@ -11,7 +11,9 @@ import ( "github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/proxydialer" C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/ntp" gost "github.com/metacubex/mihomo/transport/gost-plugin" + "github.com/metacubex/mihomo/transport/kcptun" "github.com/metacubex/mihomo/transport/restls" obfs "github.com/metacubex/mihomo/transport/simple-obfs" shadowtls "github.com/metacubex/mihomo/transport/sing-shadowtls" @@ -35,6 +37,7 @@ type ShadowSocks struct { gostOption *gost.Option shadowTLSOption *shadowtls.ShadowTLSOption restlsConfig *restls.Config + kcptunClient *kcptun.Client } type ShadowSocksOption struct { @@ -64,6 +67,8 @@ type v2rayObfsOption struct { TLS bool `obfs:"tls,omitempty"` ECHOpts ECHOptions `obfs:"ech-opts,omitempty"` Fingerprint string `obfs:"fingerprint,omitempty"` + Certificate string `obfs:"certificate,omitempty"` + PrivateKey string `obfs:"private-key,omitempty"` Headers map[string]string `obfs:"headers,omitempty"` SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"` Mux bool `obfs:"mux,omitempty"` @@ -78,6 +83,8 @@ type gostObfsOption struct { TLS bool `obfs:"tls,omitempty"` ECHOpts ECHOptions `obfs:"ech-opts,omitempty"` Fingerprint string `obfs:"fingerprint,omitempty"` + Certificate string `obfs:"certificate,omitempty"` + PrivateKey string `obfs:"private-key,omitempty"` Headers map[string]string `obfs:"headers,omitempty"` SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"` Mux bool `obfs:"mux,omitempty"` @@ -87,6 +94,8 @@ type shadowTLSOption struct { Password string `obfs:"password,omitempty"` Host string `obfs:"host"` Fingerprint string `obfs:"fingerprint,omitempty"` + Certificate string `obfs:"certificate,omitempty"` + PrivateKey string `obfs:"private-key,omitempty"` SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"` Version int `obfs:"version,omitempty"` ALPN []string `obfs:"alpn,omitempty"` @@ -99,6 +108,32 @@ type restlsOption struct { RestlsScript string `obfs:"restls-script,omitempty"` } +type kcpTunOption struct { + Key string `obfs:"key,omitempty"` + Crypt string `obfs:"crypt,omitempty"` + Mode string `obfs:"mode,omitempty"` + Conn int `obfs:"conn,omitempty"` + AutoExpire int `obfs:"autoexpire,omitempty"` + ScavengeTTL int `obfs:"scavengettl,omitempty"` + MTU int `obfs:"mtu,omitempty"` + SndWnd int `obfs:"sndwnd,omitempty"` + RcvWnd int `obfs:"rcvwnd,omitempty"` + DataShard int `obfs:"datashard,omitempty"` + ParityShard int `obfs:"parityshard,omitempty"` + DSCP int `obfs:"dscp,omitempty"` + NoComp bool `obfs:"nocomp,omitempty"` + AckNodelay bool `obfs:"acknodelay,omitempty"` + NoDelay int `obfs:"nodelay,omitempty"` + Interval int `obfs:"interval,omitempty"` + Resend int `obfs:"resend,omitempty"` + NoCongestion int `obfs:"nc,omitempty"` + SockBuf int `obfs:"sockbuf,omitempty"` + SmuxVer int `obfs:"smuxver,omitempty"` + SmuxBuf int `obfs:"smuxbuf,omitempty"` + StreamBuf int `obfs:"streambuf,omitempty"` + KeepAlive int `obfs:"keepalive,omitempty"` +} + // StreamConnContext implements C.ProxyAdapter func (ss *ShadowSocks) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ net.Conn, err error) { useEarly := false @@ -167,7 +202,27 @@ func (ss *ShadowSocks) DialContextWithDialer(ctx context.Context, dialer C.Diale return nil, err } } - c, err := dialer.DialContext(ctx, "tcp", ss.addr) + var c net.Conn + if ss.kcptunClient != nil { + c, err = ss.kcptunClient.OpenStream(ctx, func(ctx context.Context) (net.PacketConn, net.Addr, error) { + if err = ss.ResolveUDP(ctx, metadata); err != nil { + return nil, nil, err + } + addr, err := resolveUDPAddr(ctx, "udp", ss.addr, ss.prefer) + if err != nil { + return nil, nil, err + } + + pc, err := dialer.ListenPacket(ctx, "udp", "", addr.AddrPort()) + if err != nil { + return nil, nil, err + } + + return pc, addr, nil + }) + } else { + c, err = dialer.DialContext(ctx, "tcp", ss.addr) + } if err != nil { return nil, fmt.Errorf("%s connect error: %w", ss.addr, err) } @@ -249,10 +304,18 @@ func (ss *ShadowSocks) SupportUOT() bool { return ss.option.UDPOverTCP } +func (ss *ShadowSocks) Close() error { + if ss.kcptunClient != nil { + return ss.kcptunClient.Close() + } + return nil +} + func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) - method, err := shadowsocks.CreateMethod(context.Background(), option.Cipher, shadowsocks.MethodOptions{ + method, err := shadowsocks.CreateMethod(option.Cipher, shadowsocks.MethodOptions{ Password: option.Password, + TimeFunc: ntp.Now, }) if err != nil { return nil, fmt.Errorf("ss %s cipher: %s initialize error: %w", addr, option.Cipher, err) @@ -263,6 +326,7 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { var obfsOption *simpleObfsOption var shadowTLSOpt *shadowtls.ShadowTLSOption var restlsConfig *restls.Config + var kcptunClient *kcptun.Client obfsMode := "" decoder := structure.NewDecoder(structure.Option{TagName: "obfs", WeaklyTypedInput: true}) @@ -300,6 +364,8 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { v2rayOption.TLS = true v2rayOption.SkipCertVerify = opts.SkipCertVerify v2rayOption.Fingerprint = opts.Fingerprint + v2rayOption.Certificate = opts.Certificate + v2rayOption.PrivateKey = opts.PrivateKey echConfig, err := opts.ECHOpts.Parse() if err != nil { @@ -328,6 +394,8 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { gostOption.TLS = true gostOption.SkipCertVerify = opts.SkipCertVerify gostOption.Fingerprint = opts.Fingerprint + gostOption.Certificate = opts.Certificate + gostOption.PrivateKey = opts.PrivateKey echConfig, err := opts.ECHOpts.Parse() if err != nil { @@ -348,6 +416,8 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { Password: opt.Password, Host: opt.Host, Fingerprint: opt.Fingerprint, + Certificate: opt.Certificate, + PrivateKey: opt.PrivateKey, ClientFingerprint: option.ClientFingerprint, SkipCertVerify: opt.SkipCertVerify, Version: opt.Version, @@ -370,6 +440,39 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { return nil, fmt.Errorf("ss %s initialize restls-plugin error: %w", addr, err) } + } else if option.Plugin == kcptun.Mode { + obfsMode = kcptun.Mode + kcptunOpt := &kcpTunOption{} + if err := decoder.Decode(option.PluginOpts, kcptunOpt); err != nil { + return nil, fmt.Errorf("ss %s initialize kcptun-plugin error: %w", addr, err) + } + + kcptunClient = kcptun.NewClient(kcptun.Config{ + Key: kcptunOpt.Key, + Crypt: kcptunOpt.Crypt, + Mode: kcptunOpt.Mode, + Conn: kcptunOpt.Conn, + AutoExpire: kcptunOpt.AutoExpire, + ScavengeTTL: kcptunOpt.ScavengeTTL, + MTU: kcptunOpt.MTU, + SndWnd: kcptunOpt.SndWnd, + RcvWnd: kcptunOpt.RcvWnd, + DataShard: kcptunOpt.DataShard, + ParityShard: kcptunOpt.ParityShard, + DSCP: kcptunOpt.DSCP, + NoComp: kcptunOpt.NoComp, + AckNodelay: kcptunOpt.AckNodelay, + NoDelay: kcptunOpt.NoDelay, + Interval: kcptunOpt.Interval, + Resend: kcptunOpt.Resend, + NoCongestion: kcptunOpt.NoCongestion, + SockBuf: kcptunOpt.SockBuf, + SmuxVer: kcptunOpt.SmuxVer, + SmuxBuf: kcptunOpt.SmuxBuf, + StreamBuf: kcptunOpt.StreamBuf, + KeepAlive: kcptunOpt.KeepAlive, + }) + option.UDPOverTCP = true // must open uot } switch option.UDPOverTCPVersion { case uot.Version, uot.LegacyVersion: @@ -400,5 +503,6 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { obfsOption: obfsOption, shadowTLSOption: shadowTLSOpt, restlsConfig: restlsConfig, + kcptunClient: kcptunClient, }, nil } diff --git a/adapter/outbound/socks5.go b/adapter/outbound/socks5.go index 26c64dcec6..00e096c36c 100644 --- a/adapter/outbound/socks5.go +++ b/adapter/outbound/socks5.go @@ -39,6 +39,8 @@ type Socks5Option struct { UDP bool `proxy:"udp,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` Fingerprint string `proxy:"fingerprint,omitempty"` + Certificate string `proxy:"certificate,omitempty"` + PrivateKey string `proxy:"private-key,omitempty"` } // StreamConnContext implements C.ProxyAdapter @@ -193,13 +195,16 @@ func (ss *Socks5) clientHandshakeContext(ctx context.Context, c net.Conn, addr s func NewSocks5(option Socks5Option) (*Socks5, error) { var tlsConfig *tls.Config if option.TLS { - tlsConfig = &tls.Config{ - InsecureSkipVerify: option.SkipCertVerify, - ServerName: option.Server, - } - var err error - tlsConfig, err = ca.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint) + tlsConfig, err = ca.GetTLSConfig(ca.Option{ + TLSConfig: &tls.Config{ + InsecureSkipVerify: option.SkipCertVerify, + ServerName: option.Server, + }, + Fingerprint: option.Fingerprint, + Certificate: option.Certificate, + PrivateKey: option.PrivateKey, + }) if err != nil { return nil, err } diff --git a/adapter/outbound/trojan.go b/adapter/outbound/trojan.go index f33ac5d6b5..5e0de39c9e 100644 --- a/adapter/outbound/trojan.go +++ b/adapter/outbound/trojan.go @@ -48,6 +48,8 @@ type TrojanOption struct { SNI string `proxy:"sni,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` Fingerprint string `proxy:"fingerprint,omitempty"` + Certificate string `proxy:"certificate,omitempty"` + PrivateKey string `proxy:"private-key,omitempty"` UDP bool `proxy:"udp,omitempty"` Network string `proxy:"network,omitempty"` ECHOpts ECHOptions `proxy:"ech-opts,omitempty"` @@ -100,14 +102,17 @@ func (t *Trojan) StreamConnContext(ctx context.Context, c net.Conn, metadata *C. } wsOpts.TLS = true - tlsConfig := &tls.Config{ - NextProtos: alpn, - MinVersion: tls.VersionTLS12, - InsecureSkipVerify: t.option.SkipCertVerify, - ServerName: t.option.SNI, - } - - wsOpts.TLSConfig, err = ca.GetSpecifiedFingerprintTLSConfig(tlsConfig, t.option.Fingerprint) + wsOpts.TLSConfig, err = ca.GetTLSConfig(ca.Option{ + TLSConfig: &tls.Config{ + NextProtos: alpn, + MinVersion: tls.VersionTLS12, + InsecureSkipVerify: t.option.SkipCertVerify, + ServerName: t.option.SNI, + }, + Fingerprint: t.option.Fingerprint, + Certificate: t.option.Certificate, + PrivateKey: t.option.PrivateKey, + }) if err != nil { return nil, err } @@ -126,6 +131,8 @@ func (t *Trojan) StreamConnContext(ctx context.Context, c net.Conn, metadata *C. Host: t.option.SNI, SkipCertVerify: t.option.SkipCertVerify, FingerPrint: t.option.Fingerprint, + Certificate: t.option.Certificate, + PrivateKey: t.option.PrivateKey, ClientFingerprint: t.option.ClientFingerprint, NextProtos: alpn, ECH: t.echConfig, @@ -363,15 +370,17 @@ func NewTrojan(option TrojanOption) (*Trojan, error) { return c, nil } - tlsConfig := &tls.Config{ - NextProtos: option.ALPN, - MinVersion: tls.VersionTLS12, - InsecureSkipVerify: option.SkipCertVerify, - ServerName: option.SNI, - } - - var err error - tlsConfig, err = ca.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint) + tlsConfig, err := ca.GetTLSConfig(ca.Option{ + TLSConfig: &tls.Config{ + NextProtos: option.ALPN, + MinVersion: tls.VersionTLS12, + InsecureSkipVerify: option.SkipCertVerify, + ServerName: option.SNI, + }, + Fingerprint: option.Fingerprint, + Certificate: option.Certificate, + PrivateKey: option.PrivateKey, + }) if err != nil { return nil, err } diff --git a/adapter/outbound/tuic.go b/adapter/outbound/tuic.go index 7a913fbf27..f76f482f57 100644 --- a/adapter/outbound/tuic.go +++ b/adapter/outbound/tuic.go @@ -55,8 +55,8 @@ type TuicOption struct { CWND int `proxy:"cwnd,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` Fingerprint string `proxy:"fingerprint,omitempty"` - CustomCA string `proxy:"ca,omitempty"` - CustomCAString string `proxy:"ca-str,omitempty"` + Certificate string `proxy:"certificate,omitempty"` + PrivateKey string `proxy:"private-key,omitempty"` ReceiveWindowConn int `proxy:"recv-window-conn,omitempty"` ReceiveWindow int `proxy:"recv-window,omitempty"` DisableMTUDiscovery bool `proxy:"disable-mtu-discovery,omitempty"` @@ -161,17 +161,20 @@ func (t *Tuic) ProxyInfo() C.ProxyInfo { func NewTuic(option TuicOption) (*Tuic, error) { addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) serverName := option.Server - tlsConfig := &tls.Config{ - ServerName: serverName, - InsecureSkipVerify: option.SkipCertVerify, - MinVersion: tls.VersionTLS13, - } if option.SNI != "" { - tlsConfig.ServerName = option.SNI + serverName = option.SNI } - var err error - tlsConfig, err = ca.GetTLSConfig(tlsConfig, option.Fingerprint, option.CustomCA, option.CustomCAString) + tlsConfig, err := ca.GetTLSConfig(ca.Option{ + TLSConfig: &tls.Config{ + ServerName: serverName, + InsecureSkipVerify: option.SkipCertVerify, + MinVersion: tls.VersionTLS13, + }, + Fingerprint: option.Fingerprint, + Certificate: option.Certificate, + PrivateKey: option.PrivateKey, + }) if err != nil { return nil, err } diff --git a/adapter/outbound/util.go b/adapter/outbound/util.go index f84193a5bb..6252a9d0ee 100644 --- a/adapter/outbound/util.go +++ b/adapter/outbound/util.go @@ -39,6 +39,7 @@ func resolveUDPAddr(ctx context.Context, network, address string, prefer C.DNSPr if err != nil { return nil, err } + var ip netip.Addr switch prefer { case C.IPv4Only: @@ -56,7 +57,17 @@ func resolveUDPAddr(ctx context.Context, network, address string, prefer C.DNSPr } ip, port = resolver.LookupIP4P(ip, port) - return net.ResolveUDPAddr(network, net.JoinHostPort(ip.String(), port)) + + var uint16Port uint16 + if port, err := strconv.ParseUint(port, 10, 16); err == nil { + uint16Port = uint16(port) + } else { + return nil, err + } + // our resolver always unmap before return, so unneeded unmap at here + // which is different with net.ResolveUDPAddr maybe return 4in6 address + // 4in6 addresses can cause some strange effects on sing-based code + return net.UDPAddrFromAddrPort(netip.AddrPortFrom(ip, uint16Port)), nil } func safeConnClose(c net.Conn, err error) { diff --git a/adapter/outbound/vless.go b/adapter/outbound/vless.go index 3db99e1feb..00e46fdd8c 100644 --- a/adapter/outbound/vless.go +++ b/adapter/outbound/vless.go @@ -64,10 +64,11 @@ type VlessOption struct { HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"` GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"` WSOpts WSOptions `proxy:"ws-opts,omitempty"` - WSPath string `proxy:"ws-path,omitempty"` WSHeaders map[string]string `proxy:"ws-headers,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` Fingerprint string `proxy:"fingerprint,omitempty"` + Certificate string `proxy:"certificate,omitempty"` + PrivateKey string `proxy:"private-key,omitempty"` ServerName string `proxy:"servername,omitempty"` ClientFingerprint string `proxy:"client-fingerprint,omitempty"` } @@ -96,14 +97,17 @@ func (v *Vless) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M } if v.option.TLS { wsOpts.TLS = true - tlsConfig := &tls.Config{ - MinVersion: tls.VersionTLS12, - ServerName: host, - InsecureSkipVerify: v.option.SkipCertVerify, - NextProtos: []string{"http/1.1"}, - } - - wsOpts.TLSConfig, err = ca.GetSpecifiedFingerprintTLSConfig(tlsConfig, v.option.Fingerprint) + wsOpts.TLSConfig, err = ca.GetTLSConfig(ca.Option{ + TLSConfig: &tls.Config{ + MinVersion: tls.VersionTLS12, + ServerName: host, + InsecureSkipVerify: v.option.SkipCertVerify, + NextProtos: []string{"http/1.1"}, + }, + Fingerprint: v.option.Fingerprint, + Certificate: v.option.Certificate, + PrivateKey: v.option.PrivateKey, + }) if err != nil { return nil, err } @@ -206,6 +210,8 @@ func (v *Vless) streamTLSConn(ctx context.Context, conn net.Conn, isH2 bool) (ne Host: host, SkipCertVerify: v.option.SkipCertVerify, FingerPrint: v.option.Fingerprint, + Certificate: v.option.Certificate, + PrivateKey: v.option.PrivateKey, ClientFingerprint: v.option.ClientFingerprint, ECH: v.echConfig, Reality: v.realityConfig, @@ -407,7 +413,7 @@ func parseVlessAddr(metadata *C.Metadata, xudp bool) *vless.DstAddr { func NewVless(option VlessOption) (*Vless, error) { var addons *vless.Addons - if option.Network != "ws" && len(option.Flow) >= 16 { + if len(option.Flow) >= 16 { option.Flow = option.Flow[:16] if option.Flow != vless.XRV { return nil, fmt.Errorf("unsupported xtls flow type: %s", option.Flow) @@ -499,10 +505,15 @@ func NewVless(option VlessOption) (*Vless, error) { } var tlsConfig *tls.Config if option.TLS { - tlsConfig, err = ca.GetSpecifiedFingerprintTLSConfig(&tls.Config{ - InsecureSkipVerify: v.option.SkipCertVerify, - ServerName: v.option.ServerName, - }, v.option.Fingerprint) + tlsConfig, err = ca.GetTLSConfig(ca.Option{ + TLSConfig: &tls.Config{ + InsecureSkipVerify: v.option.SkipCertVerify, + ServerName: v.option.ServerName, + }, + Fingerprint: v.option.Fingerprint, + Certificate: v.option.Certificate, + PrivateKey: v.option.PrivateKey, + }) if err != nil { return nil, err } diff --git a/adapter/outbound/vmess.go b/adapter/outbound/vmess.go index fbf8c266c1..c4badf9983 100644 --- a/adapter/outbound/vmess.go +++ b/adapter/outbound/vmess.go @@ -58,6 +58,8 @@ type VmessOption struct { ALPN []string `proxy:"alpn,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` Fingerprint string `proxy:"fingerprint,omitempty"` + Certificate string `proxy:"certificate,omitempty"` + PrivateKey string `proxy:"private-key,omitempty"` ServerName string `proxy:"servername,omitempty"` ECHOpts ECHOptions `proxy:"ech-opts,omitempty"` RealityOpts RealityOptions `proxy:"reality-opts,omitempty"` @@ -123,13 +125,16 @@ func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M if v.option.TLS { wsOpts.TLS = true - tlsConfig := &tls.Config{ - ServerName: host, - InsecureSkipVerify: v.option.SkipCertVerify, - NextProtos: []string{"http/1.1"}, - } - - wsOpts.TLSConfig, err = ca.GetSpecifiedFingerprintTLSConfig(tlsConfig, v.option.Fingerprint) + wsOpts.TLSConfig, err = ca.GetTLSConfig(ca.Option{ + TLSConfig: &tls.Config{ + ServerName: host, + InsecureSkipVerify: v.option.SkipCertVerify, + NextProtos: []string{"http/1.1"}, + }, + Fingerprint: v.option.Fingerprint, + Certificate: v.option.Certificate, + PrivateKey: v.option.PrivateKey, + }) if err != nil { return nil, err } @@ -178,6 +183,8 @@ func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M Host: host, SkipCertVerify: v.option.SkipCertVerify, FingerPrint: v.option.Fingerprint, + Certificate: v.option.Certificate, + PrivateKey: v.option.PrivateKey, NextProtos: []string{"h2"}, ClientFingerprint: v.option.ClientFingerprint, Reality: v.realityConfig, @@ -208,6 +215,8 @@ func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M Host: host, SkipCertVerify: v.option.SkipCertVerify, FingerPrint: v.option.Fingerprint, + Certificate: v.option.Certificate, + PrivateKey: v.option.PrivateKey, ClientFingerprint: v.option.ClientFingerprint, ECH: v.echConfig, Reality: v.realityConfig, @@ -501,10 +510,15 @@ func NewVmess(option VmessOption) (*Vmess, error) { } var tlsConfig *tls.Config if option.TLS { - tlsConfig, err = ca.GetSpecifiedFingerprintTLSConfig(&tls.Config{ - InsecureSkipVerify: v.option.SkipCertVerify, - ServerName: v.option.ServerName, - }, v.option.Fingerprint) + tlsConfig, err = ca.GetTLSConfig(ca.Option{ + TLSConfig: &tls.Config{ + InsecureSkipVerify: v.option.SkipCertVerify, + ServerName: v.option.ServerName, + }, + Fingerprint: v.option.Fingerprint, + Certificate: v.option.Certificate, + PrivateKey: v.option.PrivateKey, + }) if err != nil { return nil, err } diff --git a/adapter/outbound/wireguard.go b/adapter/outbound/wireguard.go index f59033febe..ba6e99ee1b 100644 --- a/adapter/outbound/wireguard.go +++ b/adapter/outbound/wireguard.go @@ -87,26 +87,26 @@ type WireGuardPeerOption struct { } type AmneziaWGOption struct { - JC int `proxy:"jc,omitempty"` - JMin int `proxy:"jmin,omitempty"` - JMax int `proxy:"jmax,omitempty"` - S1 int `proxy:"s1,omitempty"` - S2 int `proxy:"s2,omitempty"` - H1 uint32 `proxy:"h1,omitempty"` - H2 uint32 `proxy:"h2,omitempty"` - H3 uint32 `proxy:"h3,omitempty"` - H4 uint32 `proxy:"h4,omitempty"` - - // AmneziaWG v1.5 - I1 string `proxy:"i1,omitempty"` - I2 string `proxy:"i2,omitempty"` - I3 string `proxy:"i3,omitempty"` - I4 string `proxy:"i4,omitempty"` - I5 string `proxy:"i5,omitempty"` - J1 string `proxy:"j1,omitempty"` - J2 string `proxy:"j2,omitempty"` - J3 string `proxy:"j3,omitempty"` - Itime int64 `proxy:"itime,omitempty"` + JC int `proxy:"jc,omitempty"` + JMin int `proxy:"jmin,omitempty"` + JMax int `proxy:"jmax,omitempty"` + S1 int `proxy:"s1,omitempty"` + S2 int `proxy:"s2,omitempty"` + S3 int `proxy:"s3,omitempty"` // AmneziaWG v1.5 and v2 + S4 int `proxy:"s4,omitempty"` // AmneziaWG v1.5 and v2 + H1 string `proxy:"h1,omitempty"` // In AmneziaWG v1.x, it was uint32, but our WeaklyTypedInput can handle this situation + H2 string `proxy:"h2,omitempty"` // In AmneziaWG v1.x, it was uint32, but our WeaklyTypedInput can handle this situation + H3 string `proxy:"h3,omitempty"` // In AmneziaWG v1.x, it was uint32, but our WeaklyTypedInput can handle this situation + H4 string `proxy:"h4,omitempty"` // In AmneziaWG v1.x, it was uint32, but our WeaklyTypedInput can handle this situation + I1 string `proxy:"i1,omitempty"` // AmneziaWG v1.5 and v2 + I2 string `proxy:"i2,omitempty"` // AmneziaWG v1.5 and v2 + I3 string `proxy:"i3,omitempty"` // AmneziaWG v1.5 and v2 + I4 string `proxy:"i4,omitempty"` // AmneziaWG v1.5 and v2 + I5 string `proxy:"i5,omitempty"` // AmneziaWG v1.5 and v2 + J1 string `proxy:"j1,omitempty"` // AmneziaWG v1.5 only (removed in v2) + J2 string `proxy:"j2,omitempty"` // AmneziaWG v1.5 only (removed in v2) + J3 string `proxy:"j3,omitempty"` // AmneziaWG v1.5 only (removed in v2) + Itime int64 `proxy:"itime,omitempty"` // AmneziaWG v1.5 only (removed in v2) } type wgSingErrorHandler struct { @@ -412,17 +412,23 @@ func (w *WireGuard) genIpcConf(ctx context.Context, updateOnly bool) (string, er if w.option.AmneziaWGOption.S2 != 0 { ipcConf += "s2=" + strconv.Itoa(w.option.AmneziaWGOption.S2) + "\n" } - if w.option.AmneziaWGOption.H1 != 0 { - ipcConf += "h1=" + strconv.FormatUint(uint64(w.option.AmneziaWGOption.H1), 10) + "\n" + if w.option.AmneziaWGOption.S3 != 0 { + ipcConf += "s3=" + strconv.Itoa(w.option.AmneziaWGOption.S3) + "\n" } - if w.option.AmneziaWGOption.H2 != 0 { - ipcConf += "h2=" + strconv.FormatUint(uint64(w.option.AmneziaWGOption.H2), 10) + "\n" + if w.option.AmneziaWGOption.S4 != 0 { + ipcConf += "s4=" + strconv.Itoa(w.option.AmneziaWGOption.S4) + "\n" } - if w.option.AmneziaWGOption.H3 != 0 { - ipcConf += "h3=" + strconv.FormatUint(uint64(w.option.AmneziaWGOption.H3), 10) + "\n" + if w.option.AmneziaWGOption.H1 != "" { + ipcConf += "h1=" + w.option.AmneziaWGOption.H1 + "\n" } - if w.option.AmneziaWGOption.H4 != 0 { - ipcConf += "h4=" + strconv.FormatUint(uint64(w.option.AmneziaWGOption.H4), 10) + "\n" + if w.option.AmneziaWGOption.H2 != "" { + ipcConf += "h2=" + w.option.AmneziaWGOption.H2 + "\n" + } + if w.option.AmneziaWGOption.H3 != "" { + ipcConf += "h3=" + w.option.AmneziaWGOption.H3 + "\n" + } + if w.option.AmneziaWGOption.H4 != "" { + ipcConf += "h4=" + w.option.AmneziaWGOption.H4 + "\n" } if w.option.AmneziaWGOption.I1 != "" { ipcConf += "i1=" + w.option.AmneziaWGOption.I1 + "\n" diff --git a/adapter/provider/provider.go b/adapter/provider/provider.go index 3c73d36140..2f71b41bb1 100644 --- a/adapter/provider/provider.go +++ b/adapter/provider/provider.go @@ -331,15 +331,22 @@ func (cp *CompatibleProvider) Close() error { } func NewProxiesParser(filter string, excludeFilter string, excludeType string, dialerProxy string, override OverrideSchema) (resource.Parser[[]C.Proxy], error) { - excludeFilterReg, err := regexp2.Compile(excludeFilter, regexp2.None) - if err != nil { - return nil, fmt.Errorf("invalid excludeFilter regex: %w", err) - } var excludeTypeArray []string if excludeType != "" { excludeTypeArray = strings.Split(excludeType, "|") } + var excludeFilterRegs []*regexp2.Regexp + if excludeFilter != "" { + for _, excludeFilter := range strings.Split(excludeFilter, "`") { + excludeFilterReg, err := regexp2.Compile(excludeFilter, regexp2.None) + if err != nil { + return nil, fmt.Errorf("invalid excludeFilter regex: %w", err) + } + excludeFilterRegs = append(excludeFilterRegs, excludeFilterReg) + } + } + var filterRegs []*regexp2.Regexp for _, filter := range strings.Split(filter, "`") { filterReg, err := regexp2.Compile(filter, regexp2.None) @@ -367,8 +374,9 @@ func NewProxiesParser(filter string, excludeFilter string, excludeType string, d proxies := []C.Proxy{} proxiesSet := map[string]struct{}{} for _, filterReg := range filterRegs { + LOOP1: for idx, mapping := range schema.Proxies { - if nil != excludeTypeArray && len(excludeTypeArray) > 0 { + if len(excludeTypeArray) > 0 { mType, ok := mapping["type"] if !ok { continue @@ -377,18 +385,11 @@ func NewProxiesParser(filter string, excludeFilter string, excludeType string, d if !ok { continue } - flag := false - for i := range excludeTypeArray { - if strings.EqualFold(pType, excludeTypeArray[i]) { - flag = true - break + for _, excludeType := range excludeTypeArray { + if strings.EqualFold(pType, excludeType) { + continue LOOP1 } - - } - if flag { - continue } - } mName, ok := mapping["name"] if !ok { @@ -398,9 +399,11 @@ func NewProxiesParser(filter string, excludeFilter string, excludeType string, d if !ok { continue } - if len(excludeFilter) > 0 { - if mat, _ := excludeFilterReg.MatchString(name); mat { - continue + if len(excludeFilterRegs) > 0 { + for _, excludeFilterReg := range excludeFilterRegs { + if mat, _ := excludeFilterReg.MatchString(name); mat { + continue LOOP1 + } } } if len(filter) > 0 { diff --git a/common/net/sing.go b/common/net/sing.go index 5cf9759427..72bfd97253 100644 --- a/common/net/sing.go +++ b/common/net/sing.go @@ -1,9 +1,8 @@ package net import ( - "context" + "io" "net" - "runtime" "github.com/metacubex/mihomo/common/net/deadline" @@ -26,11 +25,20 @@ type ReadWaitOptions = network.ReadWaitOptions var NewReadWaitOptions = network.NewReadWaitOptions +type ReaderWithUpstream = network.ReaderWithUpstream +type WithUpstreamReader = network.WithUpstreamReader +type WriterWithUpstream = network.WriterWithUpstream +type WithUpstreamWriter = network.WithUpstreamWriter +type WithUpstream = common.WithUpstream + +var UnwrapReader = network.UnwrapReader +var UnwrapWriter = network.UnwrapWriter + func NewDeadlineConn(conn net.Conn) ExtendedConn { - if deadline.IsPipe(conn) || deadline.IsPipe(network.UnwrapReader(conn)) { + if deadline.IsPipe(conn) || deadline.IsPipe(UnwrapReader(conn)) { return NewExtendedConn(conn) // pipe always have correctly deadline implement } - if deadline.IsConn(conn) || deadline.IsConn(network.UnwrapReader(conn)) { + if deadline.IsConn(conn) || deadline.IsConn(UnwrapReader(conn)) { return NewExtendedConn(conn) // was a *deadline.Conn } return deadline.NewConn(conn) @@ -47,9 +55,37 @@ type CountFunc = network.CountFunc var Pipe = deadline.Pipe +func closeWrite(writer io.Closer) error { + if c, ok := common.Cast[network.WriteCloser](writer); ok { + return c.CloseWrite() + } + return writer.Close() +} + // Relay copies between left and right bidirectionally. +// like [bufio.CopyConn] but remove unneeded [context.Context] handle and the cost of [task.Group] func Relay(leftConn, rightConn net.Conn) { - defer runtime.KeepAlive(leftConn) - defer runtime.KeepAlive(rightConn) - _ = bufio.CopyConn(context.TODO(), leftConn, rightConn) + defer func() { + _ = leftConn.Close() + _ = rightConn.Close() + }() + + ch := make(chan struct{}) + go func() { + _, err := bufio.Copy(leftConn, rightConn) + if err == nil { + _ = closeWrite(leftConn) + } else { + _ = leftConn.Close() + } + close(ch) + }() + + _, err := bufio.Copy(rightConn, leftConn) + if err == nil { + _ = closeWrite(rightConn) + } else { + _ = rightConn.Close() + } + <-ch } diff --git a/common/queue/queue.go b/common/queue/queue.go index d1b6beebe5..c328420407 100644 --- a/common/queue/queue.go +++ b/common/queue/queue.go @@ -2,8 +2,6 @@ package queue import ( "sync" - - "github.com/samber/lo" ) // Queue is a simple concurrent safe queue @@ -24,33 +22,32 @@ func (q *Queue[T]) Put(items ...T) { } // Pop returns the head of items. -func (q *Queue[T]) Pop() T { +func (q *Queue[T]) Pop() (head T) { if len(q.items) == 0 { - return lo.Empty[T]() + return } q.lock.Lock() - head := q.items[0] + head = q.items[0] q.items = q.items[1:] q.lock.Unlock() return head } // Last returns the last of item. -func (q *Queue[T]) Last() T { +func (q *Queue[T]) Last() (last T) { if len(q.items) == 0 { - return lo.Empty[T]() + return } q.lock.RLock() - last := q.items[len(q.items)-1] + last = q.items[len(q.items)-1] q.lock.RUnlock() return last } // Copy get the copy of queue. -func (q *Queue[T]) Copy() []T { - items := []T{} +func (q *Queue[T]) Copy() (items []T) { q.lock.RLock() items = append(items, q.items...) q.lock.RUnlock() diff --git a/common/queue/queue_test.go b/common/queue/queue_test.go new file mode 100644 index 0000000000..64f023c928 --- /dev/null +++ b/common/queue/queue_test.go @@ -0,0 +1,215 @@ +package queue + +import ( + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +// TestQueuePut tests the Put method of Queue +func TestQueuePut(t *testing.T) { + // Initialize a new queue + q := New[int](10) + + // Test putting a single item + q.Put(1) + assert.Equal(t, int64(1), q.Len(), "Queue length should be 1 after putting one item") + + // Test putting multiple items + q.Put(2, 3, 4) + assert.Equal(t, int64(4), q.Len(), "Queue length should be 4 after putting three more items") + + // Test putting zero items (should not change queue) + q.Put() + assert.Equal(t, int64(4), q.Len(), "Queue length should remain unchanged when putting zero items") +} + +// TestQueuePop tests the Pop method of Queue +func TestQueuePop(t *testing.T) { + // Initialize a new queue with items + q := New[int](10) + q.Put(1, 2, 3) + + // Test popping items in FIFO order + item := q.Pop() + assert.Equal(t, 1, item, "First item popped should be 1") + assert.Equal(t, int64(2), q.Len(), "Queue length should be 2 after popping one item") + + item = q.Pop() + assert.Equal(t, 2, item, "Second item popped should be 2") + assert.Equal(t, int64(1), q.Len(), "Queue length should be 1 after popping two items") + + item = q.Pop() + assert.Equal(t, 3, item, "Third item popped should be 3") + assert.Equal(t, int64(0), q.Len(), "Queue length should be 0 after popping all items") +} + +// TestQueuePopEmpty tests the Pop method on an empty queue +func TestQueuePopEmpty(t *testing.T) { + // Initialize a new empty queue + q := New[int](0) + + // Test popping from an empty queue + item := q.Pop() + assert.Equal(t, 0, item, "Popping from an empty queue should return the zero value") + assert.Equal(t, int64(0), q.Len(), "Queue length should remain 0 after popping from an empty queue") +} + +// TestQueueLast tests the Last method of Queue +func TestQueueLast(t *testing.T) { + // Initialize a new queue with items + q := New[int](10) + q.Put(1, 2, 3) + + // Test getting the last item + item := q.Last() + assert.Equal(t, 3, item, "Last item should be 3") + assert.Equal(t, int64(3), q.Len(), "Queue length should remain unchanged after calling Last") + + // Test Last on an empty queue + emptyQ := New[int](0) + emptyItem := emptyQ.Last() + assert.Equal(t, 0, emptyItem, "Last on an empty queue should return the zero value") +} + +// TestQueueCopy tests the Copy method of Queue +func TestQueueCopy(t *testing.T) { + // Initialize a new queue with items + q := New[int](10) + q.Put(1, 2, 3) + + // Test copying the queue + copy := q.Copy() + assert.Equal(t, 3, len(copy), "Copy should have the same number of items as the original queue") + assert.Equal(t, 1, copy[0], "First item in copy should be 1") + assert.Equal(t, 2, copy[1], "Second item in copy should be 2") + assert.Equal(t, 3, copy[2], "Third item in copy should be 3") + + // Verify that modifying the copy doesn't affect the original queue + copy[0] = 99 + assert.Equal(t, 1, q.Pop(), "Original queue should not be affected by modifying the copy") +} + +// TestQueueLen tests the Len method of Queue +func TestQueueLen(t *testing.T) { + // Initialize a new empty queue + q := New[int](10) + assert.Equal(t, int64(0), q.Len(), "New queue should have length 0") + + // Add items and check length + q.Put(1, 2) + assert.Equal(t, int64(2), q.Len(), "Queue length should be 2 after putting two items") + + // Remove an item and check length + q.Pop() + assert.Equal(t, int64(1), q.Len(), "Queue length should be 1 after popping one item") +} + +// TestQueueNew tests the New constructor +func TestQueueNew(t *testing.T) { + // Test creating a new queue with different hints + q1 := New[int](0) + assert.NotNil(t, q1, "New queue should not be nil") + assert.Equal(t, int64(0), q1.Len(), "New queue should have length 0") + + q2 := New[int](10) + assert.NotNil(t, q2, "New queue should not be nil") + assert.Equal(t, int64(0), q2.Len(), "New queue should have length 0") + + // Test with a different type + q3 := New[string](5) + assert.NotNil(t, q3, "New queue should not be nil") + assert.Equal(t, int64(0), q3.Len(), "New queue should have length 0") +} + +// TestQueueConcurrency tests the concurrency safety of Queue +func TestQueueConcurrency(t *testing.T) { + // Initialize a new queue + q := New[int](100) + + // Number of goroutines and operations + goroutines := 10 + operations := 100 + + // Wait group to synchronize goroutines + wg := sync.WaitGroup{} + wg.Add(goroutines * 2) // For both producers and consumers + + // Start producer goroutines + for i := 0; i < goroutines; i++ { + go func(id int) { + defer wg.Done() + for j := 0; j < operations; j++ { + q.Put(id*operations + j) + // Small sleep to increase chance of race conditions + time.Sleep(time.Microsecond) + } + }(i) + } + + // Start consumer goroutines + consumed := make(chan int, goroutines*operations) + for i := 0; i < goroutines; i++ { + go func() { + defer wg.Done() + for j := 0; j < operations; j++ { + // Try to pop an item, but don't block if queue is empty + // Use a mutex to avoid race condition between Len() check and Pop() + q.lock.Lock() + if len(q.items) > 0 { + item := q.items[0] + q.items = q.items[1:] + q.lock.Unlock() + consumed <- item + } else { + q.lock.Unlock() + } + // Small sleep to increase chance of race conditions + time.Sleep(time.Microsecond) + } + }() + } + + // Wait for all goroutines to finish + wg.Wait() + // Close the consumed channel + close(consumed) + + // Count the number of consumed items + consumedCount := 0 + for range consumed { + consumedCount++ + } + + // Check that the queue is in a consistent state + totalItems := goroutines * operations + remaining := int(q.Len()) + assert.Equal(t, totalItems, consumedCount+remaining, "Total items should equal consumed items plus remaining items") +} + +// TestQueueWithDifferentTypes tests the Queue with different types +func TestQueueWithDifferentTypes(t *testing.T) { + // Test with string type + qString := New[string](5) + qString.Put("hello", "world") + assert.Equal(t, int64(2), qString.Len(), "Queue length should be 2") + assert.Equal(t, "hello", qString.Pop(), "First item should be 'hello'") + assert.Equal(t, "world", qString.Pop(), "Second item should be 'world'") + + // Test with struct type + type Person struct { + Name string + Age int + } + + qStruct := New[Person](5) + qStruct.Put(Person{Name: "Alice", Age: 30}, Person{Name: "Bob", Age: 25}) + assert.Equal(t, int64(2), qStruct.Len(), "Queue length should be 2") + + firstPerson := qStruct.Pop() + assert.Equal(t, "Alice", firstPerson.Name, "First person's name should be 'Alice'") + secondPerson := qStruct.Pop() + assert.Equal(t, "Bob", secondPerson.Name, "Second person's name should be 'Bob'") +} diff --git a/component/ca/config.go b/component/ca/config.go index f4d3ae7510..8f96f7452e 100644 --- a/component/ca/config.go +++ b/component/ca/config.go @@ -10,7 +10,9 @@ import ( "strconv" "sync" + "github.com/metacubex/mihomo/common/once" C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/ntp" ) var globalCertPool *x509.CertPool @@ -65,70 +67,61 @@ func ResetCertificate() { initializeCertPool() } -func getCertPool() *x509.CertPool { +func GetCertPool() *x509.CertPool { + mutex.Lock() + defer mutex.Unlock() if globalCertPool == nil { - mutex.Lock() - defer mutex.Unlock() - if globalCertPool != nil { - return globalCertPool - } initializeCertPool() } return globalCertPool } -func GetCertPool(customCA string, customCAString string) (*x509.CertPool, error) { - var certificate []byte - var err error - if len(customCA) > 0 { - path := C.Path.Resolve(customCA) - if !C.Path.IsSafePath(path) { - return nil, C.Path.ErrNotSafePath(path) - } - certificate, err = os.ReadFile(path) - if err != nil { - return nil, fmt.Errorf("load ca error: %w", err) - } - } else if customCAString != "" { - certificate = []byte(customCAString) - } - if len(certificate) > 0 { - certPool := x509.NewCertPool() - if !certPool.AppendCertsFromPEM(certificate) { - return nil, fmt.Errorf("failed to parse certificate:\n\n %s", certificate) - } - return certPool, nil - } else { - return getCertPool(), nil - } +type Option struct { + TLSConfig *tls.Config + Fingerprint string + ZeroTrust bool + Certificate string + PrivateKey string } -// GetTLSConfig specified fingerprint, customCA and customCAString -func GetTLSConfig(tlsConfig *tls.Config, fingerprint string, customCA string, customCAString string) (_ *tls.Config, err error) { +func GetTLSConfig(opt Option) (tlsConfig *tls.Config, err error) { + tlsConfig = opt.TLSConfig if tlsConfig == nil { tlsConfig = &tls.Config{} } - tlsConfig.RootCAs, err = GetCertPool(customCA, customCAString) - if err != nil { - return nil, err + tlsConfig.Time = ntp.Now + + if opt.ZeroTrust { + tlsConfig.RootCAs = zeroTrustCertPool() + } else { + tlsConfig.RootCAs = GetCertPool() } - if len(fingerprint) > 0 { - tlsConfig.VerifyPeerCertificate, err = NewFingerprintVerifier(fingerprint) + if len(opt.Fingerprint) > 0 { + tlsConfig.VerifyPeerCertificate, err = NewFingerprintVerifier(opt.Fingerprint, tlsConfig.Time) if err != nil { return nil, err } tlsConfig.InsecureSkipVerify = true } - return tlsConfig, nil -} -// GetSpecifiedFingerprintTLSConfig specified fingerprint -func GetSpecifiedFingerprintTLSConfig(tlsConfig *tls.Config, fingerprint string) (*tls.Config, error) { - return GetTLSConfig(tlsConfig, fingerprint, "", "") + if len(opt.Certificate) > 0 || len(opt.PrivateKey) > 0 { + var cert tls.Certificate + cert, err = LoadTLSKeyPair(opt.Certificate, opt.PrivateKey, C.Path) + if err != nil { + return nil, err + } + tlsConfig.Certificates = []tls.Certificate{cert} + } + return tlsConfig, nil } -func GetGlobalTLSConfig(tlsConfig *tls.Config) *tls.Config { - tlsConfig, _ = GetTLSConfig(tlsConfig, "", "", "") - return tlsConfig -} +var zeroTrustCertPool = once.OnceValue(func() *x509.CertPool { + if len(_CaCertificates) != 0 { // always using embed cert first + zeroTrustCertPool := x509.NewCertPool() + if zeroTrustCertPool.AppendCertsFromPEM(_CaCertificates) { + return zeroTrustCertPool + } + } + return nil // fallback to system pool +}) diff --git a/component/ca/fingerprint.go b/component/ca/fingerprint.go index 5c44a31f32..3f240b1b05 100644 --- a/component/ca/fingerprint.go +++ b/component/ca/fingerprint.go @@ -7,10 +7,11 @@ import ( "encoding/hex" "fmt" "strings" + "time" ) // NewFingerprintVerifier returns a function that verifies whether a certificate's SHA-256 fingerprint matches the given one. -func NewFingerprintVerifier(fingerprint string) (func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error, error) { +func NewFingerprintVerifier(fingerprint string, time func() time.Time) (func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error, error) { switch fingerprint { case "chrome", "firefox", "safari", "ios", "android", "edge", "360", "qq", "random", "randomized": // WTF??? return nil, fmt.Errorf("`fingerprint` is used for TLS certificate pinning. If you need to specify the browser fingerprint, use `client-fingerprint`") @@ -27,9 +28,40 @@ func NewFingerprintVerifier(fingerprint string) (func(rawCerts [][]byte, verifie return func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { // ssl pining - for _, rawCert := range rawCerts { + for i, rawCert := range rawCerts { hash := sha256.Sum256(rawCert) if bytes.Equal(fpByte, hash[:]) { + if i > 0 { + + // When the fingerprint matches a non-leaf certificate, + // the certificate chain validity is verified using the certificate as the trusted root certificate. + // + // Currently, we do not verify that the SNI matches the certificate's DNS name, + // but we do verify the validity of the child certificate, + // including the issuance time and whether the child certificate was issued by the parent certificate. + + certs := make([]*x509.Certificate, i+1) // stop at i + for j := range certs { + cert, err := x509.ParseCertificate(rawCerts[j]) + if err != nil { + return err + } + certs[j] = cert + } + opts := x509.VerifyOptions{ + Roots: x509.NewCertPool(), + Intermediates: x509.NewCertPool(), + } + if time != nil { + opts.CurrentTime = time() + } + opts.Roots.AddCert(certs[i]) + for _, cert := range certs[1:] { + opts.Intermediates.AddCert(cert) + } + _, err := certs[0].Verify(opts) + return err + } return nil } } diff --git a/component/ca/fingerprint_test.go b/component/ca/fingerprint_test.go new file mode 100644 index 0000000000..6d8dff8434 --- /dev/null +++ b/component/ca/fingerprint_test.go @@ -0,0 +1,351 @@ +package ca + +import ( + "encoding/pem" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestFingerprintVerifierLeaf(t *testing.T) { + leafFingerprint := CalculateFingerprint(leafPEM.Bytes) + verifier, err := NewFingerprintVerifier(leafFingerprint, func() time.Time { + return time.Unix(1677615892, 0) + }) + require.NoError(t, err) + + err = verifier([][]byte{leafPEM.Bytes, intermediatePEM.Bytes, rootPEM.Bytes}, nil) + assert.NoError(t, err) + + err = verifier([][]byte{leafPEM.Bytes, smimeIntermediatePEM.Bytes, rootPEM.Bytes}, nil) + assert.NoError(t, err) + + err = verifier([][]byte{leafPEM.Bytes, intermediatePEM.Bytes, smimeRootPEM.Bytes}, nil) + assert.NoError(t, err) + + err = verifier([][]byte{leafWithInvalidHashPEM.Bytes, intermediatePEM.Bytes, rootPEM.Bytes}, nil) + assert.Error(t, err) + + err = verifier([][]byte{smimeLeafPEM.Bytes, intermediatePEM.Bytes, rootPEM.Bytes}, nil) + assert.Error(t, err) + + err = verifier([][]byte{smimeLeafPEM.Bytes, intermediatePEM.Bytes, smimeRootPEM.Bytes}, nil) + assert.Error(t, err) +} + +func TestFingerprintVerifierIntermediate(t *testing.T) { + intermediateFingerprint := CalculateFingerprint(intermediatePEM.Bytes) + verifier, err := NewFingerprintVerifier(intermediateFingerprint, func() time.Time { + return time.Unix(1677615892, 0) + }) + require.NoError(t, err) + + err = verifier([][]byte{leafPEM.Bytes, intermediatePEM.Bytes, rootPEM.Bytes}, nil) + assert.NoError(t, err) + + err = verifier([][]byte{leafPEM.Bytes, smimeIntermediatePEM.Bytes, rootPEM.Bytes}, nil) + assert.Error(t, err) + + err = verifier([][]byte{leafPEM.Bytes, intermediatePEM.Bytes, smimeRootPEM.Bytes}, nil) + assert.NoError(t, err) + + err = verifier([][]byte{leafWithInvalidHashPEM.Bytes, intermediatePEM.Bytes, rootPEM.Bytes}, nil) + assert.Error(t, err) + + err = verifier([][]byte{smimeLeafPEM.Bytes, intermediatePEM.Bytes, rootPEM.Bytes}, nil) + assert.Error(t, err) + + err = verifier([][]byte{smimeLeafPEM.Bytes, intermediatePEM.Bytes, smimeRootPEM.Bytes}, nil) + assert.Error(t, err) +} + +func TestFingerprintVerifierRoot(t *testing.T) { + rootFingerprint := CalculateFingerprint(rootPEM.Bytes) + verifier, err := NewFingerprintVerifier(rootFingerprint, func() time.Time { + return time.Unix(1677615892, 0) + }) + require.NoError(t, err) + + err = verifier([][]byte{leafPEM.Bytes, intermediatePEM.Bytes, rootPEM.Bytes}, nil) + assert.NoError(t, err) + + err = verifier([][]byte{leafPEM.Bytes, smimeIntermediatePEM.Bytes, rootPEM.Bytes}, nil) + assert.Error(t, err) + + err = verifier([][]byte{leafPEM.Bytes, intermediatePEM.Bytes, smimeRootPEM.Bytes}, nil) + assert.Error(t, err) + + err = verifier([][]byte{leafWithInvalidHashPEM.Bytes, intermediatePEM.Bytes, rootPEM.Bytes}, nil) + assert.Error(t, err) + + err = verifier([][]byte{smimeLeafPEM.Bytes, intermediatePEM.Bytes, rootPEM.Bytes}, nil) + assert.Error(t, err) + + err = verifier([][]byte{smimeLeafPEM.Bytes, intermediatePEM.Bytes, smimeRootPEM.Bytes}, nil) + assert.Error(t, err) +} + +var rootPEM, _ = pem.Decode([]byte(gtsRoot)) +var intermediatePEM, _ = pem.Decode([]byte(gtsIntermediate)) +var leafPEM, _ = pem.Decode([]byte(googleLeaf)) +var leafWithInvalidHashPEM, _ = pem.Decode([]byte(googleLeafWithInvalidHash)) +var smimeRootPEM, _ = pem.Decode([]byte(smimeRoot)) +var smimeIntermediatePEM, _ = pem.Decode([]byte(smimeIntermediate)) +var smimeLeafPEM, _ = pem.Decode([]byte(smimeLeaf)) + +const gtsIntermediate = `-----BEGIN CERTIFICATE----- +MIIFljCCA36gAwIBAgINAgO8U1lrNMcY9QFQZjANBgkqhkiG9w0BAQsFADBHMQsw +CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU +MBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMjAwODEzMDAwMDQyWhcNMjcwOTMwMDAw +MDQyWjBGMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp +Y2VzIExMQzETMBEGA1UEAxMKR1RTIENBIDFDMzCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAPWI3+dijB43+DdCkH9sh9D7ZYIl/ejLa6T/belaI+KZ9hzp +kgOZE3wJCor6QtZeViSqejOEH9Hpabu5dOxXTGZok3c3VVP+ORBNtzS7XyV3NzsX +lOo85Z3VvMO0Q+sup0fvsEQRY9i0QYXdQTBIkxu/t/bgRQIh4JZCF8/ZK2VWNAcm +BA2o/X3KLu/qSHw3TT8An4Pf73WELnlXXPxXbhqW//yMmqaZviXZf5YsBvcRKgKA +gOtjGDxQSYflispfGStZloEAoPtR28p3CwvJlk/vcEnHXG0g/Zm0tOLKLnf9LdwL +tmsTDIwZKxeWmLnwi/agJ7u2441Rj72ux5uxiZ0CAwEAAaOCAYAwggF8MA4GA1Ud +DwEB/wQEAwIBhjAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwEgYDVR0T +AQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUinR/r4XN7pXNPZzQ4kYU83E1HScwHwYD +VR0jBBgwFoAU5K8rJnEaK0gnhS9SZizv8IkTcT4waAYIKwYBBQUHAQEEXDBaMCYG +CCsGAQUFBzABhhpodHRwOi8vb2NzcC5wa2kuZ29vZy9ndHNyMTAwBggrBgEFBQcw +AoYkaHR0cDovL3BraS5nb29nL3JlcG8vY2VydHMvZ3RzcjEuZGVyMDQGA1UdHwQt +MCswKaAnoCWGI2h0dHA6Ly9jcmwucGtpLmdvb2cvZ3RzcjEvZ3RzcjEuY3JsMFcG +A1UdIARQME4wOAYKKwYBBAHWeQIFAzAqMCgGCCsGAQUFBwIBFhxodHRwczovL3Br +aS5nb29nL3JlcG9zaXRvcnkvMAgGBmeBDAECATAIBgZngQwBAgIwDQYJKoZIhvcN +AQELBQADggIBAIl9rCBcDDy+mqhXlRu0rvqrpXJxtDaV/d9AEQNMwkYUuxQkq/BQ +cSLbrcRuf8/xam/IgxvYzolfh2yHuKkMo5uhYpSTld9brmYZCwKWnvy15xBpPnrL +RklfRuFBsdeYTWU0AIAaP0+fbH9JAIFTQaSSIYKCGvGjRFsqUBITTcFTNvNCCK9U ++o53UxtkOCcXCb1YyRt8OS1b887U7ZfbFAO/CVMkH8IMBHmYJvJh8VNS/UKMG2Yr +PxWhu//2m+OBmgEGcYk1KCTd4b3rGS3hSMs9WYNRtHTGnXzGsYZbr8w0xNPM1IER +lQCh9BIiAfq0g3GvjLeMcySsN1PCAJA/Ef5c7TaUEDu9Ka7ixzpiO2xj2YC/WXGs +Yye5TBeg2vZzFb8q3o/zpWwygTMD0IZRcZk0upONXbVRWPeyk+gB9lm+cZv9TSjO +z23HFtz30dZGm6fKa+l3D/2gthsjgx0QGtkJAITgRNOidSOzNIb2ILCkXhAd4FJG +AJ2xDx8hcFH1mt0G/FX0Kw4zd8NLQsLxdxP8c4CU6x+7Nz/OAipmsHMdMqUybDKw +juDEI/9bfU1lcKwrmz3O2+BtjjKAvpafkmO8l7tdufThcV4q5O8DIrGKZTqPwJNl +1IXNDw9bg1kWRxYtnCQ6yICmJhSFm/Y3m6xv+cXDBlHz4n/FsRC6UfTd +-----END CERTIFICATE-----` + +const gtsRoot = `-----BEGIN CERTIFICATE----- +MIIFVzCCAz+gAwIBAgINAgPlk28xsBNJiGuiFzANBgkqhkiG9w0BAQwFADBHMQsw +CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU +MBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw +MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp +Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx9vaMf/vo +27xqLpeXo4xL+Sv2sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vXmX7w +Cl7raKb0xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g4M/SCYe7zUjw +TcLCeoiKu7rPWRnWr4+wB7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0Pfybl +qAj+lug8aJRT7oM6iCsVlgmy4HqMLnXWnOunVmSPlk9orj2XwoSPwLxAwAtcvfaH +szVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly4cpk9+aCEI3oncKKiPo4Zor8 +Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr06zqkUspzBmk +MiVOKvFlRNACzqrOSbTqn3yDsEB750Orp2yjj32JgfpMpf/VjsPOS+C12LOORc92 +wO1AK/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYWk70p +aDPvOmbsB4om3xPXV2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+DVrN +VjzRlwW5y0vtOUucxD/SVRNuJLDWcfr0wbrM7Rv1/oFB2ACYPTrIrnqYNxgFlQID +AQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E +FgQU5K8rJnEaK0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEMBQADggIBAJ+qQibb +C5u+/x6Wki4+omVKapi6Ist9wTrYggoGxval3sBOh2Z5ofmmWJyq+bXmYOfg6LEe +QkEzCzc9zolwFcq1JKjPa7XSQCGYzyI0zzvFIoTgxQ6KfF2I5DUkzps+GlQebtuy +h6f88/qBVRRiClmpIgUxPoLW7ttXNLwzldMXG+gnoot7TiYaelpkttGsN/H9oPM4 +7HLwEXWdyzRSjeZ2axfG34arJ45JK3VmgRAhpuo+9K4l/3wV3s6MJT/KYnAK9y8J +ZgfIPxz88NtFMN9iiMG1D53Dn0reWVlHxYciNuaCp+0KueIHoI17eko8cdLiA6Ef +MgfdG+RCzgwARWGAtQsgWSl4vflVy2PFPEz0tv/bal8xa5meLMFrUKTX5hgUvYU/ +Z6tGn6D/Qqc6f1zLXbBwHSs09dR2CQzreExZBfMzQsNhFRAbd03OIozUhfJFfbdT +6u9AWpQKXCBfTkBdYiJ23//OYb2MI3jSNwLgjt7RETeJ9r/tSQdirpLsQBqvFAnZ +0E6yove+7u7Y/9waLd64NnHi/Hm3lCXRSHNboTXns5lndcEZOitHTtNCjv0xyBZm +2tIMPNuzjsmhDYAPexZ3FL//2wmUspO8IFgV6dtxQ/PeEMMA3KgqlbbC1j+Qa3bb +bP6MvPJwNQzcmRk13NfIRmPVNnGuV/u3gm3c +-----END CERTIFICATE-----` + +const googleLeaf = `-----BEGIN CERTIFICATE----- +MIIFUjCCBDqgAwIBAgIQERmRWTzVoz0SMeozw2RM3DANBgkqhkiG9w0BAQsFADBG +MQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExM +QzETMBEGA1UEAxMKR1RTIENBIDFDMzAeFw0yMzAxMDIwODE5MTlaFw0yMzAzMjcw +ODE5MThaMBkxFzAVBgNVBAMTDnd3dy5nb29nbGUuY29tMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEAq30odrKMT54TJikMKL8S+lwoCMT5geP0u9pWjk6a +wdB6i3kO+UE4ijCAmhbcZKeKaLnGJ38weZNwB1ayabCYyX7hDiC/nRcZU49LX5+o +55kDVaNn14YKkg2kCeX25HDxSwaOsNAIXKPTqiQL5LPvc4Twhl8HY51hhNWQrTEr +N775eYbixEULvyVLq5BLbCOpPo8n0/MTjQ32ku1jQq3GIYMJC/Rf2VW5doF6t9zs +KleflAN8OdKp0ME9OHg0T1P3yyb67T7n0SpisHbeG06AmQcKJF9g/9VPJtRf4l1Q +WRPDC+6JUqzXCxAGmIRGZ7TNMxPMBW/7DRX6w8oLKVNb0wIDAQABo4ICZzCCAmMw +DgYDVR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMAwGA1UdEwEB/wQC +MAAwHQYDVR0OBBYEFBnboj3lf9+Xat4oEgo6ZtIMr8ZuMB8GA1UdIwQYMBaAFIp0 +f6+Fze6VzT2c0OJGFPNxNR0nMGoGCCsGAQUFBwEBBF4wXDAnBggrBgEFBQcwAYYb +aHR0cDovL29jc3AucGtpLmdvb2cvZ3RzMWMzMDEGCCsGAQUFBzAChiVodHRwOi8v +cGtpLmdvb2cvcmVwby9jZXJ0cy9ndHMxYzMuZGVyMBkGA1UdEQQSMBCCDnd3dy5n +b29nbGUuY29tMCEGA1UdIAQaMBgwCAYGZ4EMAQIBMAwGCisGAQQB1nkCBQMwPAYD +VR0fBDUwMzAxoC+gLYYraHR0cDovL2NybHMucGtpLmdvb2cvZ3RzMWMzL1FPdkow +TjFzVDJBLmNybDCCAQQGCisGAQQB1nkCBAIEgfUEgfIA8AB2AHoyjFTYty22IOo4 +4FIe6YQWcDIThU070ivBOlejUutSAAABhXHHOiUAAAQDAEcwRQIgBUkikUIXdo+S +3T8PP0/cvokhUlumRE3GRWGL4WRMLpcCIQDY+bwK384mZxyXGZ5lwNRTAPNzT8Fx +1+//nbaGK3BQMAB2AOg+0No+9QY1MudXKLyJa8kD08vREWvs62nhd31tBr1uAAAB +hXHHOfQAAAQDAEcwRQIgLoVydNfMFKV9IoZR+M0UuJ2zOqbxIRum7Sn9RMPOBGMC +IQD1/BgzCSDTvYvco6kpB6ifKSbg5gcb5KTnYxQYwRW14TANBgkqhkiG9w0BAQsF +AAOCAQEA2bQQu30e3OFu0bmvQHmcqYvXBu6tF6e5b5b+hj4O+Rn7BXTTmaYX3M6p +MsfRH4YVJJMB/dc3PROR2VtnKFC6gAZX+RKM6nXnZhIlOdmQnonS1ecOL19PliUd +VXbwKjXqAO0Ljd9y9oXaXnyPyHmUJNI5YXAcxE+XXiOZhcZuMYyWmoEKJQ/XlSga +zWfTn1IcKhA3IC7A1n/5bkkWD1Xi1mdWFQ6DQDMp//667zz7pKOgFMlB93aPDjvI +c78zEqNswn6xGKXpWF5xVwdFcsx9HKhJ6UAi2bQ/KQ1yb7LPUOR6wXXWrG1cLnNP +i8eNLnKL9PXQ+5SwJFCzfEhcIZuhzg== +-----END CERTIFICATE-----` + +// googleLeafWithInvalidHash is the same as googleLeaf, but the signature +// algorithm in the certificate contains a nonsense OID. +const googleLeafWithInvalidHash = `-----BEGIN CERTIFICATE----- +MIIFUjCCBDqgAwIBAgIQERmRWTzVoz0SMeozw2RM3DANBgkqhkiG9w0BAQ4FADBG +MQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExM +QzETMBEGA1UEAxMKR1RTIENBIDFDMzAeFw0yMzAxMDIwODE5MTlaFw0yMzAzMjcw +ODE5MThaMBkxFzAVBgNVBAMTDnd3dy5nb29nbGUuY29tMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEAq30odrKMT54TJikMKL8S+lwoCMT5geP0u9pWjk6a +wdB6i3kO+UE4ijCAmhbcZKeKaLnGJ38weZNwB1ayabCYyX7hDiC/nRcZU49LX5+o +55kDVaNn14YKkg2kCeX25HDxSwaOsNAIXKPTqiQL5LPvc4Twhl8HY51hhNWQrTEr +N775eYbixEULvyVLq5BLbCOpPo8n0/MTjQ32ku1jQq3GIYMJC/Rf2VW5doF6t9zs +KleflAN8OdKp0ME9OHg0T1P3yyb67T7n0SpisHbeG06AmQcKJF9g/9VPJtRf4l1Q +WRPDC+6JUqzXCxAGmIRGZ7TNMxPMBW/7DRX6w8oLKVNb0wIDAQABo4ICZzCCAmMw +DgYDVR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMAwGA1UdEwEB/wQC +MAAwHQYDVR0OBBYEFBnboj3lf9+Xat4oEgo6ZtIMr8ZuMB8GA1UdIwQYMBaAFIp0 +f6+Fze6VzT2c0OJGFPNxNR0nMGoGCCsGAQUFBwEBBF4wXDAnBggrBgEFBQcwAYYb +aHR0cDovL29jc3AucGtpLmdvb2cvZ3RzMWMzMDEGCCsGAQUFBzAChiVodHRwOi8v +cGtpLmdvb2cvcmVwby9jZXJ0cy9ndHMxYzMuZGVyMBkGA1UdEQQSMBCCDnd3dy5n +b29nbGUuY29tMCEGA1UdIAQaMBgwCAYGZ4EMAQIBMAwGCisGAQQB1nkCBQMwPAYD +VR0fBDUwMzAxoC+gLYYraHR0cDovL2NybHMucGtpLmdvb2cvZ3RzMWMzL1FPdkow +TjFzVDJBLmNybDCCAQQGCisGAQQB1nkCBAIEgfUEgfIA8AB2AHoyjFTYty22IOo4 +4FIe6YQWcDIThU070ivBOlejUutSAAABhXHHOiUAAAQDAEcwRQIgBUkikUIXdo+S +3T8PP0/cvokhUlumRE3GRWGL4WRMLpcCIQDY+bwK384mZxyXGZ5lwNRTAPNzT8Fx +1+//nbaGK3BQMAB2AOg+0No+9QY1MudXKLyJa8kD08vREWvs62nhd31tBr1uAAAB +hXHHOfQAAAQDAEcwRQIgLoVydNfMFKV9IoZR+M0UuJ2zOqbxIRum7Sn9RMPOBGMC +IQD1/BgzCSDTvYvco6kpB6ifKSbg5gcb5KTnYxQYwRW14TANBgkqhkiG9w0BAQ4F +AAOCAQEA2bQQu30e3OFu0bmvQHmcqYvXBu6tF6e5b5b+hj4O+Rn7BXTTmaYX3M6p +MsfRH4YVJJMB/dc3PROR2VtnKFC6gAZX+RKM6nXnZhIlOdmQnonS1ecOL19PliUd +VXbwKjXqAO0Ljd9y9oXaXnyPyHmUJNI5YXAcxE+XXiOZhcZuMYyWmoEKJQ/XlSga +zWfTn1IcKhA3IC7A1n/5bkkWD1Xi1mdWFQ6DQDMp//667zz7pKOgFMlB93aPDjvI +c78zEqNswn6xGKXpWF5xVwdFcsx9HKhJ6UAi2bQ/KQ1yb7LPUOR6wXXWrG1cLnNP +i8eNLnKL9PXQ+5SwJFCzfEhcIZuhzg== +-----END CERTIFICATE-----` + +const smimeLeaf = `-----BEGIN CERTIFICATE----- +MIIIPDCCBiSgAwIBAgIQaMDxFS0pOMxZZeOBxoTJtjANBgkqhkiG9w0BAQsFADCB +nTELMAkGA1UEBhMCRVMxFDASBgNVBAoMC0laRU5QRSBTLkEuMTowOAYDVQQLDDFB +WlogWml1cnRhZ2lyaSBwdWJsaWtvYSAtIENlcnRpZmljYWRvIHB1YmxpY28gU0NB +MTwwOgYDVQQDDDNFQUVrbyBIZXJyaSBBZG1pbmlzdHJhemlvZW4gQ0EgLSBDQSBB +QVBQIFZhc2NhcyAoMikwHhcNMTcwNzEyMDg1MzIxWhcNMjEwNzEyMDg1MzIxWjCC +AQwxDzANBgNVBAoMBklaRU5QRTE4MDYGA1UECwwvWml1cnRhZ2lyaSBrb3Jwb3Jh +dGlib2EtQ2VydGlmaWNhZG8gY29ycG9yYXRpdm8xQzBBBgNVBAsMOkNvbmRpY2lv +bmVzIGRlIHVzbyBlbiB3d3cuaXplbnBlLmNvbSBub2xhIGVyYWJpbGkgamFraXRl +a28xFzAVBgNVBC4TDi1kbmkgOTk5OTk5ODlaMSQwIgYDVQQDDBtDT1JQT1JBVElW +TyBGSUNUSUNJTyBBQ1RJVk8xFDASBgNVBCoMC0NPUlBPUkFUSVZPMREwDwYDVQQE +DAhGSUNUSUNJTzESMBAGA1UEBRMJOTk5OTk5ODlaMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAwVOMwUDfBtsH0XuxYnb+v/L774jMH8valX7RPH8cl2Lb +SiqSo0RchW2RGA2d1yuYHlpChC9jGmt0X/g66/E/+q2hUJlfJtqVDJFwtFYV4u2S +yzA3J36V4PRkPQrKxAsbzZriFXAF10XgiHQz9aVeMMJ9GBhmh9+DK8Tm4cMF6i8l ++AuC35KdngPF1x0ealTYrYZplpEJFO7CiW42aLi6vQkDR2R7nmZA4AT69teqBWsK +0DZ93/f0G/3+vnWwNTBF0lB6dIXoaz8OMSyHLqGnmmAtMrzbjAr/O/WWgbB/BqhR +qjJQ7Ui16cuDldXaWQ/rkMzsxmsAox0UF+zdQNvXUQIDAQABo4IDBDCCAwAwgccG +A1UdEgSBvzCBvIYVaHR0cDovL3d3dy5pemVucGUuY29tgQ9pbmZvQGl6ZW5wZS5j +b22kgZEwgY4xRzBFBgNVBAoMPklaRU5QRSBTLkEuIC0gQ0lGIEEwMTMzNzI2MC1S +TWVyYy5WaXRvcmlhLUdhc3RlaXogVDEwNTUgRjYyIFM4MUMwQQYDVQQJDDpBdmRh +IGRlbCBNZWRpdGVycmFuZW8gRXRvcmJpZGVhIDE0IC0gMDEwMTAgVml0b3JpYS1H +YXN0ZWl6MB4GA1UdEQQXMBWBE2ZpY3RpY2lvQGl6ZW5wZS5ldXMwDgYDVR0PAQH/ +BAQDAgXgMCkGA1UdJQQiMCAGCCsGAQUFBwMCBggrBgEFBQcDBAYKKwYBBAGCNxQC +AjAdBgNVHQ4EFgQUyeoOD4cgcljKY0JvrNuX2waFQLAwHwYDVR0jBBgwFoAUwKlK +90clh/+8taaJzoLSRqiJ66MwggEnBgNVHSAEggEeMIIBGjCCARYGCisGAQQB8zkB +AQEwggEGMDMGCCsGAQUFBwIBFidodHRwOi8vd3d3Lml6ZW5wZS5jb20vcnBhc2Nh +Y29ycG9yYXRpdm8wgc4GCCsGAQUFBwICMIHBGoG+Wml1cnRhZ2lyaWEgRXVza2Fs +IEF1dG9ub21pYSBFcmtpZGVnb2tvIHNla3RvcmUgcHVibGlrb2tvIGVyYWt1bmRl +ZW4gYmFybmUtc2FyZWV0YW4gYmFrYXJyaWsgZXJhYmlsIGRhaXRla2UuIFVzbyBy +ZXN0cmluZ2lkbyBhbCBhbWJpdG8gZGUgcmVkZXMgaW50ZXJuYXMgZGUgRW50aWRh +ZGVzIGRlbCBTZWN0b3IgUHVibGljbyBWYXNjbzAyBggrBgEFBQcBAQQmMCQwIgYI +KwYBBQUHMAGGFmh0dHA6Ly9vY3NwLml6ZW5wZS5jb20wOgYDVR0fBDMwMTAvoC2g +K4YpaHR0cDovL2NybC5pemVucGUuY29tL2NnaS1iaW4vY3JsaW50ZXJuYTIwDQYJ +KoZIhvcNAQELBQADggIBAIy5PQ+UZlCRq6ig43vpHwlwuD9daAYeejV0Q+ZbgWAE +GtO0kT/ytw95ZEJMNiMw3fYfPRlh27ThqiT0VDXZJDlzmn7JZd6QFcdXkCsiuv4+ +ZoXAg/QwnA3SGUUO9aVaXyuOIIuvOfb9MzoGp9xk23SMV3eiLAaLMLqwB5DTfBdt +BGI7L1MnGJBv8RfP/TL67aJ5bgq2ri4S8vGHtXSjcZ0+rCEOLJtmDNMnTZxancg3 +/H5edeNd+n6Z48LO+JHRxQufbC4mVNxVLMIP9EkGUejlq4E4w6zb5NwCQczJbSWL +i31rk2orsNsDlyaLGsWZp3JSNX6RmodU4KAUPor4jUJuUhrrm3Spb73gKlV/gcIw +bCE7mML1Kss3x1ySaXsis6SZtLpGWKkW2iguPWPs0ydV6RPhmsCxieMwPPIJ87vS +5IejfgyBae7RSuAIHyNFy4uI5xwvwUFf6OZ7az8qtW7ImFOgng3Ds+W9k1S2CNTx +d0cnKTfA6IpjGo8EeHcxnIXT8NPImWaRj0qqonvYady7ci6U4m3lkNSdXNn1afgw +mYust+gxVtOZs1gk2MUCgJ1V1X+g7r/Cg7viIn6TLkLrpS1kS1hvMqkl9M+7XqPo +Qd95nJKOkusQpy99X4dF/lfbYAQnnjnqh3DLD2gvYObXFaAYFaiBKTiMTV2X72F+ +-----END CERTIFICATE-----` + +const smimeIntermediate = `-----BEGIN CERTIFICATE----- +MIIHNzCCBSGgAwIBAgIQJMXIqlZvjuhMvqcFXOFkpDALBgkqhkiG9w0BAQswODEL +MAkGA1UEBhMCRVMxFDASBgNVBAoMC0laRU5QRSBTLkEuMRMwEQYDVQQDDApJemVu +cGUuY29tMB4XDTEwMTAyMDA4MjMzM1oXDTM3MTIxMjIzMDAwMFowgZ0xCzAJBgNV +BAYTAkVTMRQwEgYDVQQKDAtJWkVOUEUgUy5BLjE6MDgGA1UECwwxQVpaIFppdXJ0 +YWdpcmkgcHVibGlrb2EgLSBDZXJ0aWZpY2FkbyBwdWJsaWNvIFNDQTE8MDoGA1UE +AwwzRUFFa28gSGVycmkgQWRtaW5pc3RyYXppb2VuIENBIC0gQ0EgQUFQUCBWYXNj +YXMgKDIpMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAoIM7nEdI0N1h +rR5T4xuV/usKDoMIasaiKvfLhbwxaNtTt+a7W/6wV5bv3svQFIy3sUXjjdzV1nG2 +To2wo/YSPQiOt8exWvOapvL21ogiof+kelWnXFjWaKJI/vThHYLgIYEMj/y4HdtU +ojI646rZwqsb4YGAopwgmkDfUh5jOhV2IcYE3TgJAYWVkj6jku9PLaIsHiarAHjD +PY8dig8a4SRv0gm5Yk7FXLmW1d14oxQBDeHZ7zOEXfpafxdEDO2SNaRJjpkh8XRr +PGqkg2y1Q3gT6b4537jz+StyDIJ3omylmlJsGCwqT7p8mEqjGJ5kC5I2VnjXKuNn +soShc72khWZVUJiJo5SGuAkNE2ZXqltBVm5Jv6QweQKsX6bkcMc4IZok4a+hx8FM +8IBpGf/I94pU6HzGXqCyc1d46drJgDY9mXa+6YDAJFl3xeXOOW2iGCfwXqhiCrKL +MYvyMZzqF3QH5q4nb3ZnehYvraeMFXJXDn+Utqp8vd2r7ShfQJz01KtM4hgKdgSg +jtW+shkVVN5ng/fPN85ovfAH2BHXFfHmQn4zKsYnLitpwYM/7S1HxlT61cdQ7Nnk +3LZTYEgAoOmEmdheklT40WAYakksXGM5VrzG7x9S7s1Tm+Vb5LSThdHC8bxxwyTb +KsDRDNJ84N9fPDO6qHnzaL2upQ43PycCAwEAAaOCAdkwggHVMIHHBgNVHREEgb8w +gbyGFWh0dHA6Ly93d3cuaXplbnBlLmNvbYEPaW5mb0BpemVucGUuY29tpIGRMIGO +MUcwRQYDVQQKDD5JWkVOUEUgUy5BLiAtIENJRiBBMDEzMzcyNjAtUk1lcmMuVml0 +b3JpYS1HYXN0ZWl6IFQxMDU1IEY2MiBTODFDMEEGA1UECQw6QXZkYSBkZWwgTWVk +aXRlcnJhbmVvIEV0b3JiaWRlYSAxNCAtIDAxMDEwIFZpdG9yaWEtR2FzdGVpejAP +BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUwKlK90cl +h/+8taaJzoLSRqiJ66MwHwYDVR0jBBgwFoAUHRxlDqjyJXu0kc/ksbHmvVV0bAUw +OgYDVR0gBDMwMTAvBgRVHSAAMCcwJQYIKwYBBQUHAgEWGWh0dHA6Ly93d3cuaXpl +bnBlLmNvbS9jcHMwNwYIKwYBBQUHAQEEKzApMCcGCCsGAQUFBzABhhtodHRwOi8v +b2NzcC5pemVucGUuY29tOjgwOTQwMwYDVR0fBCwwKjAooCagJIYiaHR0cDovL2Ny +bC5pemVucGUuY29tL2NnaS1iaW4vYXJsMjALBgkqhkiG9w0BAQsDggIBAMbjc3HM +3DG9ubWPkzsF0QsktukpujbTTcGk4h20G7SPRy1DiiTxrRzdAMWGjZioOP3/fKCS +M539qH0M+gsySNie+iKlbSZJUyE635T1tKw+G7bDUapjlH1xyv55NC5I6wCXGC6E +3TEP5B/E7dZD0s9E4lS511ubVZivFgOzMYo1DO96diny/N/V1enaTCpRl1qH1OyL +xUYTijV4ph2gL6exwuG7pxfRcVNHYlrRaXWfTz3F6NBKyULxrI3P/y6JAtN1GqT4 +VF/+vMygx22n0DufGepBwTQz6/rr1ulSZ+eMnuJiTXgh/BzQnkUsXTb8mHII25iR +0oYF2qAsk6ecWbLiDpkHKIDHmML21MZE13MS8NSvTHoqJO4LyAmDe6SaeNHtrPlK +b6mzE1BN2ug+ZaX8wLA5IMPFaf0jKhb/Cxu8INsxjt00brsErCc9ip1VNaH0M4bi +1tGxfiew2436FaeyUxW7Pl6G5GgkNbuUc7QIoRy06DdU/U38BxW3uyJMY60zwHvS +FlKAn0OvYp4niKhAJwaBVN3kowmJuOU5Rid+TUnfyxbJ9cttSgzaF3hP/N4zgMEM +5tikXUskeckt8LUK96EH0QyssavAMECUEb/xrupyRdYWwjQGvNLq6T5+fViDGyOw +k+lzD44wofy8paAy9uC9Owae0zMEzhcsyRm7 +-----END CERTIFICATE-----` + +const smimeRoot = `-----BEGIN CERTIFICATE----- +MIIF8TCCA9mgAwIBAgIQALC3WhZIX7/hy/WL1xnmfTANBgkqhkiG9w0BAQsFADA4 +MQswCQYDVQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6 +ZW5wZS5jb20wHhcNMDcxMjEzMTMwODI4WhcNMzcxMjEzMDgyNzI1WjA4MQswCQYD +VQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6ZW5wZS5j +b20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ03rKDx6sp4boFmVq +scIbRTJxldn+EFvMr+eleQGPicPK8lVx93e+d5TzcqQsRNiekpsUOqHnJJAKClaO +xdgmlOHZSOEtPtoKct2jmRXagaKH9HtuJneJWK3W6wyyQXpzbm3benhB6QiIEn6H +LmYRY2xU+zydcsC8Lv/Ct90NduM61/e0aL6i9eOBbsFGb12N4E3GVFWJGjMxCrFX +uaOKmMPsOzTFlUFpfnXCPCDFYbpRR6AgkJOhkEvzTnyFRVSa0QUmQbC1TR0zvsQD +yCV8wXDbO/QJLVQnSKwv4cSsPsjLkkxTOTcj7NMB+eAJRE1NZMDhDVqHIrytG6P+ +JrUV86f8hBnp7KGItERphIPzidF0BqnMC9bC3ieFUCbKF7jJeodWLBoBHmy+E60Q +rLUk9TiRodZL2vG70t5HtfG8gfZZa88ZU+mNFctKy6lvROUbQc/hhqfK0GqfvEyN +BjNaooXlkDWgYlwWTvDjovoDGrQscbNYLN57C9saD+veIR8GdwYDsMnvmfzAuU8L +hij+0rnq49qlw0dpEuDb8PYZi+17cNcC1u2HGCgsBCRMd+RIihrGO5rUD8r6ddIB +QFqNeb+Lz0vPqhbBleStTIo+F5HUsWLlguWABKQDfo2/2n+iD5dPDNMN+9fR5XJ+ +HMh3/1uaD7euBUbl8agW7EekFwIDAQABo4H2MIHzMIGwBgNVHREEgagwgaWBD2lu +Zm9AaXplbnBlLmNvbaSBkTCBjjFHMEUGA1UECgw+SVpFTlBFIFMuQS4gLSBDSUYg +QTAxMzM3MjYwLVJNZXJjLlZpdG9yaWEtR2FzdGVpeiBUMTA1NSBGNjIgUzgxQzBB +BgNVBAkMOkF2ZGEgZGVsIE1lZGl0ZXJyYW5lbyBFdG9yYmlkZWEgMTQgLSAwMTAx +MCBWaXRvcmlhLUdhc3RlaXowDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AQYwHQYDVR0OBBYEFB0cZQ6o8iV7tJHP5LGx5r1VdGwFMA0GCSqGSIb3DQEBCwUA +A4ICAQB4pgwWSp9MiDrAyw6lFn2fuUhfGI8NYjb2zRlrrKvV9pF9rnHzP7MOeIWb +laQnIUdCSnxIOvVFfLMMjlF4rJUT3sb9fbgakEyrkgPH7UIBzg/YsfqikuFgba56 +awmqxinuaElnMIAkejEWOVt+8Rwu3WwJrfIxwYJOubv5vr8qhT/AQKM6WfxZSzwo +JNu0FXWuDYi6LnPAvViH5ULy617uHjAimcs30cQhbIHsvm0m5hzkQiCeR7Csg1lw +LDXWrzY0tM07+DKo7+N4ifuNRSzanLh+QBxh5z6ikixL8s36mLYp//Pye6kfLqCT +VyvehQP5aTfLnnhqBbTFMXiJ7HqnheG5ezzevh55hM6fcA5ZwjUukCox2eRFekGk +LhObNA5me0mrZJfQRsN5nXJQY6aYWwa9SG3YOYNw6DXwBdGqvOPbyALqfP2C2sJb +UjWumDqtujWTI6cfSN01RpiyEGjkpTHCClguGYEQyVB1/OpaFs4R1+7vUIgtYf8/ +QnMFlEPVjjxOAToZpR9GTnfQXeWBIiGH/pR9hNiTrdZoQ0iy2+tzJOeRf1SktoA+ +naM8THLCV8Sg1Mw4J87VBp6iSNnpn86CcDaTmjvfliHjWbcM2pE38P1ZWrOZyGls +QyYBNWNgVYkDOnXYukrZVP/u3oDYLdE41V4tC5h9Pmzb/CaIxw== +-----END CERTIFICATE-----` diff --git a/component/ca/keypair.go b/component/ca/keypair.go index 7fa6c21feb..cefd0cf667 100644 --- a/component/ca/keypair.go +++ b/component/ca/keypair.go @@ -12,6 +12,8 @@ import ( "encoding/pem" "fmt" "math/big" + "os" + "time" ) type Path interface { @@ -56,6 +58,33 @@ func LoadTLSKeyPair(certificate, privateKey string, path Path) (tls.Certificate, return cert, nil } +func LoadCertificates(certificate string, path Path) (*x509.CertPool, error) { + pool := x509.NewCertPool() + if pool.AppendCertsFromPEM([]byte(certificate)) { + return pool, nil + } + painTextErr := fmt.Errorf("invalid certificate: %s", certificate) + if path == nil { + return nil, painTextErr + } + + certificate = path.Resolve(certificate) + var loadErr error + if !path.IsSafePath(certificate) { + loadErr = path.ErrNotSafePath(certificate) + } else { + certPEMBlock, err := os.ReadFile(certificate) + if pool.AppendCertsFromPEM(certPEMBlock) { + return pool, nil + } + loadErr = err + } + if loadErr != nil { + return nil, fmt.Errorf("parse certificate failed, maybe format error:%s, or path error: %s", painTextErr.Error(), loadErr.Error()) + } + return pool, nil +} + type KeyPairType string const ( @@ -85,7 +114,11 @@ func NewRandomTLSKeyPair(keyPairType KeyPairType) (certificate string, privateKe return } - template := x509.Certificate{SerialNumber: big.NewInt(1)} + template := x509.Certificate{ + SerialNumber: big.NewInt(1), + NotBefore: time.Now().Add(-time.Hour * 24 * 365), + NotAfter: time.Now().Add(time.Hour * 24 * 365), + } certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, key.Public(), key) if err != nil { return diff --git a/component/dialer/bind_darwin.go b/component/dialer/bind_darwin.go index fdea24bfdc..159d3d270a 100644 --- a/component/dialer/bind_darwin.go +++ b/component/dialer/bind_darwin.go @@ -21,10 +21,10 @@ func bindControl(ifaceIdx int) controlFn { var innerErr error err = c.Control(func(fd uintptr) { switch network { - case "tcp4", "udp4": - innerErr = unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_BOUND_IF, ifaceIdx) - case "tcp6", "udp6": + case "tcp6", "udp6", "ip6": innerErr = unix.SetsockoptInt(int(fd), unix.IPPROTO_IPV6, unix.IPV6_BOUND_IF, ifaceIdx) + default: + innerErr = unix.SetsockoptInt(int(fd), unix.IPPROTO_IP, unix.IP_BOUND_IF, ifaceIdx) } }) diff --git a/component/dialer/dialer.go b/component/dialer/dialer.go index 4402f77459..d490dca3de 100644 --- a/component/dialer/dialer.go +++ b/component/dialer/dialer.go @@ -9,6 +9,7 @@ import ( "os" "strings" "sync" + "syscall" "time" "github.com/metacubex/mihomo/component/keepalive" @@ -177,6 +178,34 @@ func dialContext(ctx context.Context, network string, destination netip.Addr, po return dialer.DialContext(ctx, network, address) } +func ICMPControl(destination netip.Addr) func(network, address string, conn syscall.RawConn) error { + return func(network, address string, conn syscall.RawConn) error { + if DefaultSocketHook != nil { + return DefaultSocketHook(network, address, conn) + } + dialer := &net.Dialer{} + interfaceName := DefaultInterface.Load() + if interfaceName == "" { + if finder := DefaultInterfaceFinder.Load(); finder != nil { + interfaceName = finder.FindInterfaceName(destination) + } + } + if interfaceName != "" { + if err := bindIfaceToDialer(interfaceName, dialer, network, destination); err != nil { + return err + } + } + routingMark := int(DefaultRoutingMark.Load()) + if routingMark != 0 { + bindMarkToDialer(routingMark, dialer, network, destination) + } + if dialer.ControlContext != nil { + return dialer.ControlContext(context.TODO(), network, address, conn) + } + return nil + } +} + func serialSingleStackDialContext(ctx context.Context, network string, ips []netip.Addr, port string, opt option) (net.Conn, error) { return serialDialContext(ctx, network, ips, port, opt) } diff --git a/component/http/http.go b/component/http/http.go index a2c44d85c3..1683c4da38 100644 --- a/component/http/http.go +++ b/component/http/http.go @@ -2,7 +2,6 @@ package http import ( "context" - "crypto/tls" "io" "net" "net/http" @@ -28,11 +27,11 @@ func SetUA(UA string) { ua = UA } -func HttpRequest(ctx context.Context, url, method string, header map[string][]string, body io.Reader) (*http.Response, error) { - return HttpRequestWithProxy(ctx, url, method, header, body, "") -} - -func HttpRequestWithProxy(ctx context.Context, url, method string, header map[string][]string, body io.Reader, specialProxy string) (*http.Response, error) { +func HttpRequest(ctx context.Context, url, method string, header map[string][]string, body io.Reader, options ...Option) (*http.Response, error) { + opt := option{} + for _, o := range options { + o(&opt) + } method = strings.ToUpper(method) urlRes, err := URL.Parse(url) if err != nil { @@ -40,6 +39,10 @@ func HttpRequestWithProxy(ctx context.Context, url, method string, header map[st } req, err := http.NewRequest(method, urlRes.String(), body) + if err != nil { + return nil, err + } + for k, v := range header { for _, v := range v { req.Header.Add(k, v) @@ -50,10 +53,6 @@ func HttpRequestWithProxy(ctx context.Context, url, method string, header map[st req.Header.Set("User-Agent", UA()) } - if err != nil { - return nil, err - } - if user := urlRes.User; user != nil { password, _ := user.Password() req.SetBasicAuth(user.Username(), password) @@ -61,6 +60,11 @@ func HttpRequestWithProxy(ctx context.Context, url, method string, header map[st req = req.WithContext(ctx) + tlsConfig, err := ca.GetTLSConfig(opt.caOption) + if err != nil { + return nil, err + } + transport := &http.Transport{ // from http.DefaultTransport DisableKeepAlives: runtime.GOOS == "android", @@ -69,15 +73,34 @@ func HttpRequestWithProxy(ctx context.Context, url, method string, header map[st TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, DialContext: func(ctx context.Context, network, address string) (net.Conn, error) { - if conn, err := inner.HandleTcp(inner.GetTunnel(), address, specialProxy); err == nil { + if conn, err := inner.HandleTcp(inner.GetTunnel(), address, opt.specialProxy); err == nil { return conn, nil } else { return dialer.DialContext(ctx, network, address) } }, - TLSClientConfig: ca.GetGlobalTLSConfig(&tls.Config{}), + TLSClientConfig: tlsConfig, } client := http.Client{Transport: transport} return client.Do(req) } + +type Option func(opt *option) + +type option struct { + specialProxy string + caOption ca.Option +} + +func WithSpecialProxy(name string) Option { + return func(opt *option) { + opt.specialProxy = name + } +} + +func WithCAOption(caOption ca.Option) Option { + return func(opt *option) { + opt.caOption = caOption + } +} diff --git a/component/memory/memory.go b/component/memory/memory.go new file mode 100644 index 0000000000..0269c37b1f --- /dev/null +++ b/component/memory/memory.go @@ -0,0 +1,29 @@ +// Package memory return MemoryInfoStat +// modify from https://github.com/shirou/gopsutil/tree/v4.25.8/process +package memory + +import ( + "errors" + "fmt" + "math" +) + +var ErrNotImplementedError = errors.New("not implemented yet") + +type MemoryInfoStat struct { + RSS uint64 `json:"rss"` // bytes + VMS uint64 `json:"vms"` // bytes +} + +// PrettyByteSize convert size in bytes to Bytes, Kilobytes, Megabytes, GB and TB +// https://gist.github.com/anikitenko/b41206a49727b83a530142c76b1cb82d?permalink_comment_id=4467913#gistcomment-4467913 +func PrettyByteSize(b uint64) string { + bf := float64(b) + for _, unit := range []string{"", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"} { + if math.Abs(bf) < 1024.0 { + return fmt.Sprintf("%3.1f%sB", bf, unit) + } + bf /= 1024.0 + } + return fmt.Sprintf("%.1fYiB", bf) +} diff --git a/component/memory/memory_darwin.go b/component/memory/memory_darwin.go new file mode 100644 index 0000000000..1dd33af32a --- /dev/null +++ b/component/memory/memory_darwin.go @@ -0,0 +1,56 @@ +package memory + +import ( + "unsafe" + + "github.com/ebitengine/purego" +) + +const PROC_PIDTASKINFO = 4 + +type ProcTaskInfo struct { + Virtual_size uint64 + Resident_size uint64 + Total_user uint64 + Total_system uint64 + Threads_user uint64 + Threads_system uint64 + Policy int32 + Faults int32 + Pageins int32 + Cow_faults int32 + Messages_sent int32 + Messages_received int32 + Syscalls_mach int32 + Syscalls_unix int32 + Csw int32 + Threadnum int32 + Numrunning int32 + Priority int32 +} + +const System = "/usr/lib/libSystem.B.dylib" + +type ProcPidInfoFunc func(pid, flavor int32, arg uint64, buffer uintptr, bufferSize int32) int32 + +const ProcPidInfoSym = "proc_pidinfo" + +func GetMemoryInfo(pid int32) (*MemoryInfoStat, error) { + lib, err := purego.Dlopen(System, purego.RTLD_LAZY|purego.RTLD_GLOBAL) + if err != nil { + return nil, err + } + defer purego.Dlclose(lib) + + var procPidInfo ProcPidInfoFunc + purego.RegisterLibFunc(&procPidInfo, lib, ProcPidInfoSym) + + var ti ProcTaskInfo + procPidInfo(pid, PROC_PIDTASKINFO, 0, uintptr(unsafe.Pointer(&ti)), int32(unsafe.Sizeof(ti))) + + ret := &MemoryInfoStat{ + RSS: uint64(ti.Resident_size), + VMS: uint64(ti.Virtual_size), + } + return ret, nil +} diff --git a/component/memory/memory_falllback.go b/component/memory/memory_falllback.go new file mode 100644 index 0000000000..918f5b0ec9 --- /dev/null +++ b/component/memory/memory_falllback.go @@ -0,0 +1,7 @@ +//go:build !darwin && !linux && !freebsd && !openbsd && !windows + +package memory + +func GetMemoryInfo(pid int32) (*MemoryInfoStat, error) { + return nil, ErrNotImplementedError +} diff --git a/component/memory/memory_freebsd.go b/component/memory/memory_freebsd.go new file mode 100644 index 0000000000..87166b8493 --- /dev/null +++ b/component/memory/memory_freebsd.go @@ -0,0 +1,97 @@ +package memory + +import ( + "bytes" + "encoding/binary" + "errors" + "unsafe" + + "golang.org/x/sys/unix" +) + +const ( + CTLKern = 1 + KernProc = 14 + KernProcPID = 1 +) + +func CallSyscall(mib []int32) ([]byte, uint64, error) { + mibptr := unsafe.Pointer(&mib[0]) + miblen := uint64(len(mib)) + + // get required buffer size + length := uint64(0) + _, _, err := unix.Syscall6( + unix.SYS___SYSCTL, + uintptr(mibptr), + uintptr(miblen), + 0, + uintptr(unsafe.Pointer(&length)), + 0, + 0) + if err != 0 { + var b []byte + return b, length, err + } + if length == 0 { + var b []byte + return b, length, err + } + // get proc info itself + buf := make([]byte, length) + _, _, err = unix.Syscall6( + unix.SYS___SYSCTL, + uintptr(mibptr), + uintptr(miblen), + uintptr(unsafe.Pointer(&buf[0])), + uintptr(unsafe.Pointer(&length)), + 0, + 0) + if err != 0 { + return buf, length, err + } + + return buf, length, nil +} + +func parseKinfoProc(buf []byte) (KinfoProc, error) { + var k KinfoProc + br := bytes.NewReader(buf) + err := binary.Read(br, binary.LittleEndian, &k) + return k, err +} + +func getKProc(pid int32) (*KinfoProc, error) { + mib := []int32{CTLKern, KernProc, KernProcPID, pid} + + buf, length, err := CallSyscall(mib) + if err != nil { + return nil, err + } + if length != sizeOfKinfoProc { + return nil, errors.New("unexpected size of KinfoProc") + } + + k, err := parseKinfoProc(buf) + if err != nil { + return nil, err + } + return &k, nil +} + +func GetMemoryInfo(pid int32) (*MemoryInfoStat, error) { + k, err := getKProc(pid) + if err != nil { + return nil, err + } + v, err := unix.Sysctl("vm.stats.vm.v_page_size") + if err != nil { + return nil, err + } + pageSize := binary.LittleEndian.Uint16([]byte(v)) + + return &MemoryInfoStat{ + RSS: uint64(k.Rssize) * uint64(pageSize), + VMS: uint64(k.Size), + }, nil +} diff --git a/component/memory/memory_freebsd_386.go b/component/memory/memory_freebsd_386.go new file mode 100644 index 0000000000..85e79c70f2 --- /dev/null +++ b/component/memory/memory_freebsd_386.go @@ -0,0 +1,119 @@ +package memory + +const sizeOfKinfoProc = 0x300 + +type Timeval struct { + Sec int32 + Usec int32 +} + +type Rusage struct { + Utime Timeval + Stime Timeval + Maxrss int32 + Ixrss int32 + Idrss int32 + Isrss int32 + Minflt int32 + Majflt int32 + Nswap int32 + Inblock int32 + Oublock int32 + Msgsnd int32 + Msgrcv int32 + Nsignals int32 + Nvcsw int32 + Nivcsw int32 +} + +type KinfoProc struct { + Structsize int32 + Layout int32 + Args int32 /* pargs */ + Paddr int32 /* proc */ + Addr int32 /* user */ + Tracep int32 /* vnode */ + Textvp int32 /* vnode */ + Fd int32 /* filedesc */ + Vmspace int32 /* vmspace */ + Wchan int32 + Pid int32 + Ppid int32 + Pgid int32 + Tpgid int32 + Sid int32 + Tsid int32 + Jobc int16 + Spare_short1 int16 + Tdev uint32 + Siglist [16]byte /* sigset */ + Sigmask [16]byte /* sigset */ + Sigignore [16]byte /* sigset */ + Sigcatch [16]byte /* sigset */ + Uid uint32 + Ruid uint32 + Svuid uint32 + Rgid uint32 + Svgid uint32 + Ngroups int16 + Spare_short2 int16 + Groups [16]uint32 + Size uint32 + Rssize int32 + Swrss int32 + Tsize int32 + Dsize int32 + Ssize int32 + Xstat uint16 + Acflag uint16 + Pctcpu uint32 + Estcpu uint32 + Slptime uint32 + Swtime uint32 + Cow uint32 + Runtime uint64 + Start Timeval + Childtime Timeval + Flag int32 + Kiflag int32 + Traceflag int32 + Stat int8 + Nice int8 + Lock int8 + Rqindex int8 + Oncpu uint8 + Lastcpu uint8 + Tdname [17]int8 + Wmesg [9]int8 + Login [18]int8 + Lockname [9]int8 + Comm [20]int8 + Emul [17]int8 + Loginclass [18]int8 + Sparestrings [50]int8 + Spareints [7]int32 + Flag2 int32 + Fibnum int32 + Cr_flags uint32 + Jid int32 + Numthreads int32 + Tid int32 + Pri Priority + Rusage Rusage + Rusage_ch Rusage + Pcb int32 /* pcb */ + Kstack int32 + Udata int32 + Tdaddr int32 /* thread */ + Spareptrs [6]int32 + Sparelongs [12]int32 + Sflag int32 + Tdflags int32 +} + +type Priority struct { + Class uint8 + Level uint8 + Native uint8 + User uint8 +} diff --git a/component/memory/memory_freebsd_amd64.go b/component/memory/memory_freebsd_amd64.go new file mode 100644 index 0000000000..2cebd34c2b --- /dev/null +++ b/component/memory/memory_freebsd_amd64.go @@ -0,0 +1,125 @@ +package memory + +const sizeOfKinfoProc = 0x440 + +type Timeval struct { + Sec int64 + Usec int64 +} + +type Rusage struct { + Utime Timeval + Stime Timeval + Maxrss int64 + Ixrss int64 + Idrss int64 + Isrss int64 + Minflt int64 + Majflt int64 + Nswap int64 + Inblock int64 + Oublock int64 + Msgsnd int64 + Msgrcv int64 + Nsignals int64 + Nvcsw int64 + Nivcsw int64 +} + +type KinfoProc struct { + Structsize int32 + Layout int32 + Args int64 /* pargs */ + Paddr int64 /* proc */ + Addr int64 /* user */ + Tracep int64 /* vnode */ + Textvp int64 /* vnode */ + Fd int64 /* filedesc */ + Vmspace int64 /* vmspace */ + Wchan int64 + Pid int32 + Ppid int32 + Pgid int32 + Tpgid int32 + Sid int32 + Tsid int32 + Jobc int16 + Spare_short1 int16 + Tdev_freebsd11 uint32 + Siglist [16]byte /* sigset */ + Sigmask [16]byte /* sigset */ + Sigignore [16]byte /* sigset */ + Sigcatch [16]byte /* sigset */ + Uid uint32 + Ruid uint32 + Svuid uint32 + Rgid uint32 + Svgid uint32 + Ngroups int16 + Spare_short2 int16 + Groups [16]uint32 + Size uint64 + Rssize int64 + Swrss int64 + Tsize int64 + Dsize int64 + Ssize int64 + Xstat uint16 + Acflag uint16 + Pctcpu uint32 + Estcpu uint32 + Slptime uint32 + Swtime uint32 + Cow uint32 + Runtime uint64 + Start Timeval + Childtime Timeval + Flag int64 + Kiflag int64 + Traceflag int32 + Stat int8 + Nice int8 + Lock int8 + Rqindex int8 + Oncpu_old uint8 + Lastcpu_old uint8 + Tdname [17]int8 + Wmesg [9]int8 + Login [18]int8 + Lockname [9]int8 + Comm [20]int8 + Emul [17]int8 + Loginclass [18]int8 + Moretdname [4]int8 + Sparestrings [46]int8 + Spareints [2]int32 + Tdev uint64 + Oncpu int32 + Lastcpu int32 + Tracer int32 + Flag2 int32 + Fibnum int32 + Cr_flags uint32 + Jid int32 + Numthreads int32 + Tid int32 + Pri Priority + Rusage Rusage + Rusage_ch Rusage + Pcb int64 /* pcb */ + Kstack int64 + Udata int64 + Tdaddr int64 /* thread */ + Pd int64 /* pwddesc, not accurate */ + Spareptrs [5]int64 + Sparelongs [12]int64 + Sflag int64 + Tdflags int64 +} + +type Priority struct { + Class uint8 + Level uint8 + Native uint8 + User uint8 +} diff --git a/component/memory/memory_freebsd_arm.go b/component/memory/memory_freebsd_arm.go new file mode 100644 index 0000000000..9c9c220024 --- /dev/null +++ b/component/memory/memory_freebsd_arm.go @@ -0,0 +1,119 @@ +package memory + +const sizeOfKinfoProc = 0x440 + +type Timeval struct { + Sec int64 + Usec int64 +} + +type Rusage struct { + Utime Timeval + Stime Timeval + Maxrss int32 + Ixrss int32 + Idrss int32 + Isrss int32 + Minflt int32 + Majflt int32 + Nswap int32 + Inblock int32 + Oublock int32 + Msgsnd int32 + Msgrcv int32 + Nsignals int32 + Nvcsw int32 + Nivcsw int32 +} + +type KinfoProc struct { + Structsize int32 + Layout int32 + Args int32 /* pargs */ + Paddr int32 /* proc */ + Addr int32 /* user */ + Tracep int32 /* vnode */ + Textvp int32 /* vnode */ + Fd int32 /* filedesc */ + Vmspace int32 /* vmspace */ + Wchan int32 + Pid int32 + Ppid int32 + Pgid int32 + Tpgid int32 + Sid int32 + Tsid int32 + Jobc int16 + Spare_short1 int16 + Tdev uint32 + Siglist [16]byte /* sigset */ + Sigmask [16]byte /* sigset */ + Sigignore [16]byte /* sigset */ + Sigcatch [16]byte /* sigset */ + Uid uint32 + Ruid uint32 + Svuid uint32 + Rgid uint32 + Svgid uint32 + Ngroups int16 + Spare_short2 int16 + Groups [16]uint32 + Size uint32 + Rssize int32 + Swrss int32 + Tsize int32 + Dsize int32 + Ssize int32 + Xstat uint16 + Acflag uint16 + Pctcpu uint32 + Estcpu uint32 + Slptime uint32 + Swtime uint32 + Cow uint32 + Runtime uint64 + Start Timeval + Childtime Timeval + Flag int32 + Kiflag int32 + Traceflag int32 + Stat int8 + Nice int8 + Lock int8 + Rqindex int8 + Oncpu uint8 + Lastcpu uint8 + Tdname [17]int8 + Wmesg [9]int8 + Login [18]int8 + Lockname [9]int8 + Comm [20]int8 + Emul [17]int8 + Loginclass [18]int8 + Sparestrings [50]int8 + Spareints [4]int32 + Flag2 int32 + Fibnum int32 + Cr_flags uint32 + Jid int32 + Numthreads int32 + Tid int32 + Pri Priority + Rusage Rusage + Rusage_ch Rusage + Pcb int32 /* pcb */ + Kstack int32 + Udata int32 + Tdaddr int32 /* thread */ + Spareptrs [6]int64 + Sparelongs [12]int64 + Sflag int64 + Tdflags int64 +} + +type Priority struct { + Class uint8 + Level uint8 + Native uint8 + User uint8 +} diff --git a/component/memory/memory_freebsd_arm64.go b/component/memory/memory_freebsd_arm64.go new file mode 100644 index 0000000000..4e228c9211 --- /dev/null +++ b/component/memory/memory_freebsd_arm64.go @@ -0,0 +1,125 @@ +package memory + +const sizeOfKinfoProc = 0x440 + +type Timeval struct { + Sec int64 + Usec int64 +} + +type Rusage struct { + Utime Timeval + Stime Timeval + Maxrss int64 + Ixrss int64 + Idrss int64 + Isrss int64 + Minflt int64 + Majflt int64 + Nswap int64 + Inblock int64 + Oublock int64 + Msgsnd int64 + Msgrcv int64 + Nsignals int64 + Nvcsw int64 + Nivcsw int64 +} + +type KinfoProc struct { + Structsize int32 + Layout int32 + Args int64 /* pargs */ + Paddr int64 /* proc */ + Addr int64 /* user */ + Tracep int64 /* vnode */ + Textvp int64 /* vnode */ + Fd int64 /* filedesc */ + Vmspace int64 /* vmspace */ + Wchan int64 + Pid int32 + Ppid int32 + Pgid int32 + Tpgid int32 + Sid int32 + Tsid int32 + Jobc int16 + Spare_short1 int16 + Tdev_freebsd11 uint32 + Siglist [16]byte /* sigset */ + Sigmask [16]byte /* sigset */ + Sigignore [16]byte /* sigset */ + Sigcatch [16]byte /* sigset */ + Uid uint32 + Ruid uint32 + Svuid uint32 + Rgid uint32 + Svgid uint32 + Ngroups int16 + Spare_short2 int16 + Groups [16]uint32 + Size uint64 + Rssize int64 + Swrss int64 + Tsize int64 + Dsize int64 + Ssize int64 + Xstat uint16 + Acflag uint16 + Pctcpu uint32 + Estcpu uint32 + Slptime uint32 + Swtime uint32 + Cow uint32 + Runtime uint64 + Start Timeval + Childtime Timeval + Flag int64 + Kiflag int64 + Traceflag int32 + Stat uint8 + Nice int8 + Lock uint8 + Rqindex uint8 + Oncpu_old uint8 + Lastcpu_old uint8 + Tdname [17]uint8 + Wmesg [9]uint8 + Login [18]uint8 + Lockname [9]uint8 + Comm [20]int8 // changed from uint8 by hand + Emul [17]uint8 + Loginclass [18]uint8 + Moretdname [4]uint8 + Sparestrings [46]uint8 + Spareints [2]int32 + Tdev uint64 + Oncpu int32 + Lastcpu int32 + Tracer int32 + Flag2 int32 + Fibnum int32 + Cr_flags uint32 + Jid int32 + Numthreads int32 + Tid int32 + Pri Priority + Rusage Rusage + Rusage_ch Rusage + Pcb int64 /* pcb */ + Kstack int64 + Udata int64 + Tdaddr int64 /* thread */ + Pd int64 /* pwddesc, not accurate */ + Spareptrs [5]int64 + Sparelongs [12]int64 + Sflag int64 + Tdflags int64 +} + +type Priority struct { + Class uint8 + Level uint8 + Native uint8 + User uint8 +} diff --git a/component/memory/memory_linux.go b/component/memory/memory_linux.go new file mode 100644 index 0000000000..f79bc28fcb --- /dev/null +++ b/component/memory/memory_linux.go @@ -0,0 +1,37 @@ +package memory + +import ( + "os" + "path/filepath" + "strconv" + "strings" +) + +var pageSize = uint64(os.Getpagesize()) + +func GetMemoryInfo(pid int32) (*MemoryInfoStat, error) { + proc := os.Getenv("HOST_PROC") + if proc == "" { + proc = "/proc" + } + memPath := filepath.Join(proc, strconv.Itoa(int(pid)), "statm") + contents, err := os.ReadFile(memPath) + if err != nil { + return nil, err + } + fields := strings.Split(string(contents), " ") + + vms, err := strconv.ParseUint(fields[0], 10, 64) + if err != nil { + return nil, err + } + rss, err := strconv.ParseUint(fields[1], 10, 64) + if err != nil { + return nil, err + } + memInfo := &MemoryInfoStat{ + RSS: rss * pageSize, + VMS: vms * pageSize, + } + return memInfo, nil +} diff --git a/component/memory/memory_openbsd.go b/component/memory/memory_openbsd.go new file mode 100644 index 0000000000..e4e89996bd --- /dev/null +++ b/component/memory/memory_openbsd.go @@ -0,0 +1,95 @@ +package memory + +import ( + "bytes" + "encoding/binary" + "errors" + "unsafe" + + "golang.org/x/sys/unix" +) + +const ( + CTLKern = 1 + KernProc = 14 + KernProcPID = 1 +) + +func callKernProcSyscall(op int32, arg int32) ([]byte, uint64, error) { + mib := []int32{CTLKern, KernProc, op, arg, sizeOfKinfoProc, 0} + mibptr := unsafe.Pointer(&mib[0]) + miblen := uint64(len(mib)) + length := uint64(0) + _, _, err := unix.Syscall6( + unix.SYS___SYSCTL, + uintptr(mibptr), + uintptr(miblen), + 0, + uintptr(unsafe.Pointer(&length)), + 0, + 0) + if err != 0 { + return nil, length, err + } + + count := int32(length / uint64(sizeOfKinfoProc)) + mib = []int32{CTLKern, KernProc, op, arg, sizeOfKinfoProc, count} + mibptr = unsafe.Pointer(&mib[0]) + miblen = uint64(len(mib)) + // get proc info itself + buf := make([]byte, length) + _, _, err = unix.Syscall6( + unix.SYS___SYSCTL, + uintptr(mibptr), + uintptr(miblen), + uintptr(unsafe.Pointer(&buf[0])), + uintptr(unsafe.Pointer(&length)), + 0, + 0) + if err != 0 { + return buf, length, err + } + + return buf, length, nil +} + +func parseKinfoProc(buf []byte) (KinfoProc, error) { + var k KinfoProc + br := bytes.NewReader(buf) + err := binary.Read(br, binary.LittleEndian, &k) + return k, err +} + +func getKProc(pid int32) (*KinfoProc, error) { + buf, length, err := callKernProcSyscall(KernProcPID, pid) + if err != nil { + return nil, err + } + if length != sizeOfKinfoProc { + return nil, errors.New("unexpected size of KinfoProc") + } + + k, err := parseKinfoProc(buf) + if err != nil { + return nil, err + } + return &k, nil +} + +func GetMemoryInfo(pid int32) (*MemoryInfoStat, error) { + k, err := getKProc(pid) + if err != nil { + return nil, err + } + uvmexp, err := unix.SysctlUvmexp("vm.uvmexp") + if err != nil { + return nil, err + } + pageSize := uint64(uvmexp.Pagesize) + + return &MemoryInfoStat{ + RSS: uint64(k.Vm_rssize) * pageSize, + VMS: uint64(k.Vm_tsize) + uint64(k.Vm_dsize) + + uint64(k.Vm_ssize), + }, nil +} diff --git a/component/memory/memory_openbsd_386.go b/component/memory/memory_openbsd_386.go new file mode 100644 index 0000000000..ead031754f --- /dev/null +++ b/component/memory/memory_openbsd_386.go @@ -0,0 +1,99 @@ +package memory + +const sizeOfKinfoProc = 0x264 + +type KinfoProc struct { + Forw uint64 + Back uint64 + Paddr uint64 + Addr uint64 + Fd uint64 + Stats uint64 + Limit uint64 + Vmspace uint64 + Sigacts uint64 + Sess uint64 + Tsess uint64 + Ru uint64 + Eflag int32 + Exitsig int32 + Flag int32 + Pid int32 + Ppid int32 + Sid int32 + X_pgid int32 + Tpgid int32 + Uid uint32 + Ruid uint32 + Gid uint32 + Rgid uint32 + Groups [16]uint32 + Ngroups int16 + Jobc int16 + Tdev uint32 + Estcpu uint32 + Rtime_sec uint32 + Rtime_usec uint32 + Cpticks int32 + Pctcpu uint32 + Swtime uint32 + Slptime uint32 + Schedflags int32 + Uticks uint64 + Sticks uint64 + Iticks uint64 + Tracep uint64 + Traceflag int32 + Holdcnt int32 + Siglist int32 + Sigmask uint32 + Sigignore uint32 + Sigcatch uint32 + Stat int8 + Priority uint8 + Usrpri uint8 + Nice uint8 + Xstat uint16 + Acflag uint16 + Comm [24]int8 + Wmesg [8]int8 + Wchan uint64 + Login [32]int8 + Vm_rssize int32 + Vm_tsize int32 + Vm_dsize int32 + Vm_ssize int32 + Uvalid int64 + Ustart_sec uint64 + Ustart_usec uint32 + Uutime_sec uint32 + Uutime_usec uint32 + Ustime_sec uint32 + Ustime_usec uint32 + Uru_maxrss uint64 + Uru_ixrss uint64 + Uru_idrss uint64 + Uru_isrss uint64 + Uru_minflt uint64 + Uru_majflt uint64 + Uru_nswap uint64 + Uru_inblock uint64 + Uru_oublock uint64 + Uru_msgsnd uint64 + Uru_msgrcv uint64 + Uru_nsignals uint64 + Uru_nvcsw uint64 + Uru_nivcsw uint64 + Uctime_sec uint32 + Uctime_usec uint32 + Psflags int32 + Spare int32 + Svuid uint32 + Svgid uint32 + Emul [8]int8 + Rlim_rss_cur uint64 + Cpuid uint64 + Vm_map_size uint64 + Tid int32 + Rtableid uint32 +} diff --git a/component/memory/memory_openbsd_amd64.go b/component/memory/memory_openbsd_amd64.go new file mode 100644 index 0000000000..1f9ae2d38a --- /dev/null +++ b/component/memory/memory_openbsd_amd64.go @@ -0,0 +1,100 @@ +package memory + +const sizeOfKinfoProc = 0x268 + +type KinfoProc struct { + Forw uint64 + Back uint64 + Paddr uint64 + Addr uint64 + Fd uint64 + Stats uint64 + Limit uint64 + Vmspace uint64 + Sigacts uint64 + Sess uint64 + Tsess uint64 + Ru uint64 + Eflag int32 + Exitsig int32 + Flag int32 + Pid int32 + Ppid int32 + Sid int32 + X_pgid int32 + Tpgid int32 + Uid uint32 + Ruid uint32 + Gid uint32 + Rgid uint32 + Groups [16]uint32 + Ngroups int16 + Jobc int16 + Tdev uint32 + Estcpu uint32 + Rtime_sec uint32 + Rtime_usec uint32 + Cpticks int32 + Pctcpu uint32 + Swtime uint32 + Slptime uint32 + Schedflags int32 + Uticks uint64 + Sticks uint64 + Iticks uint64 + Tracep uint64 + Traceflag int32 + Holdcnt int32 + Siglist int32 + Sigmask uint32 + Sigignore uint32 + Sigcatch uint32 + Stat int8 + Priority uint8 + Usrpri uint8 + Nice uint8 + Xstat uint16 + Acflag uint16 + Comm [24]int8 + Wmesg [8]int8 + Wchan uint64 + Login [32]int8 + Vm_rssize int32 + Vm_tsize int32 + Vm_dsize int32 + Vm_ssize int32 + Uvalid int64 + Ustart_sec uint64 + Ustart_usec uint32 + Uutime_sec uint32 + Uutime_usec uint32 + Ustime_sec uint32 + Ustime_usec uint32 + Pad_cgo_0 [4]byte + Uru_maxrss uint64 + Uru_ixrss uint64 + Uru_idrss uint64 + Uru_isrss uint64 + Uru_minflt uint64 + Uru_majflt uint64 + Uru_nswap uint64 + Uru_inblock uint64 + Uru_oublock uint64 + Uru_msgsnd uint64 + Uru_msgrcv uint64 + Uru_nsignals uint64 + Uru_nvcsw uint64 + Uru_nivcsw uint64 + Uctime_sec uint32 + Uctime_usec uint32 + Psflags int32 + Spare int32 + Svuid uint32 + Svgid uint32 + Emul [8]int8 + Rlim_rss_cur uint64 + Cpuid uint64 + Vm_map_size uint64 + Tid int32 + Rtableid uint32 +} diff --git a/component/memory/memory_openbsd_arm.go b/component/memory/memory_openbsd_arm.go new file mode 100644 index 0000000000..ead031754f --- /dev/null +++ b/component/memory/memory_openbsd_arm.go @@ -0,0 +1,99 @@ +package memory + +const sizeOfKinfoProc = 0x264 + +type KinfoProc struct { + Forw uint64 + Back uint64 + Paddr uint64 + Addr uint64 + Fd uint64 + Stats uint64 + Limit uint64 + Vmspace uint64 + Sigacts uint64 + Sess uint64 + Tsess uint64 + Ru uint64 + Eflag int32 + Exitsig int32 + Flag int32 + Pid int32 + Ppid int32 + Sid int32 + X_pgid int32 + Tpgid int32 + Uid uint32 + Ruid uint32 + Gid uint32 + Rgid uint32 + Groups [16]uint32 + Ngroups int16 + Jobc int16 + Tdev uint32 + Estcpu uint32 + Rtime_sec uint32 + Rtime_usec uint32 + Cpticks int32 + Pctcpu uint32 + Swtime uint32 + Slptime uint32 + Schedflags int32 + Uticks uint64 + Sticks uint64 + Iticks uint64 + Tracep uint64 + Traceflag int32 + Holdcnt int32 + Siglist int32 + Sigmask uint32 + Sigignore uint32 + Sigcatch uint32 + Stat int8 + Priority uint8 + Usrpri uint8 + Nice uint8 + Xstat uint16 + Acflag uint16 + Comm [24]int8 + Wmesg [8]int8 + Wchan uint64 + Login [32]int8 + Vm_rssize int32 + Vm_tsize int32 + Vm_dsize int32 + Vm_ssize int32 + Uvalid int64 + Ustart_sec uint64 + Ustart_usec uint32 + Uutime_sec uint32 + Uutime_usec uint32 + Ustime_sec uint32 + Ustime_usec uint32 + Uru_maxrss uint64 + Uru_ixrss uint64 + Uru_idrss uint64 + Uru_isrss uint64 + Uru_minflt uint64 + Uru_majflt uint64 + Uru_nswap uint64 + Uru_inblock uint64 + Uru_oublock uint64 + Uru_msgsnd uint64 + Uru_msgrcv uint64 + Uru_nsignals uint64 + Uru_nvcsw uint64 + Uru_nivcsw uint64 + Uctime_sec uint32 + Uctime_usec uint32 + Psflags int32 + Spare int32 + Svuid uint32 + Svgid uint32 + Emul [8]int8 + Rlim_rss_cur uint64 + Cpuid uint64 + Vm_map_size uint64 + Tid int32 + Rtableid uint32 +} diff --git a/component/memory/memory_openbsd_arm64.go b/component/memory/memory_openbsd_arm64.go new file mode 100644 index 0000000000..f0fb9107a8 --- /dev/null +++ b/component/memory/memory_openbsd_arm64.go @@ -0,0 +1,100 @@ +package memory + +const sizeOfKinfoProc = 0x270 + +type KinfoProc struct { + Forw uint64 + Back uint64 + Paddr uint64 + Addr uint64 + Fd uint64 + Stats uint64 + Limit uint64 + Vmspace uint64 + Sigacts uint64 + Sess uint64 + Tsess uint64 + Ru uint64 + Eflag int32 + Exitsig int32 + Flag int32 + Pid int32 + Ppid int32 + Sid int32 + X_pgid int32 + Tpgid int32 + Uid uint32 + Ruid uint32 + Gid uint32 + Rgid uint32 + Groups [16]uint32 + Ngroups int16 + Jobc int16 + Tdev uint32 + Estcpu uint32 + Rtime_sec uint32 + Rtime_usec uint32 + Cpticks int32 + Pctcpu uint32 + Swtime uint32 + Slptime uint32 + Schedflags int32 + Uticks uint64 + Sticks uint64 + Iticks uint64 + Tracep uint64 + Traceflag int32 + Holdcnt int32 + Siglist int32 + Sigmask uint32 + Sigignore uint32 + Sigcatch uint32 + Stat int8 + Priority uint8 + Usrpri uint8 + Nice uint8 + Xstat uint16 + Acflag uint16 + Comm [24]int8 + Wmesg [8]uint8 + Wchan uint64 + Login [32]uint8 + Vm_rssize int32 + Vm_tsize int32 + Vm_dsize int32 + Vm_ssize int32 + Uvalid int64 + Ustart_sec uint64 + Ustart_usec uint32 + Uutime_sec uint32 + Uutime_usec uint32 + Ustime_sec uint32 + Ustime_usec uint32 + Uru_maxrss uint64 + Uru_ixrss uint64 + Uru_idrss uint64 + Uru_isrss uint64 + Uru_minflt uint64 + Uru_majflt uint64 + Uru_nswap uint64 + Uru_inblock uint64 + Uru_oublock uint64 + Uru_msgsnd uint64 + Uru_msgrcv uint64 + Uru_nsignals uint64 + Uru_nvcsw uint64 + Uru_nivcsw uint64 + Uctime_sec uint32 + Uctime_usec uint32 + Psflags uint32 + Spare int32 + Svuid uint32 + Svgid uint32 + Emul [8]uint8 + Rlim_rss_cur uint64 + Cpuid uint64 + Vm_map_size uint64 + Tid int32 + Rtableid uint32 + Pledge uint64 +} diff --git a/component/memory/memory_openbsd_riscv64.go b/component/memory/memory_openbsd_riscv64.go new file mode 100644 index 0000000000..3b60d19bc7 --- /dev/null +++ b/component/memory/memory_openbsd_riscv64.go @@ -0,0 +1,101 @@ +package memory + +const sizeOfKinfoProc = 0x288 + +type KinfoProc struct { + Forw uint64 + Back uint64 + Paddr uint64 + Addr uint64 + Fd uint64 + Stats uint64 + Limit uint64 + Vmspace uint64 + Sigacts uint64 + Sess uint64 + Tsess uint64 + Ru uint64 + Eflag int32 + Exitsig int32 + Flag int32 + Pid int32 + Ppid int32 + Sid int32 + X_pgid int32 + Tpgid int32 + Uid uint32 + Ruid uint32 + Gid uint32 + Rgid uint32 + Groups [16]uint32 + Ngroups int16 + Jobc int16 + Tdev uint32 + Estcpu uint32 + Rtime_sec uint32 + Rtime_usec uint32 + Cpticks int32 + Pctcpu uint32 + Swtime uint32 + Slptime uint32 + Schedflags int32 + Uticks uint64 + Sticks uint64 + Iticks uint64 + Tracep uint64 + Traceflag int32 + Holdcnt int32 + Siglist int32 + Sigmask uint32 + Sigignore uint32 + Sigcatch uint32 + Stat int8 + Priority uint8 + Usrpri uint8 + Nice uint8 + Xstat uint16 + Spare uint16 + Comm [24]int8 + Wmesg [8]uint8 + Wchan uint64 + Login [32]uint8 + Vm_rssize int32 + Vm_tsize int32 + Vm_dsize int32 + Vm_ssize int32 + Uvalid int64 + Ustart_sec uint64 + Ustart_usec uint32 + Uutime_sec uint32 + Uutime_usec uint32 + Ustime_sec uint32 + Ustime_usec uint32 + Uru_maxrss uint64 + Uru_ixrss uint64 + Uru_idrss uint64 + Uru_isrss uint64 + Uru_minflt uint64 + Uru_majflt uint64 + Uru_nswap uint64 + Uru_inblock uint64 + Uru_oublock uint64 + Uru_msgsnd uint64 + Uru_msgrcv uint64 + Uru_nsignals uint64 + Uru_nvcsw uint64 + Uru_nivcsw uint64 + Uctime_sec uint32 + Uctime_usec uint32 + Psflags uint32 + Acflag uint32 + Svuid uint32 + Svgid uint32 + Emul [8]uint8 + Rlim_rss_cur uint64 + Cpuid uint64 + Vm_map_size uint64 + Tid int32 + Rtableid uint32 + Pledge uint64 + Name [24]uint8 +} diff --git a/component/memory/memory_test.go b/component/memory/memory_test.go new file mode 100644 index 0000000000..0af163be71 --- /dev/null +++ b/component/memory/memory_test.go @@ -0,0 +1,23 @@ +package memory + +import ( + "errors" + "os" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestMemoryInfo(t *testing.T) { + v, err := GetMemoryInfo(int32(os.Getpid())) + if errors.Is(err, ErrNotImplementedError) { + t.Skip("not implemented") + } + require.NoErrorf(t, err, "getting memory info error %v", err) + empty := MemoryInfoStat{} + if v == nil || *v == empty { + t.Errorf("could not get memory info %v", v) + } else { + t.Logf("memory info {RSS:%s, VMS:%s}", PrettyByteSize(v.RSS), PrettyByteSize(v.VMS)) + } +} diff --git a/component/memory/memory_windows.go b/component/memory/memory_windows.go new file mode 100644 index 0000000000..04dad08438 --- /dev/null +++ b/component/memory/memory_windows.go @@ -0,0 +1,66 @@ +package memory + +import ( + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +var ( + modpsapi = windows.NewLazySystemDLL("psapi.dll") + procGetProcessMemoryInfo = modpsapi.NewProc("GetProcessMemoryInfo") +) + +const processQueryInformation = windows.PROCESS_QUERY_LIMITED_INFORMATION + +type PROCESS_MEMORY_COUNTERS struct { + CB uint32 + PageFaultCount uint32 + PeakWorkingSetSize uint64 + WorkingSetSize uint64 + QuotaPeakPagedPoolUsage uint64 + QuotaPagedPoolUsage uint64 + QuotaPeakNonPagedPoolUsage uint64 + QuotaNonPagedPoolUsage uint64 + PagefileUsage uint64 + PeakPagefileUsage uint64 +} + +func getProcessMemoryInfo(h windows.Handle, mem *PROCESS_MEMORY_COUNTERS) (err error) { + r1, _, e1 := syscall.Syscall(procGetProcessMemoryInfo.Addr(), 3, uintptr(h), uintptr(unsafe.Pointer(mem)), uintptr(unsafe.Sizeof(*mem))) + if r1 == 0 { + if e1 != 0 { + err = error(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func getMemoryInfo(pid int32) (PROCESS_MEMORY_COUNTERS, error) { + var mem PROCESS_MEMORY_COUNTERS + c, err := windows.OpenProcess(processQueryInformation, false, uint32(pid)) + if err != nil { + return mem, err + } + defer windows.CloseHandle(c) + if err := getProcessMemoryInfo(c, &mem); err != nil { + return mem, err + } + + return mem, err +} + +func GetMemoryInfo(pid int32) (*MemoryInfoStat, error) { + mem, err := getMemoryInfo(pid) + if err != nil { + return nil, err + } + ret := &MemoryInfoStat{ + RSS: uint64(mem.WorkingSetSize), + VMS: uint64(mem.PagefileUsage), + } + return ret, nil +} diff --git a/component/resource/fetcher.go b/component/resource/fetcher.go index c384277cc9..24f24d47b9 100644 --- a/component/resource/fetcher.go +++ b/component/resource/fetcher.go @@ -183,7 +183,6 @@ func (f *Fetcher[V]) startPullLoop(forceUpdate bool) (err error) { if f.vehicle.Type() == types.File { f.watcher, err = fswatch.NewWatcher(fswatch.Options{ Path: []string{f.vehicle.Path()}, - Direct: true, Callback: f.updateCallback, }) if err != nil { diff --git a/component/resource/vehicle.go b/component/resource/vehicle.go index 00b3170bfd..67ab19b723 100644 --- a/component/resource/vehicle.go +++ b/component/resource/vehicle.go @@ -135,7 +135,7 @@ func (h *HTTPVehicle) Read(ctx context.Context, oldHash utils.HashType) (buf []b setIfNoneMatch = true } } - resp, err := mihomoHttp.HttpRequestWithProxy(ctx, h.url, http.MethodGet, header, nil, h.proxy) + resp, err := mihomoHttp.HttpRequest(ctx, h.url, http.MethodGet, header, nil, mihomoHttp.WithSpecialProxy(h.proxy)) if err != nil { return } diff --git a/component/tls/auth.go b/component/tls/auth.go new file mode 100644 index 0000000000..bfda9f3553 --- /dev/null +++ b/component/tls/auth.go @@ -0,0 +1,45 @@ +package tls + +import ( + utls "github.com/metacubex/utls" +) + +type ClientAuthType = utls.ClientAuthType + +const ( + NoClientCert = utls.NoClientCert + RequestClientCert = utls.RequestClientCert + RequireAnyClientCert = utls.RequireAnyClientCert + VerifyClientCertIfGiven = utls.VerifyClientCertIfGiven + RequireAndVerifyClientCert = utls.RequireAndVerifyClientCert +) + +func ClientAuthTypeFromString(s string) ClientAuthType { + switch s { + case "request": + return RequestClientCert + case "require-any": + return RequireAnyClientCert + case "verify-if-given": + return VerifyClientCertIfGiven + case "require-and-verify": + return RequireAndVerifyClientCert + default: + return NoClientCert + } +} + +func ClientAuthTypeToString(t ClientAuthType) string { + switch t { + case RequestClientCert: + return "request" + case RequireAnyClientCert: + return "require-any" + case VerifyClientCertIfGiven: + return "verify-if-given" + case RequireAndVerifyClientCert: + return "require-and-verify" + default: + return "" + } +} diff --git a/component/tls/reality.go b/component/tls/reality.go index 2de5c46e73..fe1135f3c4 100644 --- a/component/tls/reality.go +++ b/component/tls/reality.go @@ -24,7 +24,6 @@ import ( "github.com/metacubex/randv2" utls "github.com/metacubex/utls" - "golang.org/x/crypto/chacha20poly1305" "golang.org/x/crypto/hkdf" "golang.org/x/net/http2" ) @@ -107,13 +106,8 @@ func GetRealityConn(ctx context.Context, conn net.Conn, fingerprint UClientHello if err != nil { return nil, err } - var aeadCipher cipher.AEAD - if utls.AesgcmPreferred(hello.CipherSuites) { - aesBlock, _ := aes.NewCipher(authKey) - aeadCipher, _ = cipher.NewGCM(aesBlock) - } else { - aeadCipher, _ = chacha20poly1305.New(authKey) - } + aesBlock, _ := aes.NewCipher(authKey) + aeadCipher, _ := cipher.NewGCM(aesBlock) aeadCipher.Seal(hello.SessionId[:0], hello.Random[20:], hello.SessionId[:16], hello.Raw) copy(hello.Raw[39:], hello.SessionId) //log.Debugln("REALITY hello.sessionId: %v", hello.SessionId) @@ -185,6 +179,7 @@ func (c *realityVerifier) VerifyPeerCertificate(rawCerts [][]byte, verifiedChain opts := x509.VerifyOptions{ DNSName: c.serverName, Intermediates: x509.NewCertPool(), + CurrentTime: ntp.Now(), } for _, cert := range certs[1:] { opts.Intermediates.AddCert(cert) diff --git a/component/tls/utls.go b/component/tls/utls.go index fd5f0e5449..f68cb997f1 100644 --- a/component/tls/utls.go +++ b/component/tls/utls.go @@ -135,6 +135,8 @@ func UConfig(config *tls.Config) *utls.Config { RootCAs: config.RootCAs, NextProtos: config.NextProtos, ServerName: config.ServerName, + ClientAuth: utls.ClientAuthType(config.ClientAuth), + ClientCAs: config.ClientCAs, InsecureSkipVerify: config.InsecureSkipVerify, CipherSuites: config.CipherSuites, MinVersion: config.MinVersion, diff --git a/component/updater/update_core.go b/component/updater/update_core.go index 5f7113d0fb..ffc4fb0451 100644 --- a/component/updater/update_core.go +++ b/component/updater/update_core.go @@ -15,6 +15,7 @@ import ( "sync" "time" + "github.com/metacubex/mihomo/component/ca" mihomoHttp "github.com/metacubex/mihomo/component/http" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/constant/features" @@ -171,7 +172,7 @@ func (u *CoreUpdater) Update(currentExePath string, channel string, force bool) func (u *CoreUpdater) getLatestVersion(versionURL string) (version string, err error) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) defer cancel() - resp, err := mihomoHttp.HttpRequest(ctx, versionURL, http.MethodGet, nil, nil) + resp, err := mihomoHttp.HttpRequest(ctx, versionURL, http.MethodGet, nil, nil, mihomoHttp.WithCAOption(ca.Option{ZeroTrust: true})) if err != nil { return "", err } @@ -194,7 +195,7 @@ func (u *CoreUpdater) getLatestVersion(versionURL string) (version string, err e func (u *CoreUpdater) download(updateDir, packagePath, packageURL string) (err error) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*90) defer cancel() - resp, err := mihomoHttp.HttpRequest(ctx, packageURL, http.MethodGet, nil, nil) + resp, err := mihomoHttp.HttpRequest(ctx, packageURL, http.MethodGet, nil, nil, mihomoHttp.WithCAOption(ca.Option{ZeroTrust: true})) if err != nil { return fmt.Errorf("http request failed: %w", err) } diff --git a/component/updater/update_ui.go b/component/updater/update_ui.go index 1366f00de0..878adc1bc6 100644 --- a/component/updater/update_ui.go +++ b/component/updater/update_ui.go @@ -5,6 +5,7 @@ import ( "archive/zip" "bytes" "compress/gzip" + "errors" "fmt" "io" "os" @@ -12,6 +13,7 @@ import ( "path/filepath" "strings" "sync" + "syscall" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/log" @@ -308,7 +310,16 @@ func moveDir(src string, dst string) error { } for _, dirEntry := range dirEntryList { - err = os.Rename(filepath.Join(src, dirEntry.Name()), filepath.Join(dst, dirEntry.Name())) + srcPath := filepath.Join(src, dirEntry.Name()) + dstPath := filepath.Join(dst, dirEntry.Name()) + err = os.Rename(srcPath, dstPath) + if err != nil { + // Fallback for invalid cross-device link (errno:18). + if errors.Is(err, syscall.Errno(18)) { + err = copyAll(srcPath, dstPath) + _ = os.RemoveAll(srcPath) + } + } if err != nil { return err } @@ -316,6 +327,50 @@ func moveDir(src string, dst string) error { return nil } +// copyAll copy the src path and any children it contains to dst +// modify from [os.CopyFS] +func copyAll(src, dst string) error { + return filepath.Walk(src, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + fpath, err := filepath.Rel(src, path) + if err != nil { + return err + } + newPath := filepath.Join(dst, fpath) + + switch info.Mode().Type() { + case os.ModeDir: + return os.MkdirAll(newPath, info.Mode().Perm()) + case os.ModeSymlink: + target, err := os.Readlink(path) + if err != nil { + return err + } + return os.Symlink(target, newPath) + case 0: + r, err := os.Open(path) + if err != nil { + return err + } + defer r.Close() + w, err := os.OpenFile(newPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, info.Mode().Perm()) + if err != nil { + return err + } + + if _, err := io.Copy(w, r); err != nil { + w.Close() + return &os.PathError{Op: "Copy", Path: newPath, Err: err} + } + return w.Close() + default: + return &os.PathError{Op: "CopyFS", Path: path, Err: os.ErrInvalid} + } + }) +} + func inDest(fpath, dest string) bool { if rel, err := filepath.Rel(dest, fpath); err == nil { if filepath.IsLocal(rel) { diff --git a/config/config.go b/config/config.go index 77fdd4d64d..bcd3cc7ad2 100644 --- a/config/config.go +++ b/config/config.go @@ -117,7 +117,6 @@ type Cors struct { // Experimental config type Experimental struct { - Fingerprints []string QUICGoDisableGSO bool QUICGoDisableECN bool IP4PEnable bool @@ -175,6 +174,8 @@ type Profile struct { type TLS struct { Certificate string PrivateKey string + ClientAuthType string + ClientAuthCert string EchKey string CustomTrustCert []string } @@ -292,6 +293,7 @@ type RawTun struct { ExcludePackage []string `yaml:"exclude-package" json:"exclude-package,omitempty"` EndpointIndependentNat bool `yaml:"endpoint-independent-nat" json:"endpoint-independent-nat,omitempty"` UDPTimeout int64 `yaml:"udp-timeout" json:"udp-timeout,omitempty"` + DisableICMPForwarding bool `yaml:"disable-icmp-forwarding" json:"disable-icmp-forwarding,omitempty"` FileDescriptor int `yaml:"file-descriptor" json:"file-descriptor"` Inet4RouteAddress []netip.Prefix `yaml:"inet4-route-address" json:"inet4-route-address,omitempty"` @@ -368,6 +370,8 @@ type RawSniffingConfig struct { type RawTLS struct { Certificate string `yaml:"certificate" json:"certificate"` PrivateKey string `yaml:"private-key" json:"private-key"` + ClientAuthType string `yaml:"client-auth-type" json:"client-auth-type"` + ClientAuthCert string `yaml:"client-auth-cert" json:"client-auth-cert"` EchKey string `yaml:"ech-key" json:"ech-key"` CustomTrustCert []string `yaml:"custom-certifactes" json:"custom-certifactes"` } @@ -790,7 +794,6 @@ func parseController(cfg *RawConfig) (*Controller, error) { func parseExperimental(cfg *RawConfig) (*Experimental, error) { return &Experimental{ - Fingerprints: cfg.Experimental.Fingerprints, QUICGoDisableGSO: cfg.Experimental.QUICGoDisableGSO, QUICGoDisableECN: cfg.Experimental.QUICGoDisableECN, IP4PEnable: cfg.Experimental.IP4PEnable, @@ -828,6 +831,8 @@ func parseTLS(cfg *RawConfig) (*TLS, error) { return &TLS{ Certificate: cfg.TLS.Certificate, PrivateKey: cfg.TLS.PrivateKey, + ClientAuthType: cfg.TLS.ClientAuthType, + ClientAuthCert: cfg.TLS.ClientAuthCert, EchKey: cfg.TLS.EchKey, CustomTrustCert: cfg.TLS.CustomTrustCert, }, nil @@ -1552,6 +1557,7 @@ func parseTun(rawTun RawTun, general *General) error { ExcludePackage: rawTun.ExcludePackage, EndpointIndependentNat: rawTun.EndpointIndependentNat, UDPTimeout: rawTun.UDPTimeout, + DisableICMPForwarding: rawTun.DisableICMPForwarding, FileDescriptor: rawTun.FileDescriptor, Inet4RouteAddress: rawTun.Inet4RouteAddress, diff --git a/dns/client.go b/dns/client.go index 359cb443ae..9cc7df2d71 100644 --- a/dns/client.go +++ b/dns/client.go @@ -16,35 +16,23 @@ import ( ) type client struct { - *D.Client - port string - host string - dialer *dnsDialer - addr string + port string + host string + dialer *dnsDialer + schema string + skipCertVerify bool } var _ dnsClient = (*client)(nil) // Address implements dnsClient func (c *client) Address() string { - if len(c.addr) != 0 { - return c.addr - } - schema := "udp" - if strings.HasPrefix(c.Client.Net, "tcp") { - schema = "tcp" - if strings.HasSuffix(c.Client.Net, "tls") { - schema = "tls" - } - } - - c.addr = fmt.Sprintf("%s://%s", schema, net.JoinHostPort(c.host, c.port)) - return c.addr + return fmt.Sprintf("%s://%s", c.schema, net.JoinHostPort(c.host, c.port)) } func (c *client) ExchangeContext(ctx context.Context, m *D.Msg) (*D.Msg, error) { network := "udp" - if strings.HasPrefix(c.Client.Net, "tcp") { + if c.schema != "udp" { network = "tcp" } @@ -53,9 +41,24 @@ func (c *client) ExchangeContext(ctx context.Context, m *D.Msg) (*D.Msg, error) if err != nil { return nil, err } - defer func() { - _ = conn.Close() - }() + defer conn.Close() + + if c.schema == "tls" { + tlsConfig, err := ca.GetTLSConfig(ca.Option{ + TLSConfig: &tls.Config{ + ServerName: c.host, + InsecureSkipVerify: c.skipCertVerify, + }, + }) + if err != nil { + return nil, err + } + tlsConn := tls.Client(conn, tlsConfig) + if err := tlsConn.HandshakeContext(ctx); err != nil { + return nil, err + } + conn = tlsConn + } // miekg/dns ExchangeContext doesn't respond to context cancel. // this is a workaround @@ -65,34 +68,30 @@ func (c *client) ExchangeContext(ctx context.Context, m *D.Msg) (*D.Msg, error) } ch := make(chan result, 1) go func() { - if strings.HasSuffix(c.Client.Net, "tls") { - conn = tls.Client(conn, ca.GetGlobalTLSConfig(c.Client.TLSConfig)) + dClient := &D.Client{ + UDPSize: 4096, + Timeout: 5 * time.Second, } - dConn := &D.Conn{ - Conn: conn, - UDPSize: c.Client.UDPSize, - TsigSecret: c.Client.TsigSecret, - TsigProvider: c.Client.TsigProvider, + Conn: conn, + UDPSize: dClient.UDPSize, } - msg, _, err := c.Client.ExchangeWithConn(m, dConn) + msg, _, err := dClient.ExchangeWithConn(m, dConn) // Resolvers MUST resend queries over TCP if they receive a truncated UDP response (with TC=1 set)! if msg != nil && msg.Truncated && network == "udp" { - tcpClient := *c.Client // copy a client - tcpClient.Net = "tcp" network = "tcp" log.Debugln("[DNS] Truncated reply from %s:%s for %s over UDP, retrying over TCP", c.host, c.port, m.Question[0].String()) - dConn.Conn, err = c.dialer.DialContext(ctx, network, addr) + var tcpConn net.Conn + tcpConn, err = c.dialer.DialContext(ctx, network, addr) if err != nil { ch <- result{msg, err} return } - defer func() { - _ = conn.Close() - }() - msg, _, err = tcpClient.ExchangeWithConn(m, dConn) + defer tcpConn.Close() + dConn.Conn = tcpConn + msg, _, err = dClient.ExchangeWithConn(m, dConn) } ch <- result{msg, err} @@ -111,20 +110,19 @@ func (c *client) ResetConnection() {} func newClient(addr string, resolver *Resolver, netType string, params map[string]string, proxyAdapter C.ProxyAdapter, proxyName string) *client { host, port, _ := net.SplitHostPort(addr) c := &client{ - Client: &D.Client{ - Net: netType, - TLSConfig: &tls.Config{ - ServerName: host, - }, - UDPSize: 4096, - Timeout: 5 * time.Second, - }, port: port, host: host, dialer: newDNSDialer(resolver, proxyAdapter, proxyName), + schema: "udp", + } + if strings.HasPrefix(netType, "tcp") { + c.schema = "tcp" + if strings.HasSuffix(netType, "tls") { + c.schema = "tls" + } } if params["skip-cert-verify"] == "true" { - c.TLSConfig.InsecureSkipVerify = true + c.skipCertVerify = true } return c } diff --git a/dns/doh.go b/dns/doh.go index a394e812e9..29be78b2d4 100644 --- a/dns/doh.go +++ b/dns/doh.go @@ -397,12 +397,16 @@ func (doh *dnsOverHTTPS) createTransport(ctx context.Context) (t http.RoundTripp return transport, nil } - tlsConfig := ca.GetGlobalTLSConfig( - &tls.Config{ + tlsConfig, err := ca.GetTLSConfig(ca.Option{ + TLSConfig: &tls.Config{ InsecureSkipVerify: doh.skipCertVerify, MinVersion: tls.VersionTLS12, SessionTicketsDisabled: false, - }) + }, + }) + if err != nil { + return nil, err + } var nextProtos []string for _, v := range doh.httpVersions { nextProtos = append(nextProtos, string(v)) diff --git a/dns/doq.go b/dns/doq.go index a611265d86..6861bd4d11 100644 --- a/dns/doq.go +++ b/dns/doq.go @@ -331,15 +331,19 @@ func (doq *dnsOverQUIC) openConnection(ctx context.Context) (conn *quic.Conn, er return nil, err } - tlsConfig := ca.GetGlobalTLSConfig( - &tls.Config{ + tlsConfig, err := ca.GetTLSConfig(ca.Option{ + TLSConfig: &tls.Config{ ServerName: host, InsecureSkipVerify: doq.skipCertVerify, NextProtos: []string{ NextProtoDQ, }, SessionTicketsDisabled: false, - }) + }, + }) + if err != nil { + return nil, err + } transport := quic.Transport{Conn: udp} transport.SetCreatedConn(true) // auto close conn diff --git a/docs/config.yaml b/docs/config.yaml index aa105ff671..574188182a 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -48,6 +48,9 @@ ipv6: true # 开启 IPv6 总开关,关闭阻断所有 IPv6 链接和屏蔽 DNS tls: certificate: string # 证书 PEM 格式,或者 证书的路径 private-key: string # 证书对应的私钥 PEM 格式,或者私钥路径 + # 下面两项为mTLS配置项,如果client-auth-type设置为 "verify-if-given" 或 "require-and-verify" 则client-auth-cert必须不为空 + # client-auth-type: "" # 可选值:""、"request"、"require-any"、"verify-if-given"、"require-and-verify" + # client-auth-cert: string # 证书 PEM 格式,或者 证书的路径 # 如果填写则开启ech(可由 mihomo generate ech-keypair <明文域名> 生成) # ech-key: | # -----BEGIN ECH KEYS----- @@ -142,6 +145,7 @@ tun: # gso-max-size: 65536 # 通用分段卸载包的最大大小 auto-redirect: false # 自动配置 iptables 以重定向 TCP 连接。仅支持 Linux。带有 auto-redirect 的 auto-route 现在可以在路由器上按预期工作,无需干预。 # strict-route: true # 将所有连接路由到 tun 来防止泄漏,但你的设备将无法其他设备被访问 + # disable-icmp-forwarding: true # 禁用 ICMP 转发,防止某些情况下的 ICMP 环回问题,ping 将不会显示真实的延迟 route-address-set: # 将指定规则集中的目标 IP CIDR 规则添加到防火墙, 不匹配的流量将绕过路由, 仅支持 Linux,且需要 nftables,`auto-route` 和 `auto-redirect` 已启用。 - ruleset-1 - ruleset-2 @@ -348,7 +352,10 @@ proxies: # socks5 # username: username # password: password # tls: true - # fingerprint: xxxx + # fingerprint: xxxx # 配置指纹将实现 SSL Pining 效果, 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 + # 下面两项如果填写则开启 mTLS(需要同时填写) + # certificate: ./client.crt # 证书 PEM 格式,或者 证书的路径 + # private-key: ./client.key # 证书对应的私钥 PEM 格式,或者私钥路径 # skip-cert-verify: true # udp: true # ip-version: ipv6 @@ -363,7 +370,10 @@ proxies: # socks5 # tls: true # https # skip-cert-verify: true # sni: custom.com - # fingerprint: xxxx # 同 experimental.fingerprints 使用 sha256 指纹,配置协议独立的指纹,将忽略 experimental.fingerprints + # fingerprint: xxxx # 配置指纹将实现 SSL Pining 效果, 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 + # 下面两项如果填写则开启 mTLS(需要同时填写) + # certificate: ./client.crt # 证书 PEM 格式,或者 证书的路径 + # private-key: ./client.key # 证书对应的私钥 PEM 格式,或者私钥路径 # ip-version: dual # Snell @@ -431,9 +441,10 @@ proxies: # socks5 plugin-opts: mode: websocket # no QUIC now # tls: true # wss - # 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 - # 配置指纹将实现 SSL Pining 效果 - # fingerprint: xxxx + # fingerprint: xxxx # 配置指纹将实现 SSL Pining 效果, 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 + # 下面两项如果填写则开启 mTLS(需要同时填写) + # certificate: ./client.crt # 证书 PEM 格式,或者 证书的路径 + # private-key: ./client.key # 证书对应的私钥 PEM 格式,或者私钥路径 # ech-opts: # enable: true # 必须手动开启 # # 如果config为空则通过dns解析,不为空则通过该值指定,格式为经过base64编码的ech参数(dig +short TYPE65 tls-ech.dev) @@ -471,9 +482,10 @@ proxies: # socks5 plugin-opts: mode: websocket # tls: true # wss - # 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 - # 配置指纹将实现 SSL Pining 效果 - # fingerprint: xxxx + # fingerprint: xxxx # 配置指纹将实现 SSL Pining 效果, 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 + # 下面两项如果填写则开启 mTLS(需要同时填写) + # certificate: ./client.crt # 证书 PEM 格式,或者 证书的路径 + # private-key: ./client.key # 证书对应的私钥 PEM 格式,或者私钥路径 # skip-cert-verify: true # host: bing.com # path: "/" @@ -522,6 +534,37 @@ proxies: # socks5 version-hint: "tls12" restls-script: "1000?100<1,500~100,350~100,600~100,400~200" + - name: "ss-kcptun" + type: ss + server: [YOUR_SERVER_IP] + port: 443 + cipher: chacha20-ietf-poly1305 + password: [YOUR_SS_PASSWORD] + plugin: kcptun + plugin-opts: + key: it's a secrect # pre-shared secret between client and server + crypt: aes # aes, aes-128, aes-192, salsa20, blowfish, twofish, cast5, 3des, tea, xtea, xor, sm4, none, null + mode: fast # profiles: fast3, fast2, fast, normal, manual + conn: 1 # set num of UDP connections to server + autoexpire: 0 # set auto expiration time(in seconds) for a single UDP connection, 0 to disable + scavengettl: 600 # set how long an expired connection can live (in seconds) + mtu: 1350 # set maximum transmission unit for UDP packets + sndwnd: 128 # set send window size(num of packets) + rcvwnd: 512 # set receive window size(num of packets) + datashard: 10 # set reed-solomon erasure coding - datashard + parityshard: 3 # set reed-solomon erasure coding - parityshard + dscp: 0 # set DSCP(6bit) + nocomp: false # disable compression + acknodelay: false # flush ack immediately when a packet is received + nodelay: 0 + interval: 50 + resend: false + sockbuf: 4194304 # per-socket buffer in bytes + smuxver: 1 # specify smux version, available 1,2 + smuxbuf: 4194304 # the overall de-mux buffer in bytes + streambuf: 2097152 # per stream receive buffer in bytes, smux v2+ + keepalive: 10 # seconds between heartbeats + # vmess # cipher 支持 auto/aes-128-gcm/chacha20-poly1305/none - name: "vmess" @@ -533,7 +576,10 @@ proxies: # socks5 cipher: auto # udp: true # tls: true - # fingerprint: xxxx + # fingerprint: xxxx # 配置指纹将实现 SSL Pining 效果, 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 + # 下面两项如果填写则开启 mTLS(需要同时填写) + # certificate: ./client.crt # 证书 PEM 格式,或者 证书的路径 + # private-key: ./client.key # 证书对应的私钥 PEM 格式,或者私钥路径 # client-fingerprint: chrome # Available: "chrome","firefox","safari","ios","random", currently only support TLS transport in TCP/GRPC/WS/HTTP for VLESS/Vmess and trojan. # skip-cert-verify: true # servername: example.com # priority over wss host @@ -560,7 +606,10 @@ proxies: # socks5 cipher: auto network: h2 tls: true - # fingerprint: xxxx + # fingerprint: xxxx # 配置指纹将实现 SSL Pining 效果, 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 + # 下面两项如果填写则开启 mTLS(需要同时填写) + # certificate: ./client.crt # 证书 PEM 格式,或者 证书的路径 + # private-key: ./client.key # 证书对应的私钥 PEM 格式,或者私钥路径 h2-opts: host: - http.example.com @@ -595,7 +644,10 @@ proxies: # socks5 cipher: auto network: grpc tls: true - # fingerprint: xxxx + # fingerprint: xxxx # 配置指纹将实现 SSL Pining 效果, 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 + # 下面两项如果填写则开启 mTLS(需要同时填写) + # certificate: ./client.crt # 证书 PEM 格式,或者 证书的路径 + # private-key: ./client.key # 证书对应的私钥 PEM 格式,或者私钥路径 servername: example.com # skip-cert-verify: true grpc-opts: @@ -610,9 +662,11 @@ proxies: # socks5 uuid: uuid network: tcp servername: example.com # AKA SNI - # flow: xtls-rprx-direct # xtls-rprx-origin # enable XTLS # skip-cert-verify: true - # fingerprint: xxxx + # 下面两项如果填写则开启 mTLS(需要同时填写) + # certificate: ./client.crt # 证书 PEM 格式,或者 证书的路径 + # private-key: ./client.key # 证书对应的私钥 PEM 格式,或者私钥路径 + # fingerprint: xxxx # 配置指纹将实现 SSL Pining 效果, 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 # client-fingerprint: random # Available: "chrome","firefox","safari","random","none" # ech-opts: # enable: true # 必须手动开启 @@ -629,7 +683,10 @@ proxies: # socks5 udp: true flow: xtls-rprx-vision client-fingerprint: chrome - # fingerprint: xxxx + # 下面两项如果填写则开启 mTLS(需要同时填写) + # certificate: ./client.crt # 证书 PEM 格式,或者 证书的路径 + # private-key: ./client.key # 证书对应的私钥 PEM 格式,或者私钥路径 + # fingerprint: xxxx # 配置指纹将实现 SSL Pining 效果, 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 # skip-cert-verify: true - name: "vless-encryption" @@ -640,10 +697,16 @@ proxies: # socks5 network: tcp # ------------------------- # vless encryption客户端配置: - # (native/xorpub 的 XTLS 可以 Splice。只使用 1-RTT 模式 / 若服务端发的 ticket 中秒数不为零则 0-RTT 复用) + # (native/xorpub 的 XTLS Vision 可以 Splice。只使用 1-RTT 模式 / 若服务端发的 ticket 中秒数不为零则 0-RTT 复用) # / 是只能选一个,后面 base64 至少一个,无限串联,使用 mihomo generate vless-x25519 和 mihomo generate vless-mlkem768 生成,替换值时需去掉括号 + # + # Padding 是可选的参数,仅作用于 1-RTT 以消除握手的长度特征,双端默认值均为 "100-111-1111.75-0-111.50-0-3333": + # 在 1-RTT client/server hello 后以 100% 的概率粘上随机 111 到 1111 字节的 padding + # 以 75% 的概率等待随机 0 到 111 毫秒("probability-from-to") + # 再次以 50% 的概率发送随机 0 到 3333 字节的 padding(若为 0 则不 Write()) + # 服务端、客户端可以设置不同的 padding 参数,按 len、gap 的顺序无限串联,第一个 padding 需概率 100%、至少 35 字节 # ------------------------- - encryption: "mlkem768x25519plus.native/xorpub/random.1rtt/0rtt.(X25519 Password).(ML-KEM-768 Client)..." + encryption: "mlkem768x25519plus.native/xorpub/random.1rtt/0rtt.(padding len).(padding gap).(X25519 Password).(ML-KEM-768 Client)..." tls: false #可以不开启tls udp: true @@ -693,7 +756,10 @@ proxies: # socks5 # client-fingerprint: random # Available: "chrome","firefox","safari","random","none" servername: example.com # priority over wss host # skip-cert-verify: true - # fingerprint: xxxx + # fingerprint: xxxx # 配置指纹将实现 SSL Pining 效果, 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 + # 下面两项如果填写则开启 mTLS(需要同时填写) + # certificate: ./client.crt # 证书 PEM 格式,或者 证书的路径 + # private-key: ./client.key # 证书对应的私钥 PEM 格式,或者私钥路径 ws-opts: path: "/" headers: @@ -708,7 +774,10 @@ proxies: # socks5 port: 443 password: yourpsk # client-fingerprint: random # Available: "chrome","firefox","safari","random","none" - # fingerprint: xxxx + # fingerprint: xxxx # 配置指纹将实现 SSL Pining 效果, 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 + # 下面两项如果填写则开启 mTLS(需要同时填写) + # certificate: ./client.crt # 证书 PEM 格式,或者 证书的路径 + # private-key: ./client.key # 证书对应的私钥 PEM 格式,或者私钥路径 # udp: true # sni: example.com # aka server name # alpn: @@ -732,7 +801,10 @@ proxies: # socks5 network: grpc sni: example.com # skip-cert-verify: true - # fingerprint: xxxx + # fingerprint: xxxx # 配置指纹将实现 SSL Pining 效果, 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 + # 下面两项如果填写则开启 mTLS(需要同时填写) + # certificate: ./client.crt # 证书 PEM 格式,或者 证书的路径 + # private-key: ./client.key # 证书对应的私钥 PEM 格式,或者私钥路径 udp: true grpc-opts: grpc-service-name: "example" @@ -745,7 +817,10 @@ proxies: # socks5 network: ws sni: example.com # skip-cert-verify: true - # fingerprint: xxxx + # fingerprint: xxxx # 配置指纹将实现 SSL Pining 效果, 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 + # 下面两项如果填写则开启 mTLS(需要同时填写) + # certificate: ./client.crt # 证书 PEM 格式,或者 证书的路径 + # private-key: ./client.key # 证书对应的私钥 PEM 格式,或者私钥路径 udp: true # ws-opts: # path: /path @@ -764,7 +839,10 @@ proxies: # socks5 # udp: true # sni: example.com # aka server name # skip-cert-verify: true - # fingerprint: xxxx + # fingerprint: xxxx # 配置指纹将实现 SSL Pining 效果, 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 + # 下面两项如果填写则开启 mTLS(需要同时填写) + # certificate: ./client.crt # 证书 PEM 格式,或者 证书的路径 + # private-key: ./client.key # 证书对应的私钥 PEM 格式,或者私钥路径 #hysteria - name: "hysteria" @@ -787,10 +865,11 @@ proxies: # socks5 # skip-cert-verify: false # recv-window-conn: 12582912 # recv-window: 52428800 - # ca: "./my.ca" - # ca-str: "xyz" # disable-mtu-discovery: false - # fingerprint: xxxx + # fingerprint: xxxx # 配置指纹将实现 SSL Pining 效果, 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 + # 下面两项如果填写则开启 mTLS(需要同时填写) + # certificate: ./client.crt # 证书 PEM 格式,或者 证书的路径 + # private-key: ./client.key # 证书对应的私钥 PEM 格式,或者私钥路径 # fast-open: true # 支持 TCP 快速打开,默认为 false #hysteria2 @@ -812,11 +891,12 @@ proxies: # socks5 # # 如果config为空则通过dns解析,不为空则通过该值指定,格式为经过base64编码的ech参数(dig +short TYPE65 tls-ech.dev) # config: AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA # skip-cert-verify: false - # fingerprint: xxxx + # fingerprint: xxxx # 配置指纹将实现 SSL Pining 效果, 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 + # 下面两项如果填写则开启 mTLS(需要同时填写) + # certificate: ./client.crt # 证书 PEM 格式,或者 证书的路径 + # private-key: ./client.key # 证书对应的私钥 PEM 格式,或者私钥路径 # alpn: # - h3 - # ca: "./my.ca" - # ca-str: "xyz" ###quic-go特殊配置项,不要随意修改除非你知道你在干什么### # initial-stream-receive-window: 8388608 # max-stream-receive-window: 8388608 @@ -857,20 +937,25 @@ proxies: # socks5 # jmax: 501 # s1: 30 # s2: 40 - # h1: 123456 - # h2: 67543 - # h4: 32345 - # h3: 123123 - # # AmneziaWG v1.5 - # i1: - # i2: - # i3: "" - # i4: "" - # i5: "" - # j1: - # j2: - # j3: - # itime: 60 + # s3: 50 # AmneziaWG v1.5 and v2 + # s4: 5 # AmneziaWG v1.5 and v2 + # h1: 123456 # AmneziaWG v1.0 and v1.5 + # h2: 67543 # AmneziaWG v1.0 and v1.5 + # h3: 123123 # AmneziaWG v1.0 and v1.5 + # h4: 32345 # AmneziaWG v1.0 and v1.5 + # h1: 123456-123500 # AmneziaWG v2.0 only + # h2: 67543-67550 # AmneziaWG v2.0 only + # h3: 123123-123200 # AmneziaWG v2.0 only + # h4: 32345-32350 # AmneziaWG v2.0 only + # i1: # AmneziaWG v1.5 and v2 + # i2: # AmneziaWG v1.5 and v2 + # i3: "" # AmneziaWG v1.5 and v2 + # i4: "" # AmneziaWG v1.5 and v2 + # i5: "" # AmneziaWG v1.5 and v2 + # j1: # AmneziaWG v1.5 only (removed in v2) + # j2: # AmneziaWG v1.5 only (removed in v2) + # j3: # AmneziaWG v1.5 only (removed in v2) + # itime: 60 # AmneziaWG v1.5 only (removed in v2) # tuic - name: tuic @@ -939,8 +1024,8 @@ proxies: # socks5 - name: mieru type: mieru server: 1.2.3.4 - port: 2999 - # port-range: 2090-2099 #(不可同时填写 port 和 port-range) + port: 2999 # 支持使用 ports 格式,例如 2999,3999 或 2999-3010,3950,3995-3999 + # port-range: 2090-2099 # 已废弃,请使用 port transport: TCP # 只支持 TCP udp: true # 支持 UDP over TCP username: user @@ -1190,8 +1275,11 @@ listeners: # - username: aaa # password: aaa # 下面两项如果填写则开启 tls(需要同时填写) - # certificate: ./server.crt - # private-key: ./server.key + # certificate: ./server.crt # 证书 PEM 格式,或者 证书的路径 + # private-key: ./server.key # 证书对应的私钥 PEM 格式,或者私钥路径 + # 下面两项为mTLS配置项,如果client-auth-type设置为 "verify-if-given" 或 "require-and-verify" 则client-auth-cert必须不为空 + # client-auth-type: "" # 可选值:""、"request"、"require-any"、"verify-if-given"、"require-and-verify" + # client-auth-cert: string # 证书 PEM 格式,或者 证书的路径 # 如果填写则开启ech(可由 mihomo generate ech-keypair <明文域名> 生成) # ech-key: | # -----BEGIN ECH KEYS----- @@ -1210,8 +1298,11 @@ listeners: # - username: aaa # password: aaa # 下面两项如果填写则开启 tls(需要同时填写) - # certificate: ./server.crt - # private-key: ./server.key + # certificate: ./server.crt # 证书 PEM 格式,或者 证书的路径 + # private-key: ./server.key # 证书对应的私钥 PEM 格式,或者私钥路径 + # 下面两项为mTLS配置项,如果client-auth-type设置为 "verify-if-given" 或 "require-and-verify" 则client-auth-cert必须不为空 + # client-auth-type: "" # 可选值:""、"request"、"require-any"、"verify-if-given"、"require-and-verify" + # client-auth-cert: string # 证书 PEM 格式,或者 证书的路径 # 如果填写则开启ech(可由 mihomo generate ech-keypair <明文域名> 生成) # ech-key: | # -----BEGIN ECH KEYS----- @@ -1231,8 +1322,11 @@ listeners: # - username: aaa # password: aaa # 下面两项如果填写则开启 tls(需要同时填写) - # certificate: ./server.crt - # private-key: ./server.key + # certificate: ./server.crt # 证书 PEM 格式,或者 证书的路径 + # private-key: ./server.key # 证书对应的私钥 PEM 格式,或者私钥路径 + # 下面两项为mTLS配置项,如果client-auth-type设置为 "verify-if-given" 或 "require-and-verify" 则client-auth-cert必须不为空 + # client-auth-type: "" # 可选值:""、"request"、"require-any"、"verify-if-given"、"require-and-verify" + # client-auth-cert: string # 证书 PEM 格式,或者 证书的路径 # 如果填写则开启ech(可由 mihomo generate ech-keypair <明文域名> 生成) # ech-key: | # -----BEGIN ECH KEYS----- @@ -1273,6 +1367,30 @@ listeners: # password: password # handshake: # dest: test.com:443 + # kcp-tun: + # enable: false + # key: it's a secrect # pre-shared secret between client and server + # crypt: aes # aes, aes-128, aes-192, salsa20, blowfish, twofish, cast5, 3des, tea, xtea, xor, sm4, none, null + # mode: fast # profiles: fast3, fast2, fast, normal, manual + # conn: 1 # set num of UDP connections to server + # autoexpire: 0 # set auto expiration time(in seconds) for a single UDP connection, 0 to disable + # scavengettl: 600 # set how long an expired connection can live (in seconds) + # mtu: 1350 # set maximum transmission unit for UDP packets + # sndwnd: 128 # set send window size(num of packets) + # rcvwnd: 512 # set receive window size(num of packets) + # datashard: 10 # set reed-solomon erasure coding - datashard + # parityshard: 3 # set reed-solomon erasure coding - parityshard + # dscp: 0 # set DSCP(6bit) + # nocomp: false # disable compression + # acknodelay: false # flush ack immediately when a packet is received + # nodelay: 0 + # interval: 50 + # resend: false + # sockbuf: 4194304 # per-socket buffer in bytes + # smuxver: 1 # specify smux version, available 1,2 + # smuxbuf: 4194304 # the overall de-mux buffer in bytes + # streambuf: 2097152 # per stream receive buffer in bytes, smux v2+ + # keepalive: 10 # seconds between heartbeats - name: vmess-in-1 type: vmess @@ -1287,8 +1405,11 @@ listeners: # ws-path: "/" # 如果不为空则开启 websocket 传输层 # grpc-service-name: "GunService" # 如果不为空则开启 grpc 传输层 # 下面两项如果填写则开启 tls(需要同时填写) - # certificate: ./server.crt - # private-key: ./server.key + # certificate: ./server.crt # 证书 PEM 格式,或者 证书的路径 + # private-key: ./server.key # 证书对应的私钥 PEM 格式,或者私钥路径 + # 下面两项为mTLS配置项,如果client-auth-type设置为 "verify-if-given" 或 "require-and-verify" 则client-auth-cert必须不为空 + # client-auth-type: "" # 可选值:""、"request"、"require-any"、"verify-if-given"、"require-and-verify" + # client-auth-cert: string # 证书 PEM 格式,或者 证书的路径 # 如果填写则开启ech(可由 mihomo generate ech-keypair <明文域名> 生成) # ech-key: | # -----BEGIN ECH KEYS----- @@ -1326,8 +1447,11 @@ listeners: # users: # tuicV5 填写(可以同时填写 token) # 00000000-0000-0000-0000-000000000000: PASSWORD_0 # 00000000-0000-0000-0000-000000000001: PASSWORD_1 - # certificate: ./server.crt - # private-key: ./server.key + # certificate: ./server.crt # 证书 PEM 格式,或者 证书的路径 + # private-key: ./server.key # 证书对应的私钥 PEM 格式,或者私钥路径 + # 下面两项为mTLS配置项,如果client-auth-type设置为 "verify-if-given" 或 "require-and-verify" 则client-auth-cert必须不为空 + # client-auth-type: "" # 可选值:""、"request"、"require-any"、"verify-if-given"、"require-and-verify" + # client-auth-cert: string # 证书 PEM 格式,或者 证书的路径 # 如果填写则开启ech(可由 mihomo generate ech-keypair <明文域名> 生成) # ech-key: | # -----BEGIN ECH KEYS----- @@ -1365,13 +1489,23 @@ listeners: # grpc-service-name: "GunService" # 如果不为空则开启 grpc 传输层 # ------------------------- # vless encryption服务端配置: - # (原生外观 / 只 XOR 公钥 / 全随机数。只允许 1-RTT 模式 / 同时允许 1-RTT 模式与 600 秒复用的 0-RTT 模式) + # (原生外观 / 只 XOR 公钥 / 全随机数。1-RTT 每次下发随机 300 到 600 秒的 ticket 以便 0-RTT 复用 / 只允许 1-RTT) + # 填写 "600s" 会每次随机取 50% 到 100%,即相当于填写 "300-600s" # / 是只能选一个,后面 base64 至少一个,无限串联,使用 mihomo generate vless-x25519 和 mihomo generate vless-mlkem768 生成,替换值时需去掉括号 + # + # Padding 是可选的参数,仅作用于 1-RTT 以消除握手的长度特征,双端默认值均为 "100-111-1111.75-0-111.50-0-3333": + # 在 1-RTT client/server hello 后以 100% 的概率粘上随机 111 到 1111 字节的 padding + # 以 75% 的概率等待随机 0 到 111 毫秒("probability-from-to") + # 再次以 50% 的概率发送随机 0 到 3333 字节的 padding(若为 0 则不 Write()) + # 服务端、客户端可以设置不同的 padding 参数,按 len、gap 的顺序无限串联,第一个 padding 需概率 100%、至少 35 字节 # ------------------------- - # decryption: "mlkem768x25519plus.native/xorpub/random.1rtt/600s.(X25519 PrivateKey).(ML-KEM-768 Seed)..." + # decryption: "mlkem768x25519plus.native/xorpub/random.600s(300-600s)/0s.(padding len).(padding gap).(X25519 PrivateKey).(ML-KEM-768 Seed)..." # 下面两项如果填写则开启 tls(需要同时填写) - # certificate: ./server.crt - # private-key: ./server.key + # certificate: ./server.crt # 证书 PEM 格式,或者 证书的路径 + # private-key: ./server.key # 证书对应的私钥 PEM 格式,或者私钥路径 + # 下面两项为mTLS配置项,如果client-auth-type设置为 "verify-if-given" 或 "require-and-verify" 则client-auth-cert必须不为空 + # client-auth-type: "" # 可选值:""、"request"、"require-any"、"verify-if-given"、"require-and-verify" + # client-auth-cert: string # 证书 PEM 格式,或者 证书的路径 # 如果填写则开启ech(可由 mihomo generate ech-keypair <明文域名> 生成) # ech-key: | # -----BEGIN ECH KEYS----- @@ -1407,8 +1541,11 @@ listeners: username1: password1 username2: password2 # "certificate" and "private-key" are required - certificate: ./server.crt + certificate: ./server.crt # 证书 PEM 格式,或者 证书的路径 private-key: ./server.key + # 下面两项为mTLS配置项,如果client-auth-type设置为 "verify-if-given" 或 "require-and-verify" 则client-auth-cert必须不为空 + # client-auth-type: "" # 可选值:""、"request"、"require-any"、"verify-if-given"、"require-and-verify" + # client-auth-cert: string # 证书 PEM 格式,或者 证书的路径 # 如果填写则开启ech(可由 mihomo generate ech-keypair <明文域名> 生成) # ech-key: | # -----BEGIN ECH KEYS----- @@ -1430,8 +1567,11 @@ listeners: # ws-path: "/" # 如果不为空则开启 websocket 传输层 # grpc-service-name: "GunService" # 如果不为空则开启 grpc 传输层 # 下面两项如果填写则开启 tls(需要同时填写) - certificate: ./server.crt - private-key: ./server.key + certificate: ./server.crt # 证书 PEM 格式,或者 证书的路径 + private-key: ./server.key # 证书对应的私钥 PEM 格式,或者私钥路径 + # 下面两项为mTLS配置项,如果client-auth-type设置为 "verify-if-given" 或 "require-and-verify" 则client-auth-cert必须不为空 + # client-auth-type: "" # 可选值:""、"request"、"require-any"、"verify-if-given"、"require-and-verify" + # client-auth-cert: string # 证书 PEM 格式,或者 证书的路径 # 如果填写则开启ech(可由 mihomo generate ech-keypair <明文域名> 生成) # ech-key: | # -----BEGIN ECH KEYS----- @@ -1472,8 +1612,11 @@ listeners: users: 00000000-0000-0000-0000-000000000000: PASSWORD_0 00000000-0000-0000-0000-000000000001: PASSWORD_1 - # certificate: ./server.crt - # private-key: ./server.key + # certificate: ./server.crt # 证书 PEM 格式,或者 证书的路径 + # private-key: ./server.key # 证书对应的私钥 PEM 格式,或者私钥路径 + # 下面两项为mTLS配置项,如果client-auth-type设置为 "verify-if-given" 或 "require-and-verify" 则client-auth-cert必须不为空 + # client-auth-type: "" # 可选值:""、"request"、"require-any"、"verify-if-given"、"require-and-verify" + # client-auth-cert: string # 证书 PEM 格式,或者 证书的路径 # 如果填写则开启ech(可由 mihomo generate ech-keypair <明文域名> 生成) # ech-key: | # -----BEGIN ECH KEYS----- @@ -1537,6 +1680,7 @@ listeners: # - com.android.chrome # exclude-package: # 排除被路由的 Android 应用包名 # - com.android.captiveportallogin + # disable-icmp-forwarding: true # 禁用 ICMP 转发,防止某些情况下的 ICMP 环回问题,ping 将不会显示真实的延迟 # 入口配置与 Listener 等价,传入流量将和 socks,mixed 等入口一样按照 mode 所指定的方式进行匹配处理 # shadowsocks,vmess 入口配置(传入流量将和 socks,mixed 等入口一样按照 mode 所指定的方式进行匹配处理) # ss-config: ss://2022-blake3-aes-256-gcm:vlmpIPSyHH6f4S8WVPdRIHIlzmB+GIRfoH3aNJ/t9Gg=@:23456 diff --git a/go.mod b/go.mod index 834bac83cf..6b7a8d6997 100644 --- a/go.mod +++ b/go.mod @@ -6,36 +6,39 @@ require ( github.com/bahlo/generic-list-go v0.2.0 github.com/coreos/go-iptables v0.8.0 github.com/dlclark/regexp2 v1.11.5 - github.com/enfein/mieru/v3 v3.19.1 + github.com/ebitengine/purego v0.9.0 + github.com/enfein/mieru/v3 v3.20.0 github.com/go-chi/chi/v5 v5.2.3 github.com/go-chi/render v1.0.3 github.com/gobwas/ws v1.4.0 github.com/gofrs/uuid/v5 v5.3.2 + github.com/golang/snappy v1.0.0 github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905 github.com/klauspost/compress v1.17.9 // lastest version compatible with golang1.20 github.com/mdlayher/netlink v1.7.2 - github.com/metacubex/amneziawg-go v0.0.0-20250820070344-732c0c9d418a - github.com/metacubex/bart v0.20.5 + github.com/metacubex/amneziawg-go v0.0.0-20250902133113-a7f637c14281 + github.com/metacubex/bart v0.24.0 github.com/metacubex/bbolt v0.0.0-20250725135710-010dbbbb7a5b github.com/metacubex/blake3 v0.1.0 github.com/metacubex/chacha v0.1.5 github.com/metacubex/fswatch v0.1.1 github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 + github.com/metacubex/kcp-go v0.0.0-20250923001605-1ba6f691c45b github.com/metacubex/quic-go v0.54.1-0.20250730114134-a1ae705fe295 github.com/metacubex/randv2 v0.2.0 github.com/metacubex/restls-client-go v0.1.7 - github.com/metacubex/sing v0.5.5 - github.com/metacubex/sing-mux v0.3.3-0.20250813083925-d7c9aeaeeaac - github.com/metacubex/sing-quic v0.0.0-20250718154553-1b193bec4cbb + github.com/metacubex/sing v0.5.6 + github.com/metacubex/sing-mux v0.3.4 + github.com/metacubex/sing-quic v0.0.0-20250909002258-06122df8f231 github.com/metacubex/sing-shadowsocks v0.2.12 - github.com/metacubex/sing-shadowsocks2 v0.2.6 + github.com/metacubex/sing-shadowsocks2 v0.2.7 github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 - github.com/metacubex/sing-tun v0.4.7 - github.com/metacubex/sing-vmess v0.2.4-0.20250822020810-4856053566f0 + github.com/metacubex/sing-tun v0.4.8 + github.com/metacubex/sing-vmess v0.2.4 github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f - github.com/metacubex/smux v0.0.0-20250503055512-501391591dee - github.com/metacubex/tfo-go v0.0.0-20250827083229-aa432b865617 - github.com/metacubex/utls v1.8.1-0.20250823120917-12f5ba126142 + github.com/metacubex/smux v0.0.0-20250922175018-15c9a6a78719 + github.com/metacubex/tfo-go v0.0.0-20250921095601-b102db4216c0 + github.com/metacubex/utls v1.8.1 github.com/metacubex/wireguard-go v0.0.0-20250820062549-a6cecdd7f57f github.com/miekg/dns v1.1.63 // lastest version compatible with golang1.20 github.com/mroth/weightedrand/v2 v2.1.0 @@ -44,9 +47,8 @@ require ( github.com/sagernet/cors v1.2.1 github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a github.com/samber/lo v1.51.0 - github.com/shirou/gopsutil/v4 v4.25.1 // lastest version compatible with golang1.20 github.com/sirupsen/logrus v1.9.3 - github.com/stretchr/testify v1.11.0 + github.com/stretchr/testify v1.11.1 github.com/vmihailenco/msgpack/v5 v5.4.1 github.com/wk8/go-ordered-map/v2 v2.1.8 gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7 @@ -68,7 +70,6 @@ require ( github.com/andybalholm/brotli v1.0.6 // indirect github.com/buger/jsonparser v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/ebitengine/purego v0.8.4 // indirect github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 // indirect github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect @@ -82,30 +83,27 @@ require ( github.com/google/btree v1.1.3 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect - github.com/hashicorp/yamux v0.1.2 // indirect github.com/josharian/native v1.1.0 // indirect + github.com/klauspost/cpuid/v2 v2.2.6 // indirect + github.com/klauspost/reedsolomon v1.12.3 // indirect github.com/kr/text v0.2.0 // indirect - github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mdlayher/socket v0.4.1 // indirect github.com/metacubex/ascon v0.1.0 // indirect - github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b // indirect + github.com/metacubex/gvisor v0.0.0-20250919004547-6122b699a301 // indirect github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793 // indirect + github.com/metacubex/yamux v0.0.0-20250918083631-dd5f17c0be49 // indirect github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 // indirect github.com/onsi/ginkgo/v2 v2.9.5 // indirect github.com/pierrec/lz4/v4 v4.1.14 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/quic-go/qpack v0.4.0 // indirect github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b // indirect github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c // indirect github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e // indirect - github.com/tklauser/go-sysconf v0.3.12 // indirect - github.com/tklauser/numcpus v0.6.1 // indirect github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect github.com/vishvananda/netns v0.0.4 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect - github.com/yusufpapurcu/wmi v1.2.4 // indirect gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect go.uber.org/mock v0.4.0 // indirect golang.org/x/mod v0.20.0 // indirect diff --git a/go.sum b/go.sum index 15b38771a1..271e8c1c80 100644 --- a/go.sum +++ b/go.sum @@ -23,10 +23,10 @@ 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.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= -github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= -github.com/enfein/mieru/v3 v3.19.1 h1:19b9kgFC7oJXX9RLEO5Pi1gO6yek5cWlpK7IJVUoE8I= -github.com/enfein/mieru/v3 v3.19.1/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM= +github.com/ebitengine/purego v0.9.0 h1:mh0zpKBIXDceC63hpvPuGLiJ8ZAa3DfrFTudmfi8A4k= +github.com/ebitengine/purego v0.9.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/enfein/mieru/v3 v3.20.0 h1:1ob7pCIVSH5FYFAfYvim8isLW1vBOS4cFOUF9exJS38= +github.com/enfein/mieru/v3 v3.20.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM= github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 h1:kXYqH/sL8dS/FdoFjr12ePjnLPorPo2FsnrHNuXSDyo= github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I= github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g= @@ -45,7 +45,6 @@ github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hH github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4= github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= -github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= @@ -60,16 +59,15 @@ github.com/gofrs/uuid/v5 v5.3.2 h1:2jfO8j3XgSwlz/wHqemAEugfnTlikAYHhnqQ8Xh4fE0= github.com/gofrs/uuid/v5 v5.3.2/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/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 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= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/tink/go v1.6.1 h1:t7JHqO8Ath2w2ig5vjwQYJzhGEZymedQc90lQXUBa4I= -github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= -github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905 h1:q3OEI9RaN/wwcx+qgGo6ZaoJkCiDYe/gjDLfq7lQQF4= github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905/go.mod h1:VvGYjkZoJyKqlmT1yzakUs4mfKMNB0XdODP0+rdml6k= @@ -79,23 +77,25 @@ github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtL github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= +github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/klauspost/reedsolomon v1.12.3 h1:tzUznbfc3OFwJaTebv/QdhnFf2Xvb7gZ24XaHLBPmdc= +github.com/klauspost/reedsolomon v1.12.3/go.mod h1:3K5rXwABAvzGeR01r6pWZieUALXO/Tq7bFKGIb4m4WI= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= -github.com/metacubex/amneziawg-go v0.0.0-20250820070344-732c0c9d418a h1:c1QSGpacSeQdBdWcEKZKGuWLcqIG2wxHEygAcXuDwS4= -github.com/metacubex/amneziawg-go v0.0.0-20250820070344-732c0c9d418a/go.mod h1:MsM/5czONyXMJ3PRr5DbQ4O/BxzAnJWOIcJdLzW6qHY= +github.com/metacubex/amneziawg-go v0.0.0-20250902133113-a7f637c14281 h1:09EM0sOLb2kfL0KETGhHujsBLB5iy5U/2yHRHsxf/pI= +github.com/metacubex/amneziawg-go v0.0.0-20250902133113-a7f637c14281/go.mod h1:MsM/5czONyXMJ3PRr5DbQ4O/BxzAnJWOIcJdLzW6qHY= github.com/metacubex/ascon v0.1.0 h1:6ZWxmXYszT1XXtwkf6nxfFhc/OTtQ9R3Vyj1jN32lGM= github.com/metacubex/ascon v0.1.0/go.mod h1:eV5oim4cVPPdEL8/EYaTZ0iIKARH9pnhAK/fcT5Kacc= -github.com/metacubex/bart v0.20.5 h1:XkgLZ17QxfxkqKdGsojoM2Zu01mmHyyQSFzt2/calTM= -github.com/metacubex/bart v0.20.5/go.mod h1:DCcyfP4MC+Zy7sLK7XeGuMw+P5K9mIRsYOBgiE8icsI= +github.com/metacubex/bart v0.24.0 h1:EyNiPeVOlg0joSHTzi5oentI0j5M89utUq/5dd76pWM= +github.com/metacubex/bart v0.24.0/go.mod h1:DCcyfP4MC+Zy7sLK7XeGuMw+P5K9mIRsYOBgiE8icsI= github.com/metacubex/bbolt v0.0.0-20250725135710-010dbbbb7a5b h1:j7dadXD8I2KTmMt8jg1JcaP1ANL3JEObJPdANKcSYPY= github.com/metacubex/bbolt v0.0.0-20250725135710-010dbbbb7a5b/go.mod h1:+WmP0VJZDkDszvpa83HzfUp6QzARl/IKkMorH4+nODw= github.com/metacubex/blake3 v0.1.0 h1:KGnjh/56REO7U+cgZA8dnBhxdP7jByrG7hTP+bu6cqY= @@ -106,8 +106,10 @@ github.com/metacubex/fswatch v0.1.1 h1:jqU7C/v+g0qc2RUFgmAOPoVvfl2BXXUXEumn6oQux github.com/metacubex/fswatch v0.1.1/go.mod h1:czrTT7Zlbz7vWft8RQu9Qqh+JoX+Nnb+UabuyN1YsgI= github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI= github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88= -github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b h1:RUh4OdVPz/jDrM9MQ2ySuqu2aeBqcA8rtfWUYLZ8RtI= -github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b/go.mod h1:8LpS0IJW1VmWzUm3ylb0e2SK5QDm5lO/2qwWLZgRpBU= +github.com/metacubex/gvisor v0.0.0-20250919004547-6122b699a301 h1:N5GExQJqYAH3gOCshpp2u/J3CtNYzMctmlb0xK9wtbQ= +github.com/metacubex/gvisor v0.0.0-20250919004547-6122b699a301/go.mod h1:8LpS0IJW1VmWzUm3ylb0e2SK5QDm5lO/2qwWLZgRpBU= +github.com/metacubex/kcp-go v0.0.0-20250923001605-1ba6f691c45b h1:z7JLKjugnQ1qvDOAD8yMA5C8AlJY3bG+VrrgRntRlUY= +github.com/metacubex/kcp-go v0.0.0-20250923001605-1ba6f691c45b/go.mod h1:HIJZW4QMhbBqXuqC1ly6Hn0TEYT2SzRw58ns1yGhXTs= github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793 h1:1Qpuy+sU3DmyX9HwI+CrBT/oLNJngvBorR2RbajJcqo= github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793/go.mod h1:RjRNb4G52yAgfR+Oe/kp9G4PJJ97Fnj89eY1BFO3YyA= github.com/metacubex/quic-go v0.54.1-0.20250730114134-a1ae705fe295 h1:8JVlYuE8uSJAvmyCd4TjvDxs57xjb0WxEoaWafK5+qs= @@ -117,32 +119,34 @@ github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFq github.com/metacubex/restls-client-go v0.1.7 h1:eCwiXCTQb5WJu9IlgYvDBA1OgrINv58dEe7hcN5H15k= github.com/metacubex/restls-client-go v0.1.7/go.mod h1:BN/U52vPw7j8VTSh2vleD/MnmVKCov84mS5VcjVHH4g= github.com/metacubex/sing v0.5.2/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w= -github.com/metacubex/sing v0.5.5 h1:m5U8iHvRAUxlme3FZlE/LPIGHjU8oMCUzXWGbQQAC1E= -github.com/metacubex/sing v0.5.5/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w= -github.com/metacubex/sing-mux v0.3.3-0.20250813083925-d7c9aeaeeaac h1:wDH/Jh/yqWbzPktqJP+Y1cUG8hchcrzKzUxJiSpnaQs= -github.com/metacubex/sing-mux v0.3.3-0.20250813083925-d7c9aeaeeaac/go.mod h1:3rt1soewn0O6j89GCLmwAQFsq257u0jf2zQSPhTL3Bw= -github.com/metacubex/sing-quic v0.0.0-20250718154553-1b193bec4cbb h1:U/m3h8lp/j7i8zFgfvScLdZa1/Y8dd74oO7iZaQq80s= -github.com/metacubex/sing-quic v0.0.0-20250718154553-1b193bec4cbb/go.mod h1:B60FxaPHjR1SeQB0IiLrgwgvKsaoASfOWdiqhLjmMGA= +github.com/metacubex/sing v0.5.6 h1:mEPDCadsCj3DB8gn+t/EtposlYuALEkExa/LUguw6/c= +github.com/metacubex/sing v0.5.6/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w= +github.com/metacubex/sing-mux v0.3.4 h1:tf4r27CIkzaxq9kBlAXQkgMXq2HPp5Mta60Kb4RCZF0= +github.com/metacubex/sing-mux v0.3.4/go.mod h1:SEJfAuykNj/ozbPqngEYqyggwSr81+L7Nu09NRD5mh4= +github.com/metacubex/sing-quic v0.0.0-20250909002258-06122df8f231 h1:dGvo7UahC/gYBQNBoictr14baJzBjAKUAorP63QFFtg= +github.com/metacubex/sing-quic v0.0.0-20250909002258-06122df8f231/go.mod h1:B60FxaPHjR1SeQB0IiLrgwgvKsaoASfOWdiqhLjmMGA= github.com/metacubex/sing-shadowsocks v0.2.12 h1:Wqzo8bYXrK5aWqxu/TjlTnYZzAKtKsaFQBdr6IHFaBE= github.com/metacubex/sing-shadowsocks v0.2.12/go.mod h1:2e5EIaw0rxKrm1YTRmiMnDulwbGxH9hAFlrwQLQMQkU= -github.com/metacubex/sing-shadowsocks2 v0.2.6 h1:ZR1kYT0f0Vi64iQSS09OdhFfppiNkh7kjgRdMm0SB98= -github.com/metacubex/sing-shadowsocks2 v0.2.6/go.mod h1:vOEbfKC60txi0ca+yUlqEwOGc3Obl6cnSgx9Gf45KjE= +github.com/metacubex/sing-shadowsocks2 v0.2.7 h1:hSuuc0YpsfiqYqt1o+fP4m34BQz4e6wVj3PPBVhor3A= +github.com/metacubex/sing-shadowsocks2 v0.2.7/go.mod h1:vOEbfKC60txi0ca+yUlqEwOGc3Obl6cnSgx9Gf45KjE= github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 h1:gXU+MYPm7Wme3/OAY2FFzVq9d9GxPHOqu5AQfg/ddhI= github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2/go.mod h1:mbfboaXauKJNIHJYxQRa+NJs4JU9NZfkA+I33dS2+9E= -github.com/metacubex/sing-tun v0.4.7 h1:ZDY/W+1c7PeWWKeKRyUo18fySF/TWjB0i5ui81Ar778= -github.com/metacubex/sing-tun v0.4.7/go.mod h1:xHecZRwBnKWe6zG9amAK9cXf91lF6blgjBqm+VvOrmU= -github.com/metacubex/sing-vmess v0.2.4-0.20250822020810-4856053566f0 h1:WZepq4TOZa6WewB8tGAZrrL+bL2R2ivoBzuEgAeolWc= -github.com/metacubex/sing-vmess v0.2.4-0.20250822020810-4856053566f0/go.mod h1:21R5R1u90uUvBQF0owoooEu96/SAYYD56nDrwm6nFaM= +github.com/metacubex/sing-tun v0.4.8 h1:3PyiUKWXYi37yHptXskzL1723O3OUdyt0Aej4XHVikM= +github.com/metacubex/sing-tun v0.4.8/go.mod h1:L/TjQY5JEGy8nvsuYmy/XgMFMCPiF0+AWSFCYfS6r9w= +github.com/metacubex/sing-vmess v0.2.4 h1:Tx6AGgCiEf400E/xyDuYyafsel6sGbR8oF7RkAaus6I= +github.com/metacubex/sing-vmess v0.2.4/go.mod h1:21R5R1u90uUvBQF0owoooEu96/SAYYD56nDrwm6nFaM= github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f h1:Sr/DYKYofKHKc4GF3qkRGNuj6XA6c0eqPgEDN+VAsYU= github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f/go.mod h1:jpAkVLPnCpGSfNyVmj6Cq4YbuZsFepm/Dc+9BAOcR80= -github.com/metacubex/smux v0.0.0-20250503055512-501391591dee h1:lp6hJ+4wCLZu113awp7P6odM2okB5s60HUyF0FMqKmo= -github.com/metacubex/smux v0.0.0-20250503055512-501391591dee/go.mod h1:4bPD8HWx9jPJ9aE4uadgyN7D1/Wz3KmPy+vale8sKLE= -github.com/metacubex/tfo-go v0.0.0-20250827083229-aa432b865617 h1:yN3mQ4cT9sPUciw/rO0Isc/8QlO86DB6g9SEMRgQ8Cw= -github.com/metacubex/tfo-go v0.0.0-20250827083229-aa432b865617/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw= -github.com/metacubex/utls v1.8.1-0.20250823120917-12f5ba126142 h1:csEbKOzRAxJXffOeZnnS3/kA/F55JiTbKv5jcYqCXms= -github.com/metacubex/utls v1.8.1-0.20250823120917-12f5ba126142/go.mod h1:67I3skhEY4Sya8f1YxELwWPoeQdXqZCrWNYLvq8gn2U= +github.com/metacubex/smux v0.0.0-20250922175018-15c9a6a78719 h1:T6qCCfolRDAVJKeaPW/mXwNLjnlo65AYN7WS2jrBNaM= +github.com/metacubex/smux v0.0.0-20250922175018-15c9a6a78719/go.mod h1:4bPD8HWx9jPJ9aE4uadgyN7D1/Wz3KmPy+vale8sKLE= +github.com/metacubex/tfo-go v0.0.0-20250921095601-b102db4216c0 h1:Ui+/2s5Qz0lSnDUBmEL12M5Oi/PzvFxGTNohm8ZcsmE= +github.com/metacubex/tfo-go v0.0.0-20250921095601-b102db4216c0/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw= +github.com/metacubex/utls v1.8.1 h1:RW8GeCGWAegjV0HW5nw9DoqNoeGAXXeYUP6AysmRvx4= +github.com/metacubex/utls v1.8.1/go.mod h1:kncGGVhFaoGn5M3pFe3SXhZCzsbCJayNOH4UEqTKTko= github.com/metacubex/wireguard-go v0.0.0-20250820062549-a6cecdd7f57f h1:FGBPRb1zUabhPhDrlKEjQ9lgIwQ6cHL4x8M9lrERhbk= github.com/metacubex/wireguard-go v0.0.0-20250820062549-a6cecdd7f57f/go.mod h1:oPGcV994OGJedmmxrcK9+ni7jUEMGhR+uVQAdaduIP4= +github.com/metacubex/yamux v0.0.0-20250918083631-dd5f17c0be49 h1:lhlqpYHopuTLx9xQt22kSA9HtnyTDmk5XjjQVCGHe2E= +github.com/metacubex/yamux v0.0.0-20250918083631-dd5f17c0be49/go.mod h1:MBeEa9IVBphH7vc3LNtW6ZujVXFizotPo3OEiHQ+TNU= github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY= github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs= github.com/mroth/weightedrand/v2 v2.1.0 h1:o1ascnB1CIVzsqlfArQQjeMy1U0NcIbBO5rfd5E/OeU= @@ -164,8 +168,6 @@ github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFu github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= -github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= @@ -175,8 +177,6 @@ github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZN github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= github.com/samber/lo v1.51.0 h1:kysRYLbHy/MB7kQZf5DSN50JHmMsNEdeY24VzJFu7wI= github.com/samber/lo v1.51.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= -github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs= -github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI= github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b h1:rXHg9GrUEtWZhEkrykicdND3VPjlVbYiLdX9J7gimS8= github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b/go.mod h1:X7qrxNQViEaAN9LNZOPl9PfvQtp3V3c7LTo0dvGi0fM= github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c h1:DjKMC30y6yjG3IxDaeAj3PCoRr+IsO+bzyT+Se2m2Hk= @@ -196,12 +196,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8= -github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= -github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= -github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= -github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 h1:tHNk7XK9GkmKUR6Gh8gVBKXc2MVSZ4G/NnWLtzw4gNA= github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264= github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= @@ -215,8 +211,7 @@ github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAh github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= -github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= -github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae h1:J0GxkO96kL4WF+AIT3M4mfUVinOCPgf2uUWYFUzN0sM= gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7 h1:UNrDfkQqiEYzdMlNsVvBYOAJWZjdktqFE9tQh5BT2+4= gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7/go.mod h1:E+rxHvJG9H6PUdzq9NRG6csuLN3XUx98BfGOVWNYnXs= gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec h1:FpfFs4EhNehiVfzQttTuxanPIT43FtkkCFypIod8LHo= @@ -248,16 +243,13 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= @@ -271,7 +263,6 @@ golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapK 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/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= diff --git a/hub/executor/executor.go b/hub/executor/executor.go index c6bef52523..fcf176e0e0 100644 --- a/hub/executor/executor.go +++ b/hub/executor/executor.go @@ -231,6 +231,8 @@ func updateNTP(c *config.NTP) { c.DialerProxy, c.WriteToSystem, ) + } else { + ntp.ReCreateNTPService("", 0, "", false) } } diff --git a/hub/hub.go b/hub/hub.go index fc4fe81a5d..d751f99c27 100644 --- a/hub/hub.go +++ b/hub/hub.go @@ -50,16 +50,18 @@ func applyRoute(cfg *config.Config) { route.SetUIPath(cfg.Controller.ExternalUI) } route.ReCreateServer(&route.Config{ - Addr: cfg.Controller.ExternalController, - TLSAddr: cfg.Controller.ExternalControllerTLS, - UnixAddr: cfg.Controller.ExternalControllerUnix, - PipeAddr: cfg.Controller.ExternalControllerPipe, - Secret: cfg.Controller.Secret, - Certificate: cfg.TLS.Certificate, - PrivateKey: cfg.TLS.PrivateKey, - EchKey: cfg.TLS.EchKey, - DohServer: cfg.Controller.ExternalDohServer, - IsDebug: cfg.General.LogLevel == log.DEBUG, + Addr: cfg.Controller.ExternalController, + TLSAddr: cfg.Controller.ExternalControllerTLS, + UnixAddr: cfg.Controller.ExternalControllerUnix, + PipeAddr: cfg.Controller.ExternalControllerPipe, + Secret: cfg.Controller.Secret, + Certificate: cfg.TLS.Certificate, + PrivateKey: cfg.TLS.PrivateKey, + ClientAuthType: cfg.TLS.ClientAuthType, + ClientAuthCert: cfg.TLS.ClientAuthCert, + EchKey: cfg.TLS.EchKey, + DohServer: cfg.Controller.ExternalDohServer, + IsDebug: cfg.General.LogLevel == log.DEBUG, Cors: route.Cors{ AllowOrigins: cfg.Controller.Cors.AllowOrigins, AllowPrivateNetwork: cfg.Controller.Cors.AllowPrivateNetwork, diff --git a/hub/route/server.go b/hub/route/server.go index 94e3a9224e..f2a52d4208 100644 --- a/hub/route/server.go +++ b/hub/route/server.go @@ -20,6 +20,7 @@ import ( tlsC "github.com/metacubex/mihomo/component/tls" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/log" + "github.com/metacubex/mihomo/ntp" "github.com/metacubex/mihomo/tunnel/statistic" "github.com/go-chi/chi/v5" @@ -56,17 +57,19 @@ type Memory struct { } type Config struct { - Addr string - TLSAddr string - UnixAddr string - PipeAddr string - Secret string - Certificate string - PrivateKey string - EchKey string - DohServer string - IsDebug bool - Cors Cors + Addr string + TLSAddr string + UnixAddr string + PipeAddr string + Secret string + Certificate string + PrivateKey string + ClientAuthType string + ClientAuthCert string + EchKey string + DohServer string + IsDebug bool + Cors Cors } type Cors struct { @@ -201,9 +204,23 @@ func startTLS(cfg *Config) { } log.Infoln("RESTful API tls listening at: %s", l.Addr().String()) - tlsConfig := &tlsC.Config{} + tlsConfig := &tlsC.Config{Time: ntp.Now} tlsConfig.NextProtos = []string{"h2", "http/1.1"} tlsConfig.Certificates = []tlsC.Certificate{tlsC.UCertificate(cert)} + tlsConfig.ClientAuth = tlsC.ClientAuthTypeFromString(cfg.ClientAuthType) + if len(cfg.ClientAuthCert) > 0 { + if tlsConfig.ClientAuth == tlsC.NoClientCert { + tlsConfig.ClientAuth = tlsC.RequireAndVerifyClientCert + } + } + if tlsConfig.ClientAuth == tlsC.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tlsC.RequireAndVerifyClientCert { + pool, err := ca.LoadCertificates(cfg.ClientAuthCert, C.Path) + if err != nil { + log.Errorln("External controller tls listen error: %s", err) + return + } + tlsConfig.ClientCAs = pool + } if cfg.EchKey != "" { err = ech.LoadECHKey(cfg.EchKey, tlsConfig, C.Path) diff --git a/listener/anytls/server.go b/listener/anytls/server.go index ccbc47a2c0..99f1d7bb56 100644 --- a/listener/anytls/server.go +++ b/listener/anytls/server.go @@ -17,6 +17,7 @@ import ( C "github.com/metacubex/mihomo/constant" LC "github.com/metacubex/mihomo/listener/config" "github.com/metacubex/mihomo/listener/sing" + "github.com/metacubex/mihomo/ntp" "github.com/metacubex/mihomo/transport/anytls/padding" "github.com/metacubex/mihomo/transport/anytls/session" @@ -42,7 +43,7 @@ func New(config LC.AnyTLSServer, tunnel C.Tunnel, additions ...inbound.Addition) } } - tlsConfig := &tlsC.Config{} + tlsConfig := &tlsC.Config{Time: ntp.Now} if config.Certificate != "" && config.PrivateKey != "" { cert, err := ca.LoadTLSKeyPair(config.Certificate, config.PrivateKey, C.Path) if err != nil { @@ -57,6 +58,19 @@ func New(config LC.AnyTLSServer, tunnel C.Tunnel, additions ...inbound.Addition) } } } + tlsConfig.ClientAuth = tlsC.ClientAuthTypeFromString(config.ClientAuthType) + if len(config.ClientAuthCert) > 0 { + if tlsConfig.ClientAuth == tlsC.NoClientCert { + tlsConfig.ClientAuth = tlsC.RequireAndVerifyClientCert + } + } + if tlsConfig.ClientAuth == tlsC.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tlsC.RequireAndVerifyClientCert { + pool, err := ca.LoadCertificates(config.ClientAuthCert, C.Path) + if err != nil { + return nil, err + } + tlsConfig.ClientCAs = pool + } sl = &Listener{ config: config, diff --git a/listener/config/anytls.go b/listener/config/anytls.go index 874b796404..723d2d4f8c 100644 --- a/listener/config/anytls.go +++ b/listener/config/anytls.go @@ -5,13 +5,15 @@ import ( ) type AnyTLSServer struct { - Enable bool `yaml:"enable" json:"enable"` - Listen string `yaml:"listen" json:"listen"` - Users map[string]string `yaml:"users" json:"users,omitempty"` - Certificate string `yaml:"certificate" json:"certificate"` - PrivateKey string `yaml:"private-key" json:"private-key"` - EchKey string `yaml:"ech-key" json:"ech-key"` - PaddingScheme string `yaml:"padding-scheme" json:"padding-scheme,omitempty"` + Enable bool `yaml:"enable" json:"enable"` + Listen string `yaml:"listen" json:"listen"` + Users map[string]string `yaml:"users" json:"users,omitempty"` + Certificate string `yaml:"certificate" json:"certificate"` + PrivateKey string `yaml:"private-key" json:"private-key"` + ClientAuthType string `yaml:"client-auth-type" json:"client-auth-type,omitempty"` + ClientAuthCert string `yaml:"client-auth-cert" json:"client-auth-cert,omitempty"` + EchKey string `yaml:"ech-key" json:"ech-key"` + PaddingScheme string `yaml:"padding-scheme" json:"padding-scheme,omitempty"` } func (t AnyTLSServer) String() string { diff --git a/listener/config/auth.go b/listener/config/auth.go index cd0430ec83..60f8ac4b43 100644 --- a/listener/config/auth.go +++ b/listener/config/auth.go @@ -7,11 +7,13 @@ import ( // AuthServer for http/socks/mixed server type AuthServer struct { - Enable bool - Listen string - AuthStore auth.AuthStore - Certificate string - PrivateKey string - EchKey string - RealityConfig reality.Config + Enable bool + Listen string + AuthStore auth.AuthStore + Certificate string + PrivateKey string + ClientAuthType string + ClientAuthCert string + EchKey string + RealityConfig reality.Config } diff --git a/listener/config/hysteria2.go b/listener/config/hysteria2.go index e8042b0d5d..2b8312a9ad 100644 --- a/listener/config/hysteria2.go +++ b/listener/config/hysteria2.go @@ -14,6 +14,8 @@ type Hysteria2Server struct { ObfsPassword string `yaml:"obfs-password" json:"obfs-password,omitempty"` Certificate string `yaml:"certificate" json:"certificate"` PrivateKey string `yaml:"private-key" json:"private-key"` + ClientAuthType string `yaml:"client-auth-type" json:"client-auth-type,omitempty"` + ClientAuthCert string `yaml:"client-auth-cert" json:"client-auth-cert,omitempty"` EchKey string `yaml:"ech-key" json:"ech-key,omitempty"` MaxIdleTime int `yaml:"max-idle-time" json:"max-idle-time,omitempty"` ALPN []string `yaml:"alpn" json:"alpn,omitempty"` diff --git a/listener/config/kcptun.go b/listener/config/kcptun.go new file mode 100644 index 0000000000..02293dfaed --- /dev/null +++ b/listener/config/kcptun.go @@ -0,0 +1,8 @@ +package config + +import "github.com/metacubex/mihomo/transport/kcptun" + +type KcpTun struct { + Enable bool `json:"enable"` + kcptun.Config `json:",inline"` +} diff --git a/listener/config/shadowsocks.go b/listener/config/shadowsocks.go index 442743ef78..37bbb72119 100644 --- a/listener/config/shadowsocks.go +++ b/listener/config/shadowsocks.go @@ -14,6 +14,7 @@ type ShadowsocksServer struct { Udp bool MuxOption sing.MuxOption `yaml:"mux-option" json:"mux-option,omitempty"` ShadowTLS ShadowTLS `yaml:"shadow-tls" json:"shadow-tls,omitempty"` + KcpTun KcpTun `yaml:"kcp-tun" json:"kcp-tun,omitempty"` } func (t ShadowsocksServer) String() string { diff --git a/listener/config/trojan.go b/listener/config/trojan.go index e38a2022c8..40ba3dad31 100644 --- a/listener/config/trojan.go +++ b/listener/config/trojan.go @@ -20,6 +20,8 @@ type TrojanServer struct { GrpcServiceName string Certificate string PrivateKey string + ClientAuthType string + ClientAuthCert string EchKey string RealityConfig reality.Config MuxOption sing.MuxOption diff --git a/listener/config/tuic.go b/listener/config/tuic.go index d923e9a02a..ad85b54145 100644 --- a/listener/config/tuic.go +++ b/listener/config/tuic.go @@ -13,6 +13,8 @@ type TuicServer struct { Users map[string]string `yaml:"users" json:"users,omitempty"` Certificate string `yaml:"certificate" json:"certificate"` PrivateKey string `yaml:"private-key" json:"private-key"` + ClientAuthType string `yaml:"client-auth-type" json:"client-auth-type,omitempty"` + ClientAuthCert string `yaml:"client-auth-cert" json:"client-auth-cert,omitempty"` EchKey string `yaml:"ech-key" json:"ech-key"` CongestionController string `yaml:"congestion-controller" json:"congestion-controller,omitempty"` MaxIdleTime int `yaml:"max-idle-time" json:"max-idle-time,omitempty"` diff --git a/listener/config/tun.go b/listener/config/tun.go index 0efbc82789..0e26232984 100644 --- a/listener/config/tun.go +++ b/listener/config/tun.go @@ -48,6 +48,7 @@ type Tun struct { ExcludePackage []string `yaml:"exclude-package" json:"exclude-package,omitempty"` EndpointIndependentNat bool `yaml:"endpoint-independent-nat" json:"endpoint-independent-nat,omitempty"` UDPTimeout int64 `yaml:"udp-timeout" json:"udp-timeout,omitempty"` + DisableICMPForwarding bool `yaml:"disable-icmp-forwarding" json:"disable-icmp-forwarding,omitempty"` FileDescriptor int `yaml:"file-descriptor" json:"file-descriptor"` Inet4RouteAddress []netip.Prefix `yaml:"inet4-route-address" json:"inet4-route-address,omitempty"` @@ -186,6 +187,9 @@ func (t *Tun) Equal(other Tun) bool { if t.UDPTimeout != other.UDPTimeout { return false } + if t.DisableICMPForwarding != other.DisableICMPForwarding { + return false + } if t.FileDescriptor != other.FileDescriptor { return false } diff --git a/listener/config/vless.go b/listener/config/vless.go index 135747da42..7facaf25f3 100644 --- a/listener/config/vless.go +++ b/listener/config/vless.go @@ -22,6 +22,8 @@ type VlessServer struct { GrpcServiceName string Certificate string PrivateKey string + ClientAuthType string + ClientAuthCert string EchKey string RealityConfig reality.Config MuxOption sing.MuxOption `yaml:"mux-option" json:"mux-option,omitempty"` diff --git a/listener/config/vmess.go b/listener/config/vmess.go index 2a0e0054d2..5883bd7d2b 100644 --- a/listener/config/vmess.go +++ b/listener/config/vmess.go @@ -21,6 +21,8 @@ type VmessServer struct { GrpcServiceName string Certificate string PrivateKey string + ClientAuthType string + ClientAuthCert string EchKey string RealityConfig reality.Config MuxOption sing.MuxOption `yaml:"mux-option" json:"mux-option,omitempty"` diff --git a/listener/http/server.go b/listener/http/server.go index bacfa844eb..8b86ac6213 100644 --- a/listener/http/server.go +++ b/listener/http/server.go @@ -12,6 +12,7 @@ import ( authStore "github.com/metacubex/mihomo/listener/auth" LC "github.com/metacubex/mihomo/listener/config" "github.com/metacubex/mihomo/listener/reality" + "github.com/metacubex/mihomo/ntp" ) type Listener struct { @@ -65,7 +66,7 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A return nil, err } - tlsConfig := &tlsC.Config{} + tlsConfig := &tlsC.Config{Time: ntp.Now} var realityBuilder *reality.Builder if config.Certificate != "" && config.PrivateKey != "" { @@ -82,10 +83,26 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A } } } + tlsConfig.ClientAuth = tlsC.ClientAuthTypeFromString(config.ClientAuthType) + if len(config.ClientAuthCert) > 0 { + if tlsConfig.ClientAuth == tlsC.NoClientCert { + tlsConfig.ClientAuth = tlsC.RequireAndVerifyClientCert + } + } + if tlsConfig.ClientAuth == tlsC.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tlsC.RequireAndVerifyClientCert { + pool, err := ca.LoadCertificates(config.ClientAuthCert, C.Path) + if err != nil { + return nil, err + } + tlsConfig.ClientCAs = pool + } if config.RealityConfig.PrivateKey != "" { if tlsConfig.Certificates != nil { return nil, errors.New("certificate is unavailable in reality") } + if tlsConfig.ClientAuth != tlsC.NoClientCert { + return nil, errors.New("client-auth is unavailable in reality") + } realityBuilder, err = config.RealityConfig.Build(tunnel) if err != nil { return nil, err diff --git a/listener/inbound/anytls.go b/listener/inbound/anytls.go index 224b053454..9980789dd3 100644 --- a/listener/inbound/anytls.go +++ b/listener/inbound/anytls.go @@ -11,11 +11,13 @@ import ( type AnyTLSOption struct { BaseOption - Users map[string]string `inbound:"users,omitempty"` - Certificate string `inbound:"certificate"` - PrivateKey string `inbound:"private-key"` - EchKey string `inbound:"ech-key,omitempty"` - PaddingScheme string `inbound:"padding-scheme,omitempty"` + Users map[string]string `inbound:"users,omitempty"` + Certificate string `inbound:"certificate"` + PrivateKey string `inbound:"private-key"` + ClientAuthType string `inbound:"client-auth-type,omitempty"` + ClientAuthCert string `inbound:"client-auth-cert,omitempty"` + EchKey string `inbound:"ech-key,omitempty"` + PaddingScheme string `inbound:"padding-scheme,omitempty"` } func (o AnyTLSOption) Equal(config C.InboundConfig) bool { @@ -38,13 +40,15 @@ func NewAnyTLS(options *AnyTLSOption) (*AnyTLS, error) { Base: base, config: options, vs: LC.AnyTLSServer{ - Enable: true, - Listen: base.RawAddress(), - Users: options.Users, - Certificate: options.Certificate, - PrivateKey: options.PrivateKey, - EchKey: options.EchKey, - PaddingScheme: options.PaddingScheme, + Enable: true, + Listen: base.RawAddress(), + Users: options.Users, + Certificate: options.Certificate, + PrivateKey: options.PrivateKey, + ClientAuthType: options.ClientAuthType, + ClientAuthCert: options.ClientAuthCert, + EchKey: options.EchKey, + PaddingScheme: options.PaddingScheme, }, }, nil } diff --git a/listener/inbound/anytls_test.go b/listener/inbound/anytls_test.go index 7759b4c7d9..444c943a46 100644 --- a/listener/inbound/anytls_test.go +++ b/listener/inbound/anytls_test.go @@ -70,4 +70,25 @@ func TestInboundAnyTLS_TLS(t *testing.T) { } testInboundAnyTLS(t, inboundOptions, outboundOptions) }) + t.Run("mTLS", func(t *testing.T) { + inboundOptions := inboundOptions + outboundOptions := outboundOptions + inboundOptions.ClientAuthCert = tlsAuthCertificate + outboundOptions.Certificate = tlsAuthCertificate + outboundOptions.PrivateKey = tlsAuthPrivateKey + testInboundAnyTLS(t, inboundOptions, outboundOptions) + }) + t.Run("mTLS+ECH", func(t *testing.T) { + inboundOptions := inboundOptions + outboundOptions := outboundOptions + inboundOptions.ClientAuthCert = tlsAuthCertificate + outboundOptions.Certificate = tlsAuthCertificate + outboundOptions.PrivateKey = tlsAuthPrivateKey + inboundOptions.EchKey = echKeyPem + outboundOptions.ECHOpts = outbound.ECHOptions{ + Enable: true, + Config: echConfigBase64, + } + testInboundAnyTLS(t, inboundOptions, outboundOptions) + }) } diff --git a/listener/inbound/common_test.go b/listener/inbound/common_test.go index 79e382fa81..72874c7b54 100644 --- a/listener/inbound/common_test.go +++ b/listener/inbound/common_test.go @@ -37,9 +37,10 @@ var httpData = make([]byte, 2*pool.RelayBufferSize) var remoteAddr = netip.MustParseAddr("1.2.3.4") var userUUID = utils.NewUUIDV4().String() var tlsCertificate, tlsPrivateKey, tlsFingerprint, _ = ca.NewRandomTLSKeyPair(ca.KeyPairTypeP256) +var tlsAuthCertificate, tlsAuthPrivateKey, _, _ = ca.NewRandomTLSKeyPair(ca.KeyPairTypeP256) var tlsConfigCert, _ = tls.X509KeyPair([]byte(tlsCertificate), []byte(tlsPrivateKey)) var tlsConfig = &tls.Config{Certificates: []tls.Certificate{tlsConfigCert}, NextProtos: []string{"h2", "http/1.1"}} -var tlsClientConfig, _ = ca.GetTLSConfig(nil, tlsFingerprint, "", "") +var tlsClientConfig, _ = ca.GetTLSConfig(ca.Option{Fingerprint: tlsFingerprint}) var realityPrivateKey, realityPublickey string var realityDest = "itunes.apple.com" var realityShortid = "10f897e26c4b9478" diff --git a/listener/inbound/http.go b/listener/inbound/http.go index 16693a21d8..fdd0e8789a 100644 --- a/listener/inbound/http.go +++ b/listener/inbound/http.go @@ -13,11 +13,13 @@ import ( type HTTPOption struct { BaseOption - Users AuthUsers `inbound:"users,omitempty"` - Certificate string `inbound:"certificate,omitempty"` - PrivateKey string `inbound:"private-key,omitempty"` - EchKey string `inbound:"ech-key,omitempty"` - RealityConfig RealityConfig `inbound:"reality-config,omitempty"` + Users AuthUsers `inbound:"users,omitempty"` + Certificate string `inbound:"certificate,omitempty"` + PrivateKey string `inbound:"private-key,omitempty"` + ClientAuthType string `inbound:"client-auth-type,omitempty"` + ClientAuthCert string `inbound:"client-auth-cert,omitempty"` + EchKey string `inbound:"ech-key,omitempty"` + RealityConfig RealityConfig `inbound:"reality-config,omitempty"` } func (o HTTPOption) Equal(config C.InboundConfig) bool { @@ -60,13 +62,15 @@ func (h *HTTP) Listen(tunnel C.Tunnel) error { for _, addr := range strings.Split(h.RawAddress(), ",") { l, err := http.NewWithConfig( LC.AuthServer{ - Enable: true, - Listen: addr, - AuthStore: h.config.Users.GetAuthStore(), - Certificate: h.config.Certificate, - PrivateKey: h.config.PrivateKey, - EchKey: h.config.EchKey, - RealityConfig: h.config.RealityConfig.Build(), + Enable: true, + Listen: addr, + AuthStore: h.config.Users.GetAuthStore(), + Certificate: h.config.Certificate, + PrivateKey: h.config.PrivateKey, + ClientAuthType: h.config.ClientAuthType, + ClientAuthCert: h.config.ClientAuthCert, + EchKey: h.config.EchKey, + RealityConfig: h.config.RealityConfig.Build(), }, tunnel, h.Additions()..., diff --git a/listener/inbound/hysteria2.go b/listener/inbound/hysteria2.go index bcdca0b784..d296f544b8 100644 --- a/listener/inbound/hysteria2.go +++ b/listener/inbound/hysteria2.go @@ -16,6 +16,8 @@ type Hysteria2Option struct { ObfsPassword string `inbound:"obfs-password,omitempty"` Certificate string `inbound:"certificate"` PrivateKey string `inbound:"private-key"` + ClientAuthType string `inbound:"client-auth-type,omitempty"` + ClientAuthCert string `inbound:"client-auth-cert,omitempty"` EchKey string `inbound:"ech-key,omitempty"` MaxIdleTime int `inbound:"max-idle-time,omitempty"` ALPN []string `inbound:"alpn,omitempty"` @@ -61,6 +63,8 @@ func NewHysteria2(options *Hysteria2Option) (*Hysteria2, error) { ObfsPassword: options.ObfsPassword, Certificate: options.Certificate, PrivateKey: options.PrivateKey, + ClientAuthType: options.ClientAuthType, + ClientAuthCert: options.ClientAuthCert, EchKey: options.EchKey, MaxIdleTime: options.MaxIdleTime, ALPN: options.ALPN, diff --git a/listener/inbound/hysteria2_test.go b/listener/inbound/hysteria2_test.go index fd2d411735..52fc07b7e1 100644 --- a/listener/inbound/hysteria2_test.go +++ b/listener/inbound/hysteria2_test.go @@ -51,14 +51,7 @@ func testInboundHysteria2(t *testing.T, inboundOptions inbound.Hysteria2Option, tunnel.DoTest(t, out) } -func TestInboundHysteria2_TLS(t *testing.T) { - inboundOptions := inbound.Hysteria2Option{ - Certificate: tlsCertificate, - PrivateKey: tlsPrivateKey, - } - outboundOptions := outbound.Hysteria2Option{ - Fingerprint: tlsFingerprint, - } +func testInboundHysteria2TLS(t *testing.T, inboundOptions inbound.Hysteria2Option, outboundOptions outbound.Hysteria2Option) { testInboundHysteria2(t, inboundOptions, outboundOptions) t.Run("ECH", func(t *testing.T) { inboundOptions := inboundOptions @@ -70,6 +63,38 @@ func TestInboundHysteria2_TLS(t *testing.T) { } testInboundHysteria2(t, inboundOptions, outboundOptions) }) + t.Run("mTLS", func(t *testing.T) { + inboundOptions := inboundOptions + outboundOptions := outboundOptions + inboundOptions.ClientAuthCert = tlsAuthCertificate + outboundOptions.Certificate = tlsAuthCertificate + outboundOptions.PrivateKey = tlsAuthPrivateKey + testInboundHysteria2(t, inboundOptions, outboundOptions) + }) + t.Run("mTLS+ECH", func(t *testing.T) { + inboundOptions := inboundOptions + outboundOptions := outboundOptions + inboundOptions.ClientAuthCert = tlsAuthCertificate + outboundOptions.Certificate = tlsAuthCertificate + outboundOptions.PrivateKey = tlsAuthPrivateKey + inboundOptions.EchKey = echKeyPem + outboundOptions.ECHOpts = outbound.ECHOptions{ + Enable: true, + Config: echConfigBase64, + } + testInboundHysteria2(t, inboundOptions, outboundOptions) + }) +} + +func TestInboundHysteria2_TLS(t *testing.T) { + inboundOptions := inbound.Hysteria2Option{ + Certificate: tlsCertificate, + PrivateKey: tlsPrivateKey, + } + outboundOptions := outbound.Hysteria2Option{ + Fingerprint: tlsFingerprint, + } + testInboundHysteria2TLS(t, inboundOptions, outboundOptions) } func TestInboundHysteria2_Salamander(t *testing.T) { @@ -84,17 +109,7 @@ func TestInboundHysteria2_Salamander(t *testing.T) { Obfs: "salamander", ObfsPassword: userUUID, } - testInboundHysteria2(t, inboundOptions, outboundOptions) - t.Run("ECH", func(t *testing.T) { - inboundOptions := inboundOptions - outboundOptions := outboundOptions - inboundOptions.EchKey = echKeyPem - outboundOptions.ECHOpts = outbound.ECHOptions{ - Enable: true, - Config: echConfigBase64, - } - testInboundHysteria2(t, inboundOptions, outboundOptions) - }) + testInboundHysteria2TLS(t, inboundOptions, outboundOptions) } func TestInboundHysteria2_Brutal(t *testing.T) { @@ -109,15 +124,5 @@ func TestInboundHysteria2_Brutal(t *testing.T) { Up: "30 Mbps", Down: "200 Mbps", } - testInboundHysteria2(t, inboundOptions, outboundOptions) - t.Run("ECH", func(t *testing.T) { - inboundOptions := inboundOptions - outboundOptions := outboundOptions - inboundOptions.EchKey = echKeyPem - outboundOptions.ECHOpts = outbound.ECHOptions{ - Enable: true, - Config: echConfigBase64, - } - testInboundHysteria2(t, inboundOptions, outboundOptions) - }) + testInboundHysteria2TLS(t, inboundOptions, outboundOptions) } diff --git a/listener/inbound/kcptun.go b/listener/inbound/kcptun.go new file mode 100644 index 0000000000..e4098f9657 --- /dev/null +++ b/listener/inbound/kcptun.go @@ -0,0 +1,64 @@ +package inbound + +import ( + LC "github.com/metacubex/mihomo/listener/config" + "github.com/metacubex/mihomo/transport/kcptun" +) + +type KcpTun struct { + Enable bool `inbound:"enable"` + Key string `inbound:"key,omitempty"` + Crypt string `inbound:"crypt,omitempty"` + Mode string `inbound:"mode,omitempty"` + Conn int `inbound:"conn,omitempty"` + AutoExpire int `inbound:"autoexpire,omitempty"` + ScavengeTTL int `inbound:"scavengettl,omitempty"` + MTU int `inbound:"mtu,omitempty"` + SndWnd int `inbound:"sndwnd,omitempty"` + RcvWnd int `inbound:"rcvwnd,omitempty"` + DataShard int `inbound:"datashard,omitempty"` + ParityShard int `inbound:"parityshard,omitempty"` + DSCP int `inbound:"dscp,omitempty"` + NoComp bool `inbound:"nocomp,omitempty"` + AckNodelay bool `inbound:"acknodelay,omitempty"` + NoDelay int `inbound:"nodelay,omitempty"` + Interval int `inbound:"interval,omitempty"` + Resend int `inbound:"resend,omitempty"` + NoCongestion int `inbound:"nc,omitempty"` + SockBuf int `inbound:"sockbuf,omitempty"` + SmuxVer int `inbound:"smuxver,omitempty"` + SmuxBuf int `inbound:"smuxbuf,omitempty"` + StreamBuf int `inbound:"streambuf,omitempty"` + KeepAlive int `inbound:"keepalive,omitempty"` +} + +func (c KcpTun) Build() LC.KcpTun { + return LC.KcpTun{ + Enable: c.Enable, + Config: kcptun.Config{ + Key: c.Key, + Crypt: c.Crypt, + Mode: c.Mode, + Conn: c.Conn, + AutoExpire: c.AutoExpire, + ScavengeTTL: c.ScavengeTTL, + MTU: c.MTU, + SndWnd: c.SndWnd, + RcvWnd: c.RcvWnd, + DataShard: c.DataShard, + ParityShard: c.ParityShard, + DSCP: c.DSCP, + NoComp: c.NoComp, + AckNodelay: c.AckNodelay, + NoDelay: c.NoDelay, + Interval: c.Interval, + Resend: c.Resend, + NoCongestion: c.NoCongestion, + SockBuf: c.SockBuf, + SmuxVer: c.SmuxVer, + SmuxBuf: c.SmuxBuf, + StreamBuf: c.StreamBuf, + KeepAlive: c.KeepAlive, + }, + } +} diff --git a/listener/inbound/mixed.go b/listener/inbound/mixed.go index db32512be4..ab4331bb7f 100644 --- a/listener/inbound/mixed.go +++ b/listener/inbound/mixed.go @@ -14,12 +14,14 @@ import ( type MixedOption struct { BaseOption - Users AuthUsers `inbound:"users,omitempty"` - UDP bool `inbound:"udp,omitempty"` - Certificate string `inbound:"certificate,omitempty"` - PrivateKey string `inbound:"private-key,omitempty"` - EchKey string `inbound:"ech-key,omitempty"` - RealityConfig RealityConfig `inbound:"reality-config,omitempty"` + Users AuthUsers `inbound:"users,omitempty"` + UDP bool `inbound:"udp,omitempty"` + Certificate string `inbound:"certificate,omitempty"` + PrivateKey string `inbound:"private-key,omitempty"` + ClientAuthType string `inbound:"client-auth-type,omitempty"` + ClientAuthCert string `inbound:"client-auth-cert,omitempty"` + EchKey string `inbound:"ech-key,omitempty"` + RealityConfig RealityConfig `inbound:"reality-config,omitempty"` } func (o MixedOption) Equal(config C.InboundConfig) bool { @@ -65,13 +67,15 @@ func (m *Mixed) Listen(tunnel C.Tunnel) error { for _, addr := range strings.Split(m.RawAddress(), ",") { l, err := mixed.NewWithConfig( LC.AuthServer{ - Enable: true, - Listen: addr, - AuthStore: m.config.Users.GetAuthStore(), - Certificate: m.config.Certificate, - PrivateKey: m.config.PrivateKey, - EchKey: m.config.EchKey, - RealityConfig: m.config.RealityConfig.Build(), + Enable: true, + Listen: addr, + AuthStore: m.config.Users.GetAuthStore(), + Certificate: m.config.Certificate, + PrivateKey: m.config.PrivateKey, + ClientAuthType: m.config.ClientAuthType, + ClientAuthCert: m.config.ClientAuthCert, + EchKey: m.config.EchKey, + RealityConfig: m.config.RealityConfig.Build(), }, tunnel, m.Additions()..., diff --git a/listener/inbound/mux_test.go b/listener/inbound/mux_test.go index a7c0208dd8..0f632220e6 100644 --- a/listener/inbound/mux_test.go +++ b/listener/inbound/mux_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/assert" ) -var singMuxProtocolList = []string{"smux"} // don't test "h2mux" and "yamux" because it has some confused bugs +var singMuxProtocolList = []string{"smux", "yamux"} // don't test "h2mux" because it has some confused bugs // notCloseProxyAdapter is a proxy adapter that does not close the underlying outbound.ProxyAdapter. // The outbound.SingMux will close the underlying outbound.ProxyAdapter when it is closed, but we don't want to close it. diff --git a/listener/inbound/shadowsocks.go b/listener/inbound/shadowsocks.go index 994f4c597c..b88013a80f 100644 --- a/listener/inbound/shadowsocks.go +++ b/listener/inbound/shadowsocks.go @@ -16,6 +16,7 @@ type ShadowSocksOption struct { UDP bool `inbound:"udp,omitempty"` MuxOption MuxOption `inbound:"mux-option,omitempty"` ShadowTLS ShadowTLS `inbound:"shadow-tls,omitempty"` + KcpTun KcpTun `inbound:"kcp-tun,omitempty"` } func (o ShadowSocksOption) Equal(config C.InboundConfig) bool { @@ -45,6 +46,7 @@ func NewShadowSocks(options *ShadowSocksOption) (*ShadowSocks, error) { Udp: options.UDP, MuxOption: options.MuxOption.Build(), ShadowTLS: options.ShadowTLS.Build(), + KcpTun: options.KcpTun.Build(), }, }, nil } diff --git a/listener/inbound/shadowsocks_test.go b/listener/inbound/shadowsocks_test.go index 7a26eecaf4..9950984de1 100644 --- a/listener/inbound/shadowsocks_test.go +++ b/listener/inbound/shadowsocks_test.go @@ -10,6 +10,7 @@ import ( "github.com/metacubex/mihomo/adapter/outbound" "github.com/metacubex/mihomo/listener/inbound" + "github.com/metacubex/mihomo/transport/kcptun" shadowtls "github.com/metacubex/mihomo/transport/sing-shadowtls" shadowsocks "github.com/metacubex/sing-shadowsocks" @@ -21,7 +22,7 @@ import ( var noneList = []string{shadowsocks.MethodNone} var shadowsocksCipherLists = [][]string{noneList, shadowaead.List, shadowaead_2022.List, shadowstream.List} -var shadowsocksCipherShortLists = [][]string{noneList, shadowaead.List[:5]} // for test shadowTLS +var shadowsocksCipherShortLists = [][]string{noneList, shadowaead.List[:5]} // for test shadowTLS and kcptun var shadowsocksPassword32 string var shadowsocksPassword16 string @@ -32,11 +33,11 @@ func init() { shadowsocksPassword16 = base64.StdEncoding.EncodeToString(passwordBytes[:16]) } -func testInboundShadowSocks(t *testing.T, inboundOptions inbound.ShadowSocksOption, outboundOptions outbound.ShadowSocksOption, cipherLists [][]string) { +func testInboundShadowSocks(t *testing.T, inboundOptions inbound.ShadowSocksOption, outboundOptions outbound.ShadowSocksOption, cipherLists [][]string, enableSingMux bool) { t.Parallel() for _, cipherList := range cipherLists { for i, cipher := range cipherList { - enableSingMux := i == 0 + enableSingMux := enableSingMux && i == 0 cipher := cipher t.Run(cipher, func(t *testing.T) { inboundOptions, outboundOptions := inboundOptions, outboundOptions // don't modify outside options value @@ -100,19 +101,19 @@ func testInboundShadowSocks0(t *testing.T, inboundOptions inbound.ShadowSocksOpt func TestInboundShadowSocks_Basic(t *testing.T) { inboundOptions := inbound.ShadowSocksOption{} outboundOptions := outbound.ShadowSocksOption{} - testInboundShadowSocks(t, inboundOptions, outboundOptions, shadowsocksCipherLists) + testInboundShadowSocks(t, inboundOptions, outboundOptions, shadowsocksCipherLists, true) } func testInboundShadowSocksShadowTls(t *testing.T, inboundOptions inbound.ShadowSocksOption, outboundOptions outbound.ShadowSocksOption) { t.Parallel() t.Run("Conn", func(t *testing.T) { inboundOptions, outboundOptions := inboundOptions, outboundOptions // don't modify outside options value - testInboundShadowSocks(t, inboundOptions, outboundOptions, shadowsocksCipherShortLists) + testInboundShadowSocks(t, inboundOptions, outboundOptions, shadowsocksCipherShortLists, true) }) t.Run("UConn", func(t *testing.T) { inboundOptions, outboundOptions := inboundOptions, outboundOptions // don't modify outside options value outboundOptions.ClientFingerprint = "chrome" - testInboundShadowSocks(t, inboundOptions, outboundOptions, shadowsocksCipherShortLists) + testInboundShadowSocks(t, inboundOptions, outboundOptions, shadowsocksCipherShortLists, true) }) } @@ -163,3 +164,17 @@ func TestInboundShadowSocks_ShadowTlsv3(t *testing.T) { } testInboundShadowSocksShadowTls(t, inboundOptions, outboundOptions) } + +func TestInboundShadowSocks_KcpTun(t *testing.T) { + inboundOptions := inbound.ShadowSocksOption{ + KcpTun: inbound.KcpTun{ + Enable: true, + Key: shadowsocksPassword16, + }, + } + outboundOptions := outbound.ShadowSocksOption{ + Plugin: kcptun.Mode, + PluginOpts: map[string]any{"key": shadowsocksPassword16}, + } + testInboundShadowSocks(t, inboundOptions, outboundOptions, shadowsocksCipherShortLists, false) +} diff --git a/listener/inbound/socks.go b/listener/inbound/socks.go index fa794ae42e..fa2b75faf8 100644 --- a/listener/inbound/socks.go +++ b/listener/inbound/socks.go @@ -13,12 +13,14 @@ import ( type SocksOption struct { BaseOption - Users AuthUsers `inbound:"users,omitempty"` - UDP bool `inbound:"udp,omitempty"` - Certificate string `inbound:"certificate,omitempty"` - PrivateKey string `inbound:"private-key,omitempty"` - EchKey string `inbound:"ech-key,omitempty"` - RealityConfig RealityConfig `inbound:"reality-config,omitempty"` + Users AuthUsers `inbound:"users,omitempty"` + UDP bool `inbound:"udp,omitempty"` + Certificate string `inbound:"certificate,omitempty"` + PrivateKey string `inbound:"private-key,omitempty"` + ClientAuthType string `inbound:"client-auth-type,omitempty"` + ClientAuthCert string `inbound:"client-auth-cert,omitempty"` + EchKey string `inbound:"ech-key,omitempty"` + RealityConfig RealityConfig `inbound:"reality-config,omitempty"` } func (o SocksOption) Equal(config C.InboundConfig) bool { @@ -85,13 +87,15 @@ func (s *Socks) Listen(tunnel C.Tunnel) error { for _, addr := range strings.Split(s.RawAddress(), ",") { stl, err := socks.NewWithConfig( LC.AuthServer{ - Enable: true, - Listen: addr, - AuthStore: s.config.Users.GetAuthStore(), - Certificate: s.config.Certificate, - PrivateKey: s.config.PrivateKey, - EchKey: s.config.EchKey, - RealityConfig: s.config.RealityConfig.Build(), + Enable: true, + Listen: addr, + AuthStore: s.config.Users.GetAuthStore(), + Certificate: s.config.Certificate, + PrivateKey: s.config.PrivateKey, + ClientAuthType: s.config.ClientAuthType, + ClientAuthCert: s.config.ClientAuthCert, + EchKey: s.config.EchKey, + RealityConfig: s.config.RealityConfig.Build(), }, tunnel, s.Additions()..., diff --git a/listener/inbound/trojan.go b/listener/inbound/trojan.go index 04d73bf82c..a40bcdba00 100644 --- a/listener/inbound/trojan.go +++ b/listener/inbound/trojan.go @@ -16,6 +16,8 @@ type TrojanOption struct { GrpcServiceName string `inbound:"grpc-service-name,omitempty"` Certificate string `inbound:"certificate,omitempty"` PrivateKey string `inbound:"private-key,omitempty"` + ClientAuthType string `inbound:"client-auth-type,omitempty"` + ClientAuthCert string `inbound:"client-auth-cert,omitempty"` EchKey string `inbound:"ech-key,omitempty"` RealityConfig RealityConfig `inbound:"reality-config,omitempty"` MuxOption MuxOption `inbound:"mux-option,omitempty"` @@ -68,6 +70,8 @@ func NewTrojan(options *TrojanOption) (*Trojan, error) { GrpcServiceName: options.GrpcServiceName, Certificate: options.Certificate, PrivateKey: options.PrivateKey, + ClientAuthType: options.ClientAuthType, + ClientAuthCert: options.ClientAuthCert, EchKey: options.EchKey, RealityConfig: options.RealityConfig.Build(), MuxOption: options.MuxOption.Build(), diff --git a/listener/inbound/trojan_test.go b/listener/inbound/trojan_test.go index 5fd584912e..7b92b08560 100644 --- a/listener/inbound/trojan_test.go +++ b/listener/inbound/trojan_test.go @@ -52,17 +52,13 @@ func testInboundTrojan(t *testing.T, inboundOptions inbound.TrojanOption, outbou tunnel.DoTest(t, out) + if outboundOptions.Network == "grpc" { // don't test sing-mux over grpc + return + } testSingMux(t, tunnel, out) } -func TestInboundTrojan_TLS(t *testing.T) { - inboundOptions := inbound.TrojanOption{ - Certificate: tlsCertificate, - PrivateKey: tlsPrivateKey, - } - outboundOptions := outbound.TrojanOption{ - Fingerprint: tlsFingerprint, - } +func testInboundTrojanTLS(t *testing.T, inboundOptions inbound.TrojanOption, outboundOptions outbound.TrojanOption) { testInboundTrojan(t, inboundOptions, outboundOptions) t.Run("ECH", func(t *testing.T) { inboundOptions := inboundOptions @@ -74,6 +70,38 @@ func TestInboundTrojan_TLS(t *testing.T) { } testInboundTrojan(t, inboundOptions, outboundOptions) }) + t.Run("mTLS", func(t *testing.T) { + inboundOptions := inboundOptions + outboundOptions := outboundOptions + inboundOptions.ClientAuthCert = tlsAuthCertificate + outboundOptions.Certificate = tlsAuthCertificate + outboundOptions.PrivateKey = tlsAuthPrivateKey + testInboundTrojan(t, inboundOptions, outboundOptions) + }) + t.Run("mTLS+ECH", func(t *testing.T) { + inboundOptions := inboundOptions + outboundOptions := outboundOptions + inboundOptions.ClientAuthCert = tlsAuthCertificate + outboundOptions.Certificate = tlsAuthCertificate + outboundOptions.PrivateKey = tlsAuthPrivateKey + inboundOptions.EchKey = echKeyPem + outboundOptions.ECHOpts = outbound.ECHOptions{ + Enable: true, + Config: echConfigBase64, + } + testInboundTrojan(t, inboundOptions, outboundOptions) + }) +} + +func TestInboundTrojan_TLS(t *testing.T) { + inboundOptions := inbound.TrojanOption{ + Certificate: tlsCertificate, + PrivateKey: tlsPrivateKey, + } + outboundOptions := outbound.TrojanOption{ + Fingerprint: tlsFingerprint, + } + testInboundTrojanTLS(t, inboundOptions, outboundOptions) } func TestInboundTrojan_Wss1(t *testing.T) { @@ -89,17 +117,7 @@ func TestInboundTrojan_Wss1(t *testing.T) { Path: "/ws", }, } - testInboundTrojan(t, inboundOptions, outboundOptions) - t.Run("ECH", func(t *testing.T) { - inboundOptions := inboundOptions - outboundOptions := outboundOptions - inboundOptions.EchKey = echKeyPem - outboundOptions.ECHOpts = outbound.ECHOptions{ - Enable: true, - Config: echConfigBase64, - } - testInboundTrojan(t, inboundOptions, outboundOptions) - }) + testInboundTrojanTLS(t, inboundOptions, outboundOptions) } func TestInboundTrojan_Wss2(t *testing.T) { @@ -116,17 +134,7 @@ func TestInboundTrojan_Wss2(t *testing.T) { Path: "/ws", }, } - testInboundTrojan(t, inboundOptions, outboundOptions) - t.Run("ECH", func(t *testing.T) { - inboundOptions := inboundOptions - outboundOptions := outboundOptions - inboundOptions.EchKey = echKeyPem - outboundOptions.ECHOpts = outbound.ECHOptions{ - Enable: true, - Config: echConfigBase64, - } - testInboundTrojan(t, inboundOptions, outboundOptions) - }) + testInboundTrojanTLS(t, inboundOptions, outboundOptions) } func TestInboundTrojan_Grpc1(t *testing.T) { @@ -140,17 +148,7 @@ func TestInboundTrojan_Grpc1(t *testing.T) { Network: "grpc", GrpcOpts: outbound.GrpcOptions{GrpcServiceName: "GunService"}, } - testInboundTrojan(t, inboundOptions, outboundOptions) - t.Run("ECH", func(t *testing.T) { - inboundOptions := inboundOptions - outboundOptions := outboundOptions - inboundOptions.EchKey = echKeyPem - outboundOptions.ECHOpts = outbound.ECHOptions{ - Enable: true, - Config: echConfigBase64, - } - testInboundTrojan(t, inboundOptions, outboundOptions) - }) + testInboundTrojanTLS(t, inboundOptions, outboundOptions) } func TestInboundTrojan_Grpc2(t *testing.T) { @@ -165,17 +163,7 @@ func TestInboundTrojan_Grpc2(t *testing.T) { Network: "grpc", GrpcOpts: outbound.GrpcOptions{GrpcServiceName: "GunService"}, } - testInboundTrojan(t, inboundOptions, outboundOptions) - t.Run("ECH", func(t *testing.T) { - inboundOptions := inboundOptions - outboundOptions := outboundOptions - inboundOptions.EchKey = echKeyPem - outboundOptions.ECHOpts = outbound.ECHOptions{ - Enable: true, - Config: echConfigBase64, - } - testInboundTrojan(t, inboundOptions, outboundOptions) - }) + testInboundTrojanTLS(t, inboundOptions, outboundOptions) } func TestInboundTrojan_Reality(t *testing.T) { @@ -239,17 +227,7 @@ func TestInboundTrojan_TLS_TrojanSS(t *testing.T) { Password: "password", }, } - testInboundTrojan(t, inboundOptions, outboundOptions) - t.Run("ECH", func(t *testing.T) { - inboundOptions := inboundOptions - outboundOptions := outboundOptions - inboundOptions.EchKey = echKeyPem - outboundOptions.ECHOpts = outbound.ECHOptions{ - Enable: true, - Config: echConfigBase64, - } - testInboundTrojan(t, inboundOptions, outboundOptions) - }) + testInboundTrojanTLS(t, inboundOptions, outboundOptions) } func TestInboundTrojan_Wss_TrojanSS(t *testing.T) { @@ -275,15 +253,5 @@ func TestInboundTrojan_Wss_TrojanSS(t *testing.T) { Path: "/ws", }, } - testInboundTrojan(t, inboundOptions, outboundOptions) - t.Run("ECH", func(t *testing.T) { - inboundOptions := inboundOptions - outboundOptions := outboundOptions - inboundOptions.EchKey = echKeyPem - outboundOptions.ECHOpts = outbound.ECHOptions{ - Enable: true, - Config: echConfigBase64, - } - testInboundTrojan(t, inboundOptions, outboundOptions) - }) + testInboundTrojanTLS(t, inboundOptions, outboundOptions) } diff --git a/listener/inbound/tuic.go b/listener/inbound/tuic.go index 67349156ce..a70175d1b8 100644 --- a/listener/inbound/tuic.go +++ b/listener/inbound/tuic.go @@ -15,6 +15,8 @@ type TuicOption struct { Users map[string]string `inbound:"users,omitempty"` Certificate string `inbound:"certificate"` PrivateKey string `inbound:"private-key"` + ClientAuthType string `inbound:"client-auth-type,omitempty"` + ClientAuthCert string `inbound:"client-auth-cert,omitempty"` EchKey string `inbound:"ech-key,omitempty"` CongestionController string `inbound:"congestion-controller,omitempty"` MaxIdleTime int `inbound:"max-idle-time,omitempty"` @@ -51,6 +53,8 @@ func NewTuic(options *TuicOption) (*Tuic, error) { Users: options.Users, Certificate: options.Certificate, PrivateKey: options.PrivateKey, + ClientAuthType: options.ClientAuthType, + ClientAuthCert: options.ClientAuthCert, EchKey: options.EchKey, CongestionController: options.CongestionController, MaxIdleTime: options.MaxIdleTime, diff --git a/listener/inbound/tuic_test.go b/listener/inbound/tuic_test.go index 1cf3991de1..f887609de7 100644 --- a/listener/inbound/tuic_test.go +++ b/listener/inbound/tuic_test.go @@ -99,4 +99,25 @@ func TestInboundTuic_TLS(t *testing.T) { } testInboundTuic(t, inboundOptions, outboundOptions) }) + t.Run("mTLS", func(t *testing.T) { + inboundOptions := inboundOptions + outboundOptions := outboundOptions + inboundOptions.ClientAuthCert = tlsAuthCertificate + outboundOptions.Certificate = tlsAuthCertificate + outboundOptions.PrivateKey = tlsAuthPrivateKey + testInboundTuic(t, inboundOptions, outboundOptions) + }) + t.Run("mTLS+ECH", func(t *testing.T) { + inboundOptions := inboundOptions + outboundOptions := outboundOptions + inboundOptions.ClientAuthCert = tlsAuthCertificate + outboundOptions.Certificate = tlsAuthCertificate + outboundOptions.PrivateKey = tlsAuthPrivateKey + inboundOptions.EchKey = echKeyPem + outboundOptions.ECHOpts = outbound.ECHOptions{ + Enable: true, + Config: echConfigBase64, + } + testInboundTuic(t, inboundOptions, outboundOptions) + }) } diff --git a/listener/inbound/tun.go b/listener/inbound/tun.go index e6ebb2a109..79004023a7 100644 --- a/listener/inbound/tun.go +++ b/listener/inbound/tun.go @@ -49,6 +49,7 @@ type TunOption struct { ExcludePackage []string `inbound:"exclude-package,omitempty"` EndpointIndependentNat bool `inbound:"endpoint-independent-nat,omitempty"` UDPTimeout int64 `inbound:"udp-timeout,omitempty"` + DisableICMPForwarding bool `inbound:"disable-icmp-forwarding,omitempty"` FileDescriptor int `inbound:"file-descriptor,omitempty"` Inet4RouteAddress []netip.Prefix `inbound:"inet4-route-address,omitempty"` @@ -122,6 +123,7 @@ func NewTun(options *TunOption) (*Tun, error) { ExcludePackage: options.ExcludePackage, EndpointIndependentNat: options.EndpointIndependentNat, UDPTimeout: options.UDPTimeout, + DisableICMPForwarding: options.DisableICMPForwarding, FileDescriptor: options.FileDescriptor, Inet4RouteAddress: options.Inet4RouteAddress, diff --git a/listener/inbound/vless.go b/listener/inbound/vless.go index 305df50ac8..d8109f8443 100644 --- a/listener/inbound/vless.go +++ b/listener/inbound/vless.go @@ -17,6 +17,8 @@ type VlessOption struct { GrpcServiceName string `inbound:"grpc-service-name,omitempty"` Certificate string `inbound:"certificate,omitempty"` PrivateKey string `inbound:"private-key,omitempty"` + ClientAuthType string `inbound:"client-auth-type,omitempty"` + ClientAuthCert string `inbound:"client-auth-cert,omitempty"` EchKey string `inbound:"ech-key,omitempty"` RealityConfig RealityConfig `inbound:"reality-config,omitempty"` MuxOption MuxOption `inbound:"mux-option,omitempty"` @@ -64,6 +66,8 @@ func NewVless(options *VlessOption) (*Vless, error) { GrpcServiceName: options.GrpcServiceName, Certificate: options.Certificate, PrivateKey: options.PrivateKey, + ClientAuthType: options.ClientAuthType, + ClientAuthCert: options.ClientAuthCert, EchKey: options.EchKey, RealityConfig: options.RealityConfig.Build(), MuxOption: options.MuxOption.Build(), diff --git a/listener/inbound/vless_test.go b/listener/inbound/vless_test.go index 9a7fed59eb..e12e95567d 100644 --- a/listener/inbound/vless_test.go +++ b/listener/inbound/vless_test.go @@ -53,41 +53,87 @@ func testInboundVless(t *testing.T, inboundOptions inbound.VlessOption, outbound tunnel.DoTest(t, out) + if outboundOptions.Network == "grpc" { // don't test sing-mux over grpc + return + } testSingMux(t, tunnel, out) } -func TestInboundVless_TLS(t *testing.T) { - inboundOptions := inbound.VlessOption{ - Certificate: tlsCertificate, - PrivateKey: tlsPrivateKey, - } - outboundOptions := outbound.VlessOption{ - TLS: true, - Fingerprint: tlsFingerprint, - } +func testInboundVlessTLS(t *testing.T, inboundOptions inbound.VlessOption, outboundOptions outbound.VlessOption, testVision bool) { testInboundVless(t, inboundOptions, outboundOptions) - t.Run("xtls-rprx-vision", func(t *testing.T) { + if testVision { + t.Run("xtls-rprx-vision", func(t *testing.T) { + outboundOptions := outboundOptions + outboundOptions.Flow = "xtls-rprx-vision" + testInboundVless(t, inboundOptions, outboundOptions) + }) + } + t.Run("ECH", func(t *testing.T) { + inboundOptions := inboundOptions outboundOptions := outboundOptions - outboundOptions.Flow = "xtls-rprx-vision" + inboundOptions.EchKey = echKeyPem + outboundOptions.ECHOpts = outbound.ECHOptions{ + Enable: true, + Config: echConfigBase64, + } testInboundVless(t, inboundOptions, outboundOptions) + if testVision { + t.Run("xtls-rprx-vision", func(t *testing.T) { + outboundOptions := outboundOptions + outboundOptions.Flow = "xtls-rprx-vision" + testInboundVless(t, inboundOptions, outboundOptions) + }) + } }) - t.Run("ECH", func(t *testing.T) { + t.Run("mTLS", func(t *testing.T) { + inboundOptions := inboundOptions + outboundOptions := outboundOptions + inboundOptions.ClientAuthCert = tlsAuthCertificate + outboundOptions.Certificate = tlsAuthCertificate + outboundOptions.PrivateKey = tlsAuthPrivateKey + testInboundVless(t, inboundOptions, outboundOptions) + if testVision { + t.Run("xtls-rprx-vision", func(t *testing.T) { + outboundOptions := outboundOptions + outboundOptions.Flow = "xtls-rprx-vision" + testInboundVless(t, inboundOptions, outboundOptions) + }) + } + }) + t.Run("mTLS+ECH", func(t *testing.T) { inboundOptions := inboundOptions outboundOptions := outboundOptions + inboundOptions.ClientAuthCert = tlsAuthCertificate + outboundOptions.Certificate = tlsAuthCertificate + outboundOptions.PrivateKey = tlsAuthPrivateKey inboundOptions.EchKey = echKeyPem outboundOptions.ECHOpts = outbound.ECHOptions{ Enable: true, Config: echConfigBase64, } testInboundVless(t, inboundOptions, outboundOptions) - t.Run("xtls-rprx-vision", func(t *testing.T) { - outboundOptions := outboundOptions - outboundOptions.Flow = "xtls-rprx-vision" - testInboundVless(t, inboundOptions, outboundOptions) - }) + if testVision { + t.Run("xtls-rprx-vision", func(t *testing.T) { + outboundOptions := outboundOptions + outboundOptions.Flow = "xtls-rprx-vision" + testInboundVless(t, inboundOptions, outboundOptions) + }) + } }) } +func TestInboundVless_TLS(t *testing.T) { + inboundOptions := inbound.VlessOption{ + Certificate: tlsCertificate, + PrivateKey: tlsPrivateKey, + } + outboundOptions := outbound.VlessOption{ + TLS: true, + Fingerprint: tlsFingerprint, + } + testInboundVlessTLS(t, inboundOptions, outboundOptions, true) +} + func TestInboundVless_Encryption(t *testing.T) { seedBase64, clientBase64, _, err := encryption.GenMLKEM768("") if err != nil { @@ -99,6 +145,15 @@ func TestInboundVless_Encryption(t *testing.T) { t.Fatal(err) return } + paddings := []struct { + name string + data string + }{ + {"unconfigured-padding", ""}, + {"default-padding", "100-111-1111.75-0-111.50-0-3333."}, + {"old-padding", "100-100-1000."}, // Xray-core v25.8.29 + {"custom-padding", "100-1234-7890.33-0-1111.66-0-6666.55-111-777."}, + } var modes = []string{ "native", "xorpub", @@ -107,19 +162,55 @@ func TestInboundVless_Encryption(t *testing.T) { for i := range modes { mode := modes[i] t.Run(mode, func(t *testing.T) { - inboundOptions := inbound.VlessOption{ - Decryption: "mlkem768x25519plus." + mode + ".600s." + privateKeyBase64 + "." + seedBase64, + t.Parallel() + for i := range paddings { + padding := paddings[i].data + t.Run(paddings[i].name, func(t *testing.T) { + t.Parallel() + inboundOptions := inbound.VlessOption{ + Decryption: "mlkem768x25519plus." + mode + ".600s." + padding + privateKeyBase64 + "." + seedBase64, + } + outboundOptions := outbound.VlessOption{ + Encryption: "mlkem768x25519plus." + mode + ".0rtt." + padding + passwordBase64 + "." + clientBase64, + } + t.Run("raw", func(t *testing.T) { + testInboundVless(t, inboundOptions, outboundOptions) + t.Run("xtls-rprx-vision", func(t *testing.T) { + outboundOptions := outboundOptions + outboundOptions.Flow = "xtls-rprx-vision" + testInboundVless(t, inboundOptions, outboundOptions) + }) + }) + t.Run("ws", func(t *testing.T) { + inboundOptions := inboundOptions + inboundOptions.WsPath = "/ws" + outboundOptions := outboundOptions + outboundOptions.Network = "ws" + outboundOptions.WSOpts = outbound.WSOptions{Path: "/ws"} + testInboundVless(t, inboundOptions, outboundOptions) + t.Run("xtls-rprx-vision", func(t *testing.T) { + outboundOptions := outboundOptions + outboundOptions.Flow = "xtls-rprx-vision" + testInboundVless(t, inboundOptions, outboundOptions) + }) + }) + t.Run("grpc", func(t *testing.T) { + inboundOptions := inboundOptions + inboundOptions.GrpcServiceName = "GunService" + outboundOptions := outboundOptions + outboundOptions.Network = "grpc" + outboundOptions.GrpcOpts = outbound.GrpcOptions{GrpcServiceName: "GunService"} + testInboundVless(t, inboundOptions, outboundOptions) + t.Run("xtls-rprx-vision", func(t *testing.T) { + outboundOptions := outboundOptions + outboundOptions.Flow = "xtls-rprx-vision" + testInboundVless(t, inboundOptions, outboundOptions) + }) + }) + }) } - outboundOptions := outbound.VlessOption{ - Encryption: "mlkem768x25519plus." + mode + ".0rtt." + passwordBase64 + "." + clientBase64, - } - testInboundVless(t, inboundOptions, outboundOptions) - t.Run("xtls-rprx-vision", func(t *testing.T) { - outboundOptions := outboundOptions - outboundOptions.Flow = "xtls-rprx-vision" - testInboundVless(t, inboundOptions, outboundOptions) - }) }) + } } @@ -133,31 +224,9 @@ func TestInboundVless_Wss1(t *testing.T) { TLS: true, Fingerprint: tlsFingerprint, Network: "ws", - WSOpts: outbound.WSOptions{ - Path: "/ws", - }, + WSOpts: outbound.WSOptions{Path: "/ws"}, } - testInboundVless(t, inboundOptions, outboundOptions) - t.Run("xtls-rprx-vision", func(t *testing.T) { - outboundOptions := outboundOptions - outboundOptions.Flow = "xtls-rprx-vision" - testInboundVless(t, inboundOptions, outboundOptions) - }) - t.Run("ECH", func(t *testing.T) { - inboundOptions := inboundOptions - outboundOptions := outboundOptions - inboundOptions.EchKey = echKeyPem - outboundOptions.ECHOpts = outbound.ECHOptions{ - Enable: true, - Config: echConfigBase64, - } - testInboundVless(t, inboundOptions, outboundOptions) - t.Run("xtls-rprx-vision", func(t *testing.T) { - outboundOptions := outboundOptions - outboundOptions.Flow = "xtls-rprx-vision" - testInboundVless(t, inboundOptions, outboundOptions) - }) - }) + testInboundVlessTLS(t, inboundOptions, outboundOptions, false) } func TestInboundVless_Wss2(t *testing.T) { @@ -171,31 +240,9 @@ func TestInboundVless_Wss2(t *testing.T) { TLS: true, Fingerprint: tlsFingerprint, Network: "ws", - WSOpts: outbound.WSOptions{ - Path: "/ws", - }, + WSOpts: outbound.WSOptions{Path: "/ws"}, } - testInboundVless(t, inboundOptions, outboundOptions) - t.Run("xtls-rprx-vision", func(t *testing.T) { - outboundOptions := outboundOptions - outboundOptions.Flow = "xtls-rprx-vision" - testInboundVless(t, inboundOptions, outboundOptions) - }) - t.Run("ECH", func(t *testing.T) { - inboundOptions := inboundOptions - outboundOptions := outboundOptions - inboundOptions.EchKey = echKeyPem - outboundOptions.ECHOpts = outbound.ECHOptions{ - Enable: true, - Config: echConfigBase64, - } - testInboundVless(t, inboundOptions, outboundOptions) - t.Run("xtls-rprx-vision", func(t *testing.T) { - outboundOptions := outboundOptions - outboundOptions.Flow = "xtls-rprx-vision" - testInboundVless(t, inboundOptions, outboundOptions) - }) - }) + testInboundVlessTLS(t, inboundOptions, outboundOptions, false) } func TestInboundVless_Grpc1(t *testing.T) { @@ -210,17 +257,7 @@ func TestInboundVless_Grpc1(t *testing.T) { Network: "grpc", GrpcOpts: outbound.GrpcOptions{GrpcServiceName: "GunService"}, } - testInboundVless(t, inboundOptions, outboundOptions) - t.Run("ECH", func(t *testing.T) { - inboundOptions := inboundOptions - outboundOptions := outboundOptions - inboundOptions.EchKey = echKeyPem - outboundOptions.ECHOpts = outbound.ECHOptions{ - Enable: true, - Config: echConfigBase64, - } - testInboundVless(t, inboundOptions, outboundOptions) - }) + testInboundVlessTLS(t, inboundOptions, outboundOptions, false) } func TestInboundVless_Grpc2(t *testing.T) { @@ -236,17 +273,7 @@ func TestInboundVless_Grpc2(t *testing.T) { Network: "grpc", GrpcOpts: outbound.GrpcOptions{GrpcServiceName: "GunService"}, } - testInboundVless(t, inboundOptions, outboundOptions) - t.Run("ECH", func(t *testing.T) { - inboundOptions := inboundOptions - outboundOptions := outboundOptions - inboundOptions.EchKey = echKeyPem - outboundOptions.ECHOpts = outbound.ECHOptions{ - Enable: true, - Config: echConfigBase64, - } - testInboundVless(t, inboundOptions, outboundOptions) - }) + testInboundVlessTLS(t, inboundOptions, outboundOptions, false) } func TestInboundVless_Reality(t *testing.T) { diff --git a/listener/inbound/vmess.go b/listener/inbound/vmess.go index c04ed0933c..6090f845ce 100644 --- a/listener/inbound/vmess.go +++ b/listener/inbound/vmess.go @@ -16,6 +16,8 @@ type VmessOption struct { GrpcServiceName string `inbound:"grpc-service-name,omitempty"` Certificate string `inbound:"certificate,omitempty"` PrivateKey string `inbound:"private-key,omitempty"` + ClientAuthType string `inbound:"client-auth-type,omitempty"` + ClientAuthCert string `inbound:"client-auth-cert,omitempty"` EchKey string `inbound:"ech-key,omitempty"` RealityConfig RealityConfig `inbound:"reality-config,omitempty"` MuxOption MuxOption `inbound:"mux-option,omitempty"` @@ -62,6 +64,8 @@ func NewVmess(options *VmessOption) (*Vmess, error) { GrpcServiceName: options.GrpcServiceName, Certificate: options.Certificate, PrivateKey: options.PrivateKey, + ClientAuthType: options.ClientAuthType, + ClientAuthCert: options.ClientAuthCert, EchKey: options.EchKey, RealityConfig: options.RealityConfig.Build(), MuxOption: options.MuxOption.Build(), diff --git a/listener/inbound/vmess_test.go b/listener/inbound/vmess_test.go index 58d23dfc55..de3a62d20e 100644 --- a/listener/inbound/vmess_test.go +++ b/listener/inbound/vmess_test.go @@ -54,6 +54,9 @@ func testInboundVMess(t *testing.T, inboundOptions inbound.VmessOption, outbound tunnel.DoTest(t, out) + if outboundOptions.Network == "grpc" { // don't test sing-mux over grpc + return + } testSingMux(t, tunnel, out) } @@ -63,15 +66,7 @@ func TestInboundVMess_Basic(t *testing.T) { testInboundVMess(t, inboundOptions, outboundOptions) } -func TestInboundVMess_TLS(t *testing.T) { - inboundOptions := inbound.VmessOption{ - Certificate: tlsCertificate, - PrivateKey: tlsPrivateKey, - } - outboundOptions := outbound.VmessOption{ - TLS: true, - Fingerprint: tlsFingerprint, - } +func testInboundVMessTLS(t *testing.T, inboundOptions inbound.VmessOption, outboundOptions outbound.VmessOption) { testInboundVMess(t, inboundOptions, outboundOptions) t.Run("ECH", func(t *testing.T) { inboundOptions := inboundOptions @@ -83,6 +78,39 @@ func TestInboundVMess_TLS(t *testing.T) { } testInboundVMess(t, inboundOptions, outboundOptions) }) + t.Run("mTLS", func(t *testing.T) { + inboundOptions := inboundOptions + outboundOptions := outboundOptions + inboundOptions.ClientAuthCert = tlsAuthCertificate + outboundOptions.Certificate = tlsAuthCertificate + outboundOptions.PrivateKey = tlsAuthPrivateKey + testInboundVMess(t, inboundOptions, outboundOptions) + }) + t.Run("mTLS+ECH", func(t *testing.T) { + inboundOptions := inboundOptions + outboundOptions := outboundOptions + inboundOptions.ClientAuthCert = tlsAuthCertificate + outboundOptions.Certificate = tlsAuthCertificate + outboundOptions.PrivateKey = tlsAuthPrivateKey + inboundOptions.EchKey = echKeyPem + outboundOptions.ECHOpts = outbound.ECHOptions{ + Enable: true, + Config: echConfigBase64, + } + testInboundVMess(t, inboundOptions, outboundOptions) + }) +} + +func TestInboundVMess_TLS(t *testing.T) { + inboundOptions := inbound.VmessOption{ + Certificate: tlsCertificate, + PrivateKey: tlsPrivateKey, + } + outboundOptions := outbound.VmessOption{ + TLS: true, + Fingerprint: tlsFingerprint, + } + testInboundVMessTLS(t, inboundOptions, outboundOptions) } func TestInboundVMess_Ws(t *testing.T) { @@ -169,17 +197,7 @@ func TestInboundVMess_Wss1(t *testing.T) { Path: "/ws", }, } - testInboundVMess(t, inboundOptions, outboundOptions) - t.Run("ECH", func(t *testing.T) { - inboundOptions := inboundOptions - outboundOptions := outboundOptions - inboundOptions.EchKey = echKeyPem - outboundOptions.ECHOpts = outbound.ECHOptions{ - Enable: true, - Config: echConfigBase64, - } - testInboundVMess(t, inboundOptions, outboundOptions) - }) + testInboundVMessTLS(t, inboundOptions, outboundOptions) } func TestInboundVMess_Wss2(t *testing.T) { @@ -197,17 +215,7 @@ func TestInboundVMess_Wss2(t *testing.T) { Path: "/ws", }, } - testInboundVMess(t, inboundOptions, outboundOptions) - t.Run("ECH", func(t *testing.T) { - inboundOptions := inboundOptions - outboundOptions := outboundOptions - inboundOptions.EchKey = echKeyPem - outboundOptions.ECHOpts = outbound.ECHOptions{ - Enable: true, - Config: echConfigBase64, - } - testInboundVMess(t, inboundOptions, outboundOptions) - }) + testInboundVMessTLS(t, inboundOptions, outboundOptions) } func TestInboundVMess_Grpc1(t *testing.T) { @@ -222,17 +230,7 @@ func TestInboundVMess_Grpc1(t *testing.T) { Network: "grpc", GrpcOpts: outbound.GrpcOptions{GrpcServiceName: "GunService"}, } - testInboundVMess(t, inboundOptions, outboundOptions) - t.Run("ECH", func(t *testing.T) { - inboundOptions := inboundOptions - outboundOptions := outboundOptions - inboundOptions.EchKey = echKeyPem - outboundOptions.ECHOpts = outbound.ECHOptions{ - Enable: true, - Config: echConfigBase64, - } - testInboundVMess(t, inboundOptions, outboundOptions) - }) + testInboundVMessTLS(t, inboundOptions, outboundOptions) } func TestInboundVMess_Grpc2(t *testing.T) { @@ -248,17 +246,7 @@ func TestInboundVMess_Grpc2(t *testing.T) { Network: "grpc", GrpcOpts: outbound.GrpcOptions{GrpcServiceName: "GunService"}, } - testInboundVMess(t, inboundOptions, outboundOptions) - t.Run("ECH", func(t *testing.T) { - inboundOptions := inboundOptions - outboundOptions := outboundOptions - inboundOptions.EchKey = echKeyPem - outboundOptions.ECHOpts = outbound.ECHOptions{ - Enable: true, - Config: echConfigBase64, - } - testInboundVMess(t, inboundOptions, outboundOptions) - }) + testInboundVMessTLS(t, inboundOptions, outboundOptions) } func TestInboundVMess_Reality(t *testing.T) { diff --git a/listener/mixed/mixed.go b/listener/mixed/mixed.go index d9d99ecab4..3df49d2c5a 100644 --- a/listener/mixed/mixed.go +++ b/listener/mixed/mixed.go @@ -16,6 +16,7 @@ import ( "github.com/metacubex/mihomo/listener/http" "github.com/metacubex/mihomo/listener/reality" "github.com/metacubex/mihomo/listener/socks" + "github.com/metacubex/mihomo/ntp" "github.com/metacubex/mihomo/transport/socks4" "github.com/metacubex/mihomo/transport/socks5" ) @@ -61,7 +62,7 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A return nil, err } - tlsConfig := &tlsC.Config{} + tlsConfig := &tlsC.Config{Time: ntp.Now} var realityBuilder *reality.Builder if config.Certificate != "" && config.PrivateKey != "" { @@ -78,10 +79,26 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A } } } + tlsConfig.ClientAuth = tlsC.ClientAuthTypeFromString(config.ClientAuthType) + if len(config.ClientAuthCert) > 0 { + if tlsConfig.ClientAuth == tlsC.NoClientCert { + tlsConfig.ClientAuth = tlsC.RequireAndVerifyClientCert + } + } + if tlsConfig.ClientAuth == tlsC.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tlsC.RequireAndVerifyClientCert { + pool, err := ca.LoadCertificates(config.ClientAuthCert, C.Path) + if err != nil { + return nil, err + } + tlsConfig.ClientCAs = pool + } if config.RealityConfig.PrivateKey != "" { if tlsConfig.Certificates != nil { return nil, errors.New("certificate is unavailable in reality") } + if tlsConfig.ClientAuth != tlsC.NoClientCert { + return nil, errors.New("client-auth is unavailable in reality") + } realityBuilder, err = config.RealityConfig.Build(tunnel) if err != nil { return nil, err diff --git a/listener/sing/sing.go b/listener/sing/sing.go index d2108fea6d..4e229500d1 100644 --- a/listener/sing/sing.go +++ b/listener/sing/sing.go @@ -11,6 +11,7 @@ import ( "github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/adapter/outbound" N "github.com/metacubex/mihomo/common/net" + "github.com/metacubex/mihomo/common/utils" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/log" @@ -103,7 +104,7 @@ func (h *ListenerHandler) ParseSpecialFqdn(ctx context.Context, conn net.Conn, m case mux.Destination.Fqdn: return h.muxService.NewConnection(ctx, conn, UpstreamMetadata(metadata)) case vmess.MuxDestination.Fqdn: - return vmess.HandleMuxConnection(ctx, conn, h) + return vmess.HandleMuxConnection(ctx, conn, metadata, h) case uot.MagicAddress: request, err := uot.ReadRequest(conn) if err != nil { @@ -150,6 +151,8 @@ func (h *ListenerHandler) NewPacketConnection(ctx context.Context, conn network. conn = packetaddr.NewConn(bufio.NewNetPacketConn(conn), M.Socksaddr{}) } + connID := utils.NewUUIDV4().String() // make a new SNAT key + defer func() { _ = conn.Close() }() mutex := sync.Mutex{} writer := bufio.NewNetPacketWriter(conn) // a new interface to set nil in defer @@ -192,6 +195,7 @@ func (h *ListenerHandler) NewPacketConnection(ctx context.Context, conn network. lAddr: conn.LocalAddr(), buff: buff, } + cPacket.rAddr = N.NewCustomAddr(h.Type.String(), connID, cPacket.rAddr) // for tunnel's handleUDPConn if lAddr := getInAddr(ctx); lAddr != nil { cPacket.lAddr = lAddr } @@ -210,13 +214,11 @@ func (h *ListenerHandler) NewPacket(ctx context.Context, key netip.AddrPort, buf cPacket := &packet{ writer: &writer, mutex: &mutex, - rAddr: metadata.Source.UDPAddr(), + rAddr: metadata.Source.UDPAddr(), // TODO: using key argument to make a SNAT key buff: buffer, } - if conn, ok := common.Cast[localAddr](writer); ok { - cPacket.rAddr = conn.LocalAddr() - } else { - cPacket.rAddr = metadata.Source.UDPAddr() // tun does not have real inAddr + if conn, ok := common.Cast[localAddr](writer); ok { // tun does not have real inAddr + cPacket.lAddr = conn.LocalAddr() } h.handlePacket(ctx, cPacket, metadata.Source, metadata.Destination) } diff --git a/listener/sing/util.go b/listener/sing/util.go index 306301621b..20d3e9709d 100644 --- a/listener/sing/util.go +++ b/listener/sing/util.go @@ -12,11 +12,14 @@ import ( func (h *ListenerHandler) HandleSocket(target socks5.Addr, conn net.Conn, _additions ...inbound.Addition) { conn, metadata := inbound.NewSocket(target, conn, h.Type, h.Additions...) if h.IsSpecialFqdn(metadata.Host) { - _ = h.ParseSpecialFqdn( + err := h.ParseSpecialFqdn( WithAdditions(context.Background(), _additions...), conn, ConvertMetadata(metadata), ) + if err != nil { + _ = conn.Close() + } } else { inbound.ApplyAdditions(metadata, _additions...) h.Tunnel.HandleTCPConn(conn, metadata) diff --git a/listener/sing_hysteria2/server.go b/listener/sing_hysteria2/server.go index 849ed4452b..2d172aab4f 100644 --- a/listener/sing_hysteria2/server.go +++ b/listener/sing_hysteria2/server.go @@ -20,6 +20,7 @@ import ( LC "github.com/metacubex/mihomo/listener/config" "github.com/metacubex/mihomo/listener/sing" "github.com/metacubex/mihomo/log" + "github.com/metacubex/mihomo/ntp" "github.com/metacubex/sing-quic/hysteria2" @@ -61,9 +62,23 @@ func New(config LC.Hysteria2Server, tunnel C.Tunnel, additions ...inbound.Additi return nil, err } tlsConfig := &tlsC.Config{ + Time: ntp.Now, MinVersion: tlsC.VersionTLS13, } tlsConfig.Certificates = []tlsC.Certificate{tlsC.UCertificate(cert)} + tlsConfig.ClientAuth = tlsC.ClientAuthTypeFromString(config.ClientAuthType) + if len(config.ClientAuthCert) > 0 { + if tlsConfig.ClientAuth == tlsC.NoClientCert { + tlsConfig.ClientAuth = tlsC.RequireAndVerifyClientCert + } + } + if tlsConfig.ClientAuth == tlsC.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tlsC.RequireAndVerifyClientCert { + pool, err := ca.LoadCertificates(config.ClientAuthCert, C.Path) + if err != nil { + return nil, err + } + tlsConfig.ClientCAs = pool + } if config.EchKey != "" { err = ech.LoadECHKey(config.EchKey, tlsConfig, C.Path) diff --git a/listener/sing_shadowsocks/server.go b/listener/sing_shadowsocks/server.go index 65adce659a..e106867d5d 100644 --- a/listener/sing_shadowsocks/server.go +++ b/listener/sing_shadowsocks/server.go @@ -14,6 +14,7 @@ import ( "github.com/metacubex/mihomo/listener/sing" "github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/ntp" + "github.com/metacubex/mihomo/transport/kcptun" shadowsocks "github.com/metacubex/sing-shadowsocks" "github.com/metacubex/sing-shadowsocks/shadowaead" @@ -138,6 +139,12 @@ func New(config LC.ShadowsocksServer, tunnel C.Tunnel, additions ...inbound.Addi } } + var kcptunServer *kcptun.Server + if config.KcpTun.Enable { + kcptunServer = kcptun.NewServer(config.KcpTun.Config) + config.Udp = true + } + for _, addr := range strings.Split(config.Listen, ",") { addr := addr @@ -154,6 +161,14 @@ func New(config LC.ShadowsocksServer, tunnel C.Tunnel, additions ...inbound.Addi sl.udpListeners = append(sl.udpListeners, ul) + if kcptunServer != nil { + go kcptunServer.Serve(ul, func(c net.Conn) { + sl.HandleConn(c, tunnel) + }) + + continue // skip tcp listener + } + go func() { conn := bufio.NewPacketConn(ul) rwOptions := network.NewReadWaitOptions(conn, sl.service) diff --git a/listener/sing_tun/dns.go b/listener/sing_tun/dns.go index 0b8a3ebe5d..82a9fdb6c1 100644 --- a/listener/sing_tun/dns.go +++ b/listener/sing_tun/dns.go @@ -20,7 +20,8 @@ import ( type ListenerHandler struct { *sing.ListenerHandler - DnsAdds []netip.AddrPort + DnsAdds []netip.AddrPort + DisableICMPForwarding bool } func (h *ListenerHandler) ShouldHijackDns(targetAddr netip.AddrPort) bool { diff --git a/listener/sing_tun/prepare.go b/listener/sing_tun/prepare.go new file mode 100644 index 0000000000..e59947b84b --- /dev/null +++ b/listener/sing_tun/prepare.go @@ -0,0 +1,34 @@ +package sing_tun + +import ( + "context" + "time" + + "github.com/metacubex/mihomo/component/dialer" + "github.com/metacubex/mihomo/component/resolver" + "github.com/metacubex/mihomo/log" + + tun "github.com/metacubex/sing-tun" + "github.com/metacubex/sing-tun/ping" + M "github.com/metacubex/sing/common/metadata" + N "github.com/metacubex/sing/common/network" +) + +func (h *ListenerHandler) PrepareConnection(network string, source M.Socksaddr, destination M.Socksaddr, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error) { + switch network { + case N.NetworkICMP: // our fork only send those type to PrepareConnection now + if h.DisableICMPForwarding || resolver.IsFakeIP(destination.Addr) { // skip fakeip and if ICMP handling is disabled + log.Infoln("[ICMP] %s %s --> %s using fake ping echo", network, source, destination) + return nil, nil + } + log.Infoln("[ICMP] %s %s --> %s using DIRECT", network, source, destination) + directRouteDestination, err := ping.ConnectDestination(context.TODO(), log.SingLogger, dialer.ICMPControl(destination.Addr), destination.Addr, routeContext, timeout) + if err != nil { + log.Warnln("[ICMP] failed to connect to %s", destination) + return nil, err + } + log.Debugln("[ICMP] success connect to %s", destination) + return directRouteDestination, nil + } + return nil, nil +} diff --git a/listener/sing_tun/server.go b/listener/sing_tun/server.go index 416cf30ced..ccd12f42db 100644 --- a/listener/sing_tun/server.go +++ b/listener/sing_tun/server.go @@ -11,6 +11,7 @@ import ( "strconv" "strings" "sync" + "time" "github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/component/dialer" @@ -174,11 +175,11 @@ func New(options LC.Tun, tunnel C.Tunnel, additions ...inbound.Addition) (l *Lis if tunMTU == 0 { tunMTU = 9000 } - var udpTimeout int64 + var udpTimeout time.Duration if options.UDPTimeout != 0 { - udpTimeout = options.UDPTimeout + udpTimeout = time.Second * time.Duration(options.UDPTimeout) } else { - udpTimeout = int64(sing.UDPTimeout.Seconds()) + udpTimeout = sing.UDPTimeout } tableIndex := options.IPRoute2TableIndex if tableIndex == 0 { @@ -266,8 +267,9 @@ func New(options LC.Tun, tunnel C.Tunnel, additions ...inbound.Addition) (l *Lis } handler := &ListenerHandler{ - ListenerHandler: h, - DnsAdds: dnsAdds, + ListenerHandler: h, + DnsAdds: dnsAdds, + DisableICMPForwarding: options.DisableICMPForwarding, } l = &Listener{ closed: false, diff --git a/listener/sing_vless/server.go b/listener/sing_vless/server.go index 5280aa1376..a210d70a2d 100644 --- a/listener/sing_vless/server.go +++ b/listener/sing_vless/server.go @@ -5,9 +5,7 @@ import ( "errors" "net" "net/http" - "reflect" "strings" - "unsafe" "github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/component/ca" @@ -17,48 +15,20 @@ import ( LC "github.com/metacubex/mihomo/listener/config" "github.com/metacubex/mihomo/listener/reality" "github.com/metacubex/mihomo/listener/sing" - "github.com/metacubex/mihomo/log" + "github.com/metacubex/mihomo/ntp" "github.com/metacubex/mihomo/transport/gun" "github.com/metacubex/mihomo/transport/vless/encryption" mihomoVMess "github.com/metacubex/mihomo/transport/vmess" - "github.com/metacubex/sing-vmess/vless" "github.com/metacubex/sing/common" "github.com/metacubex/sing/common/metadata" - "github.com/metacubex/sing/common/network" ) -func init() { - vless.RegisterTLS(func(conn net.Conn) (loaded bool, netConn net.Conn, reflectType reflect.Type, reflectPointer unsafe.Pointer) { - tlsConn, loaded := network.CastReader[*reality.Conn](conn) // *utls.Conn - if !loaded { - return - } - return true, tlsConn.NetConn(), reflect.TypeOf(tlsConn).Elem(), unsafe.Pointer(tlsConn) - }) - - vless.RegisterTLS(func(conn net.Conn) (loaded bool, netConn net.Conn, reflectType reflect.Type, reflectPointer unsafe.Pointer) { - tlsConn, loaded := network.CastReader[*tlsC.UConn](conn) // *utls.UConn - if !loaded { - return - } - return true, tlsConn.NetConn(), reflect.TypeOf(tlsConn.Conn).Elem(), unsafe.Pointer(tlsConn.Conn) - }) - - vless.RegisterTLS(func(conn net.Conn) (loaded bool, netConn net.Conn, reflectType reflect.Type, reflectPointer unsafe.Pointer) { - tlsConn, loaded := network.CastReader[*encryption.CommonConn](conn) - if !loaded { - return - } - return true, tlsConn.Conn, reflect.TypeOf(tlsConn).Elem(), unsafe.Pointer(tlsConn) - }) -} - type Listener struct { closed bool config LC.VlessServer listeners []net.Listener - service *vless.Service[string] + service *Service[string] decryption *encryption.ServerInstance } @@ -79,7 +49,7 @@ func New(config LC.VlessServer, tunnel C.Tunnel, additions ...inbound.Addition) return nil, err } - service := vless.NewService[string](log.SingLogger, h) + service := NewService[string](h) service.UpdateUsers( common.Map(config.Users, func(it LC.VlessUser) string { return it.Username @@ -106,7 +76,7 @@ func New(config LC.VlessServer, tunnel C.Tunnel, additions ...inbound.Addition) }() } - tlsConfig := &tlsC.Config{} + tlsConfig := &tlsC.Config{Time: ntp.Now} var realityBuilder *reality.Builder var httpServer http.Server @@ -124,10 +94,26 @@ func New(config LC.VlessServer, tunnel C.Tunnel, additions ...inbound.Addition) } } } + tlsConfig.ClientAuth = tlsC.ClientAuthTypeFromString(config.ClientAuthType) + if len(config.ClientAuthCert) > 0 { + if tlsConfig.ClientAuth == tlsC.NoClientCert { + tlsConfig.ClientAuth = tlsC.RequireAndVerifyClientCert + } + } + if tlsConfig.ClientAuth == tlsC.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tlsC.RequireAndVerifyClientCert { + pool, err := ca.LoadCertificates(config.ClientAuthCert, C.Path) + if err != nil { + return nil, err + } + tlsConfig.ClientCAs = pool + } if config.RealityConfig.PrivateKey != "" { if tlsConfig.Certificates != nil { return nil, errors.New("certificate is unavailable in reality") } + if tlsConfig.ClientAuth != tlsC.NoClientCert { + return nil, errors.New("client-auth is unavailable in reality") + } realityBuilder, err = config.RealityConfig.Build(tunnel) if err != nil { return nil, err @@ -230,7 +216,7 @@ func (l *Listener) HandleConn(conn net.Conn, tunnel C.Tunnel, additions ...inbou ctx := sing.WithAdditions(context.TODO(), additions...) if l.decryption != nil { var err error - conn, err = l.decryption.Handshake(conn) + conn, err = l.decryption.Handshake(conn, nil) if err != nil { return } diff --git a/listener/sing_vless/service.go b/listener/sing_vless/service.go new file mode 100644 index 0000000000..d30b3c018f --- /dev/null +++ b/listener/sing_vless/service.go @@ -0,0 +1,304 @@ +package sing_vless + +// copy and modify from https://github.com/SagerNet/sing-vmess/tree/3c1cf255413250b09a57e4ecdf1def1fa505e3cc/vless + +import ( + "context" + "encoding/binary" + "io" + "net" + + "github.com/metacubex/mihomo/transport/vless" + "github.com/metacubex/mihomo/transport/vless/vision" + + "github.com/gofrs/uuid/v5" + "github.com/metacubex/sing-vmess" + "github.com/metacubex/sing/common/auth" + "github.com/metacubex/sing/common/buf" + "github.com/metacubex/sing/common/bufio" + E "github.com/metacubex/sing/common/exceptions" + M "github.com/metacubex/sing/common/metadata" + N "github.com/metacubex/sing/common/network" + "google.golang.org/protobuf/proto" +) + +type Service[T comparable] struct { + userMap map[[16]byte]T + userFlow map[T]string + handler Handler +} + +type Handler interface { + N.TCPConnectionHandler + N.UDPConnectionHandler + E.Handler +} + +func NewService[T comparable](handler Handler) *Service[T] { + return &Service[T]{ + handler: handler, + } +} + +func (s *Service[T]) UpdateUsers(userList []T, userUUIDList []string, userFlowList []string) { + userMap := make(map[[16]byte]T) + userFlowMap := make(map[T]string) + for i, userName := range userList { + userID, err := uuid.FromString(userUUIDList[i]) + if err != nil { + userID = uuid.NewV5(uuid.Nil, userUUIDList[i]) + } + userMap[userID] = userName + userFlowMap[userName] = userFlowList[i] + } + s.userMap = userMap + s.userFlow = userFlowMap +} + +var _ N.TCPConnectionHandler = (*Service[int])(nil) + +func (s *Service[T]) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { + var version uint8 + err := binary.Read(conn, binary.BigEndian, &version) + if err != nil { + return err + } + if version != vless.Version { + return E.New("unknown version: ", version) + } + + var requestUUID [16]byte + _, err = io.ReadFull(conn, requestUUID[:]) + if err != nil { + return err + } + + var addonsLen uint8 + err = binary.Read(conn, binary.BigEndian, &addonsLen) + if err != nil { + return err + } + + var addons vless.Addons + if addonsLen > 0 { + addonsBytes := make([]byte, addonsLen) + _, err = io.ReadFull(conn, addonsBytes) + if err != nil { + return err + } + + err = proto.Unmarshal(addonsBytes, &addons) + if err != nil { + return err + } + } + + var command byte + err = binary.Read(conn, binary.BigEndian, &command) + if err != nil { + return err + } + + var destination M.Socksaddr + if command != vless.CommandMux { + destination, err = vmess.AddressSerializer.ReadAddrPort(conn) + if err != nil { + return err + } + } + + user, loaded := s.userMap[requestUUID] + if !loaded { + return E.New("unknown UUID: ", uuid.FromBytesOrNil(requestUUID[:])) + } + ctx = auth.ContextWithUser(ctx, user) + metadata.Destination = destination + + userFlow := s.userFlow[user] + requestFlow := addons.Flow + if requestFlow != userFlow && requestFlow != "" { + return E.New("flow mismatch: expected ", flowName(userFlow), ", but got ", flowName(requestFlow)) + } + + responseConn := &serverConn{ExtendedConn: bufio.NewExtendedConn(conn)} + switch requestFlow { + case vless.XRV: + conn, err = vision.NewConn(responseConn, conn, requestUUID) + if err != nil { + return E.Cause(err, "initialize vision") + } + case "": + conn = responseConn + default: + return E.New("unknown flow: ", requestFlow) + } + switch command { + case vless.CommandTCP: + return s.handler.NewConnection(ctx, conn, metadata) + case vless.CommandUDP: + if requestFlow == vless.XRV { + return E.New(vless.XRV, " flow does not support UDP") + } + return s.handler.NewPacketConnection(ctx, &serverPacketConn{ExtendedConn: bufio.NewExtendedConn(conn), destination: destination}, metadata) + case vless.CommandMux: + return vmess.HandleMuxConnection(ctx, conn, metadata, s.handler) + default: + return E.New("unknown command: ", command) + } +} + +func flowName(value string) string { + if value == "" { + return "none" + } + return value +} + +type serverConn struct { + N.ExtendedConn + responseWritten bool +} + +func (c *serverConn) Write(b []byte) (n int, err error) { + if !c.responseWritten { + buffer := buf.NewSize(2 + len(b)) + buffer.WriteByte(vless.Version) + buffer.WriteByte(0) + buffer.Write(b) + _, err = c.ExtendedConn.Write(buffer.Bytes()) + buffer.Release() + if err == nil { + n = len(b) + } + c.responseWritten = true + return + } + return c.ExtendedConn.Write(b) +} + +func (c *serverConn) WriteBuffer(buffer *buf.Buffer) error { + if !c.responseWritten { + header := buffer.ExtendHeader(2) + header[0] = vless.Version + header[1] = 0 + c.responseWritten = true + } + return c.ExtendedConn.WriteBuffer(buffer) +} + +func (c *serverConn) FrontHeadroom() int { + if c.responseWritten { + return 0 + } + return 2 +} + +func (c *serverConn) ReaderReplaceable() bool { + return true +} + +func (c *serverConn) WriterReplaceable() bool { + return c.responseWritten +} + +func (c *serverConn) NeedAdditionalReadDeadline() bool { + return true +} + +func (c *serverConn) Upstream() any { + return c.ExtendedConn +} + +type serverPacketConn struct { + N.ExtendedConn + destination M.Socksaddr + readWaitOptions N.ReadWaitOptions +} + +func (c *serverPacketConn) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) { + c.readWaitOptions = options + return false +} + +func (c *serverPacketConn) WaitReadPacket() (buffer *buf.Buffer, destination M.Socksaddr, err error) { + var packetLen uint16 + err = binary.Read(c.ExtendedConn, binary.BigEndian, &packetLen) + if err != nil { + return + } + + buffer = c.readWaitOptions.NewPacketBuffer() + _, err = buffer.ReadFullFrom(c.ExtendedConn, int(packetLen)) + if err != nil { + buffer.Release() + return + } + c.readWaitOptions.PostReturn(buffer) + + destination = c.destination + return +} + +func (c *serverPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { + var packetLen uint16 + err = binary.Read(c.ExtendedConn, binary.BigEndian, &packetLen) + if err != nil { + return + } + if len(p) < int(packetLen) { + err = io.ErrShortBuffer + return + } + n, err = io.ReadFull(c.ExtendedConn, p[:packetLen]) + if err != nil { + return + } + if c.destination.IsFqdn() { + addr = c.destination + } else { + addr = c.destination.UDPAddr() + } + return +} + +func (c *serverPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { + err = binary.Write(c.ExtendedConn, binary.BigEndian, uint16(len(p))) + if err != nil { + return + } + return c.ExtendedConn.Write(p) +} + +func (c *serverPacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) { + var packetLen uint16 + err = binary.Read(c.ExtendedConn, binary.BigEndian, &packetLen) + if err != nil { + return + } + + _, err = buffer.ReadFullFrom(c.ExtendedConn, int(packetLen)) + if err != nil { + return + } + + destination = c.destination + return +} + +func (c *serverPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error { + packetLen := buffer.Len() + binary.BigEndian.PutUint16(buffer.ExtendHeader(2), uint16(packetLen)) + return c.ExtendedConn.WriteBuffer(buffer) +} + +func (c *serverPacketConn) FrontHeadroom() int { + return 2 +} + +func (c *serverPacketConn) NeedAdditionalReadDeadline() bool { + return true +} + +func (c *serverPacketConn) Upstream() any { + return c.ExtendedConn +} diff --git a/listener/sing_vmess/server.go b/listener/sing_vmess/server.go index 0b4d013a8b..24c323baab 100644 --- a/listener/sing_vmess/server.go +++ b/listener/sing_vmess/server.go @@ -76,7 +76,7 @@ func New(config LC.VmessServer, tunnel C.Tunnel, additions ...inbound.Addition) sl = &Listener{false, config, nil, service} - tlsConfig := &tlsC.Config{} + tlsConfig := &tlsC.Config{Time: ntp.Now} var realityBuilder *reality.Builder var httpServer http.Server @@ -94,10 +94,26 @@ func New(config LC.VmessServer, tunnel C.Tunnel, additions ...inbound.Addition) } } } + tlsConfig.ClientAuth = tlsC.ClientAuthTypeFromString(config.ClientAuthType) + if len(config.ClientAuthCert) > 0 { + if tlsConfig.ClientAuth == tlsC.NoClientCert { + tlsConfig.ClientAuth = tlsC.RequireAndVerifyClientCert + } + } + if tlsConfig.ClientAuth == tlsC.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tlsC.RequireAndVerifyClientCert { + pool, err := ca.LoadCertificates(config.ClientAuthCert, C.Path) + if err != nil { + return nil, err + } + tlsConfig.ClientCAs = pool + } if config.RealityConfig.PrivateKey != "" { if tlsConfig.Certificates != nil { return nil, errors.New("certificate is unavailable in reality") } + if tlsConfig.ClientAuth != tlsC.NoClientCert { + return nil, errors.New("client-auth is unavailable in reality") + } realityBuilder, err = config.RealityConfig.Build(tunnel) if err != nil { return nil, err diff --git a/listener/socks/tcp.go b/listener/socks/tcp.go index 33cf02f088..60eaa7411c 100644 --- a/listener/socks/tcp.go +++ b/listener/socks/tcp.go @@ -15,6 +15,7 @@ import ( authStore "github.com/metacubex/mihomo/listener/auth" LC "github.com/metacubex/mihomo/listener/config" "github.com/metacubex/mihomo/listener/reality" + "github.com/metacubex/mihomo/ntp" "github.com/metacubex/mihomo/transport/socks4" "github.com/metacubex/mihomo/transport/socks5" ) @@ -60,7 +61,7 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A return nil, err } - tlsConfig := &tlsC.Config{} + tlsConfig := &tlsC.Config{Time: ntp.Now} var realityBuilder *reality.Builder if config.Certificate != "" && config.PrivateKey != "" { @@ -77,10 +78,26 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A } } } + tlsConfig.ClientAuth = tlsC.ClientAuthTypeFromString(config.ClientAuthType) + if len(config.ClientAuthCert) > 0 { + if tlsConfig.ClientAuth == tlsC.NoClientCert { + tlsConfig.ClientAuth = tlsC.RequireAndVerifyClientCert + } + } + if tlsConfig.ClientAuth == tlsC.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tlsC.RequireAndVerifyClientCert { + pool, err := ca.LoadCertificates(config.ClientAuthCert, C.Path) + if err != nil { + return nil, err + } + tlsConfig.ClientCAs = pool + } if config.RealityConfig.PrivateKey != "" { if tlsConfig.Certificates != nil { return nil, errors.New("certificate is unavailable in reality") } + if tlsConfig.ClientAuth != tlsC.NoClientCert { + return nil, errors.New("client-auth is unavailable in reality") + } realityBuilder, err = config.RealityConfig.Build(tunnel) if err != nil { return nil, err diff --git a/listener/trojan/server.go b/listener/trojan/server.go index 3ea7c3879f..03fb02cfcb 100644 --- a/listener/trojan/server.go +++ b/listener/trojan/server.go @@ -15,6 +15,7 @@ import ( LC "github.com/metacubex/mihomo/listener/config" "github.com/metacubex/mihomo/listener/reality" "github.com/metacubex/mihomo/listener/sing" + "github.com/metacubex/mihomo/ntp" "github.com/metacubex/mihomo/transport/gun" "github.com/metacubex/mihomo/transport/shadowsocks/core" "github.com/metacubex/mihomo/transport/socks5" @@ -70,7 +71,7 @@ func New(config LC.TrojanServer, tunnel C.Tunnel, additions ...inbound.Addition) } sl = &Listener{false, config, nil, keys, pickCipher, h} - tlsConfig := &tlsC.Config{} + tlsConfig := &tlsC.Config{Time: ntp.Now} var realityBuilder *reality.Builder var httpServer http.Server @@ -88,10 +89,26 @@ func New(config LC.TrojanServer, tunnel C.Tunnel, additions ...inbound.Addition) } } } + tlsConfig.ClientAuth = tlsC.ClientAuthTypeFromString(config.ClientAuthType) + if len(config.ClientAuthCert) > 0 { + if tlsConfig.ClientAuth == tlsC.NoClientCert { + tlsConfig.ClientAuth = tlsC.RequireAndVerifyClientCert + } + } + if tlsConfig.ClientAuth == tlsC.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tlsC.RequireAndVerifyClientCert { + pool, err := ca.LoadCertificates(config.ClientAuthCert, C.Path) + if err != nil { + return nil, err + } + tlsConfig.ClientCAs = pool + } if config.RealityConfig.PrivateKey != "" { if tlsConfig.Certificates != nil { return nil, errors.New("certificate is unavailable in reality") } + if tlsConfig.ClientAuth != tlsC.NoClientCert { + return nil, errors.New("client-auth is unavailable in reality") + } realityBuilder, err = config.RealityConfig.Build(tunnel) if err != nil { return nil, err diff --git a/listener/tuic/server.go b/listener/tuic/server.go index 2037177e3a..f105c51ac3 100644 --- a/listener/tuic/server.go +++ b/listener/tuic/server.go @@ -14,6 +14,7 @@ import ( LC "github.com/metacubex/mihomo/listener/config" "github.com/metacubex/mihomo/listener/sing" "github.com/metacubex/mihomo/log" + "github.com/metacubex/mihomo/ntp" "github.com/metacubex/mihomo/transport/socks5" "github.com/metacubex/mihomo/transport/tuic" @@ -53,9 +54,23 @@ func New(config LC.TuicServer, tunnel C.Tunnel, additions ...inbound.Addition) ( return nil, err } tlsConfig := &tlsC.Config{ + Time: ntp.Now, MinVersion: tlsC.VersionTLS13, } tlsConfig.Certificates = []tlsC.Certificate{tlsC.UCertificate(cert)} + tlsConfig.ClientAuth = tlsC.ClientAuthTypeFromString(config.ClientAuthType) + if len(config.ClientAuthCert) > 0 { + if tlsConfig.ClientAuth == tlsC.NoClientCert { + tlsConfig.ClientAuth = tlsC.RequireAndVerifyClientCert + } + } + if tlsConfig.ClientAuth == tlsC.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tlsC.RequireAndVerifyClientCert { + pool, err := ca.LoadCertificates(config.ClientAuthCert, C.Path) + if err != nil { + return nil, err + } + tlsConfig.ClientCAs = pool + } if config.EchKey != "" { err = ech.LoadECHKey(config.EchKey, tlsConfig, C.Path) diff --git a/main.go b/main.go index 6a85e5df79..6f21e8110a 100644 --- a/main.go +++ b/main.go @@ -62,7 +62,19 @@ func main() { // Defensive programming: panic when code mistakenly calls net.DefaultResolver net.DefaultResolver.PreferGo = true net.DefaultResolver.Dial = func(ctx context.Context, network, address string) (net.Conn, error) { - panic("should never be called") + //panic("should never be called") + buf := make([]byte, 1024) + for { + n := runtime.Stack(buf, true) + if n < len(buf) { + buf = buf[:n] + break + } + buf = make([]byte, 2*len(buf)) + } + fmt.Fprintf(os.Stderr, "panic: should never be called\n\n%s", buf) // always print all goroutine stack + os.Exit(2) + return nil, nil } _, _ = maxprocs.Set(maxprocs.Logger(func(string, ...any) {})) diff --git a/ntp/service.go b/ntp/service.go index 6fb7453e29..8473df6a8f 100644 --- a/ntp/service.go +++ b/ntp/service.go @@ -3,6 +3,7 @@ package ntp import ( "context" "sync" + "sync/atomic" "time" "github.com/metacubex/mihomo/component/dialer" @@ -13,8 +14,8 @@ import ( "github.com/metacubex/sing/common/ntp" ) -var offset time.Duration -var service *Service +var globalSrv atomic.Pointer[Service] +var globalMu sync.Mutex type Service struct { server M.Socksaddr @@ -22,17 +23,21 @@ type Service struct { ticker *time.Ticker ctx context.Context cancel context.CancelFunc - mu sync.Mutex + offset atomic.Int64 // [time.Duration] syncSystemTime bool - running bool } func ReCreateNTPService(server string, interval time.Duration, dialerProxy string, syncSystemTime bool) { - if service != nil { + globalMu.Lock() + defer globalMu.Unlock() + if service := globalSrv.Swap(nil); service != nil { service.Stop() } + if server == "" || interval <= 0 { + return + } ctx, cancel := context.WithCancel(context.Background()) - service = &Service{ + service := &Service{ server: M.ParseSocksaddr(server), dialer: proxydialer.NewByNameSingDialer(dialerProxy, dialer.NewDialer()), ticker: time.NewTicker(interval * time.Minute), @@ -41,88 +46,78 @@ func ReCreateNTPService(server string, interval time.Duration, dialerProxy strin syncSystemTime: syncSystemTime, } service.Start() + globalSrv.Store(service) } func (srv *Service) Start() { - srv.mu.Lock() - defer srv.mu.Unlock() log.Infoln("NTP service start, sync system time is %t", srv.syncSystemTime) - err := srv.update() - if err != nil { - log.Errorln("Initialize NTP time failed: %s", err) - return - } - service.running = true go srv.loopUpdate() } func (srv *Service) Stop() { - srv.mu.Lock() - defer srv.mu.Unlock() - if service.running { - srv.ticker.Stop() - srv.cancel() - service.running = false - } + log.Infoln("NTP service stop") + srv.cancel() } -func (srv *Service) Running() bool { - if srv == nil { - return false - } - srv.mu.Lock() - defer srv.mu.Unlock() - return srv.running +func (srv *Service) Offset() time.Duration { + return time.Duration(srv.offset.Load()) } func (srv *Service) update() error { var response *ntp.Response var err error for i := 0; i < 3; i++ { - if response, err = ntp.Exchange(context.Background(), srv.dialer, srv.server); err == nil { - break + response, err = ntp.Exchange(srv.ctx, srv.dialer, srv.server) + if err != nil { + if srv.ctx.Err() != nil { + return nil + } + continue } - if i == 2 { - return err + offset := response.ClockOffset + if offset > time.Duration(0) { + log.Infoln("System clock is ahead of NTP time by %s", offset) + } else if offset < time.Duration(0) { + log.Infoln("System clock is behind NTP time by %s", -offset) } - } - offset = response.ClockOffset - if offset > time.Duration(0) { - log.Infoln("System clock is ahead of NTP time by %s", offset) - } else if offset < time.Duration(0) { - log.Infoln("System clock is behind NTP time by %s", -offset) - } - if srv.syncSystemTime { - timeNow := response.Time - syncErr := setSystemTime(timeNow) - if syncErr == nil { - log.Infoln("Sync system time success: %s", timeNow.Local().Format(ntp.TimeLayout)) - } else { - log.Errorln("Write time to system: %s", syncErr) - srv.syncSystemTime = false + srv.offset.Store(int64(offset)) + if srv.syncSystemTime { + timeNow := response.Time + syncErr := setSystemTime(timeNow) + if syncErr == nil { + log.Infoln("Sync system time success: %s", timeNow.Local().Format(ntp.TimeLayout)) + } else { + log.Errorln("Write time to system: %s", syncErr) + srv.syncSystemTime = false + } } + return nil } - return nil + return err } func (srv *Service) loopUpdate() { + defer srv.offset.Store(0) + defer srv.ticker.Stop() for { + err := srv.update() + if err != nil { + log.Warnln("Sync time failed: %s", err) + } select { case <-srv.ctx.Done(): return case <-srv.ticker.C: } - err := srv.update() - if err != nil { - log.Warnln("Sync time failed: %s", err) - } } } func Now() time.Time { now := time.Now() - if service.Running() && offset.Abs() > 0 { - now = now.Add(offset) + if service := globalSrv.Load(); service != nil { + if offset := service.Offset(); offset.Abs() > 0 { + now = now.Add(offset) + } } return now } diff --git a/transport/anytls/client.go b/transport/anytls/client.go index 4e74c9272c..dcb679ff4a 100644 --- a/transport/anytls/client.go +++ b/transport/anytls/client.go @@ -46,7 +46,7 @@ func NewClient(ctx context.Context, config ClientConfig) *Client { } // Initialize the padding state of this client padding.UpdatePaddingScheme(padding.DefaultPaddingScheme, &c.padding) - c.sessionClient = session.NewClient(ctx, c.CreateOutboundTLSConnection, &c.padding, config.IdleSessionCheckInterval, config.IdleSessionTimeout, config.MinIdleSession) + c.sessionClient = session.NewClient(ctx, c.createOutboundTLSConnection, &c.padding, config.IdleSessionCheckInterval, config.IdleSessionTimeout, config.MinIdleSession) return c } @@ -63,7 +63,7 @@ func (c *Client) CreateProxy(ctx context.Context, destination M.Socksaddr) (net. return conn, nil } -func (c *Client) CreateOutboundTLSConnection(ctx context.Context) (net.Conn, error) { +func (c *Client) createOutboundTLSConnection(ctx context.Context) (net.Conn, error) { conn, err := c.dialer.DialContext(ctx, N.NetworkTCP, c.server) if err != nil { return nil, err diff --git a/transport/anytls/session/client.go b/transport/anytls/session/client.go index 0698bd5006..b2d1cf7f52 100644 --- a/transport/anytls/session/client.go +++ b/transport/anytls/session/client.go @@ -66,23 +66,21 @@ func (c *Client) CreateStream(ctx context.Context) (net.Conn, error) { var stream *Stream var err error - for i := 0; i < 3; i++ { - session, err = c.findSession(ctx) - if session == nil { - return nil, fmt.Errorf("failed to create session: %w", err) - } - stream, err = session.OpenStream() - if err != nil { - _ = session.Close() - continue - } - break + session = c.getIdleSession() + if session == nil { + session, err = c.createSession(ctx) } - if session == nil || stream == nil { - return nil, fmt.Errorf("too many closed session: %w", err) + if session == nil { + return nil, fmt.Errorf("failed to create session: %w", err) + } + stream, err = session.OpenStream() + if err != nil { + session.Close() + return nil, fmt.Errorf("failed to create stream: %w", err) } stream.dieHook = func() { + // If Session is not closed, put this Stream to pool if !session.IsClosed() { select { case <-c.die.Done(): @@ -100,9 +98,7 @@ func (c *Client) CreateStream(ctx context.Context) (net.Conn, error) { return stream, nil } -func (c *Client) findSession(ctx context.Context) (*Session, error) { - var idle *Session - +func (c *Client) getIdleSession() (idle *Session) { c.idleSessionLock.Lock() if !c.idleSession.IsEmpty() { it := c.idleSession.Iterate() @@ -110,12 +106,7 @@ func (c *Client) findSession(ctx context.Context) (*Session, error) { c.idleSession.Remove(it.Key()) } c.idleSessionLock.Unlock() - - if idle == nil { - s, err := c.createSession(ctx) - return s, err - } - return idle, nil + return } func (c *Client) createSession(ctx context.Context) (*Session, error) { @@ -127,7 +118,6 @@ func (c *Client) createSession(ctx context.Context) (*Session, error) { session := NewClientSession(underlying, c.padding) session.seq = c.sessionCounter.Add(1) session.dieHook = func() { - //logrus.Debugln("session died", session) c.idleSessionLock.Lock() c.idleSession.Remove(math.MaxUint64 - session.seq) c.idleSessionLock.Unlock() @@ -168,12 +158,11 @@ func (c *Client) idleCleanup() { } func (c *Client) idleCleanupExpTime(expTime time.Time) { - sessionToRemove := make([]*Session, 0, c.idleSession.Len()) + activeCount := 0 + sessionToClose := make([]*Session, 0, c.idleSession.Len()) c.idleSessionLock.Lock() it := c.idleSession.Iterate() - - activeCount := 0 for it.IsNotEnd() { session := it.Value() key := it.Key() @@ -190,12 +179,12 @@ func (c *Client) idleCleanupExpTime(expTime time.Time) { continue } - sessionToRemove = append(sessionToRemove, session) + sessionToClose = append(sessionToClose, session) c.idleSession.Remove(key) } c.idleSessionLock.Unlock() - for _, session := range sessionToRemove { + for _, session := range sessionToClose { session.Close() } } diff --git a/transport/anytls/session/session.go b/transport/anytls/session/session.go index 23c3a087dc..12c5b682e5 100644 --- a/transport/anytls/session/session.go +++ b/transport/anytls/session/session.go @@ -90,7 +90,7 @@ func (s *Session) Run() { f := newFrame(cmdSettings, 0) f.data = settings.ToBytes() s.buffering = true - s.writeFrame(f) + s.writeControlFrame(f) go s.recvLoop() } @@ -119,7 +119,7 @@ func (s *Session) Close() error { } s.streamLock.Lock() for _, stream := range s.streams { - stream.Close() + stream.closeLocally() } s.streams = make(map[uint32]*Stream) s.streamLock.Unlock() @@ -138,8 +138,6 @@ func (s *Session) OpenStream() (*Stream, error) { sid := s.streamId.Add(1) stream := newStream(sid, s) - //logrus.Debugln("stream open", sid, s.streams) - if sid >= 2 && s.peerVersion >= 2 { s.synDoneLock.Lock() if s.synDone != nil { @@ -151,7 +149,7 @@ func (s *Session) OpenStream() (*Stream, error) { s.synDoneLock.Unlock() } - if _, err := s.writeFrame(newFrame(cmdSYN, sid)); err != nil { + if _, err := s.writeControlFrame(newFrame(cmdSYN, sid)); err != nil { return nil, err } @@ -207,7 +205,7 @@ func (s *Session) recvLoop() error { if !s.isClient && !receivedSettingsFromClient { f := newFrame(cmdAlert, 0) f.data = []byte("client did not send its settings") - s.writeFrame(f) + s.writeControlFrame(f) return nil } s.streamLock.Lock() @@ -241,18 +239,18 @@ func (s *Session) recvLoop() error { stream, ok := s.streams[sid] s.streamLock.RUnlock() if ok { - stream.CloseWithError(fmt.Errorf("remote: %s", string(buffer))) + stream.closeWithError(fmt.Errorf("remote: %s", string(buffer))) } pool.Put(buffer) } case cmdFIN: - s.streamLock.RLock() + s.streamLock.Lock() stream, ok := s.streams[sid] - s.streamLock.RUnlock() + delete(s.streams, sid) + s.streamLock.Unlock() if ok { - stream.Close() + stream.closeLocally() } - //logrus.Debugln("stream fin", sid, s.streams) case cmdWaste: if hdr.Length() > 0 { buffer := pool.Get(int(hdr.Length())) @@ -274,10 +272,9 @@ func (s *Session) recvLoop() error { m := util.StringMapFromBytes(buffer) paddingF := s.padding.Load() if m["padding-md5"] != paddingF.Md5 { - // logrus.Debugln("remote md5 is", m["padding-md5"]) f := newFrame(cmdUpdatePaddingScheme, 0) f.data = paddingF.RawScheme - _, err = s.writeFrame(f) + _, err = s.writeControlFrame(f) if err != nil { pool.Put(buffer) return err @@ -291,7 +288,7 @@ func (s *Session) recvLoop() error { f.data = util.StringMap{ "v": "2", }.ToBytes() - _, err = s.writeFrame(f) + _, err = s.writeControlFrame(f) if err != nil { pool.Put(buffer) return err @@ -329,7 +326,7 @@ func (s *Session) recvLoop() error { } } case cmdHeartRequest: - if _, err := s.writeFrame(newFrame(cmdHeartResponse, sid)); err != nil { + if _, err := s.writeControlFrame(newFrame(cmdHeartResponse, sid)); err != nil { return err } case cmdHeartResponse: @@ -364,14 +361,31 @@ func (s *Session) streamClosed(sid uint32) error { if s.IsClosed() { return io.ErrClosedPipe } - _, err := s.writeFrame(newFrame(cmdFIN, sid)) + _, err := s.writeControlFrame(newFrame(cmdFIN, sid)) s.streamLock.Lock() delete(s.streams, sid) s.streamLock.Unlock() return err } -func (s *Session) writeFrame(frame frame) (int, error) { +func (s *Session) writeDataFrame(sid uint32, data []byte) (int, error) { + dataLen := len(data) + + buffer := buf.NewSize(dataLen + headerOverHeadSize) + buffer.WriteByte(cmdPSH) + binary.BigEndian.PutUint32(buffer.Extend(4), sid) + binary.BigEndian.PutUint16(buffer.Extend(2), uint16(dataLen)) + buffer.Write(data) + _, err := s.writeConn(buffer.Bytes()) + buffer.Release() + if err != nil { + return 0, err + } + + return dataLen, nil +} + +func (s *Session) writeControlFrame(frame frame) (int, error) { dataLen := len(frame.data) buffer := buf.NewSize(dataLen + headerOverHeadSize) @@ -379,12 +393,18 @@ func (s *Session) writeFrame(frame frame) (int, error) { binary.BigEndian.PutUint32(buffer.Extend(4), frame.sid) binary.BigEndian.PutUint16(buffer.Extend(2), uint16(dataLen)) buffer.Write(frame.data) + + s.conn.SetWriteDeadline(time.Now().Add(time.Second * 5)) + _, err := s.writeConn(buffer.Bytes()) buffer.Release() if err != nil { + s.Close() return 0, err } + s.conn.SetWriteDeadline(time.Time{}) + return dataLen, nil } diff --git a/transport/anytls/session/stream.go b/transport/anytls/session/stream.go index f7e8de673f..9827b0b3a3 100644 --- a/transport/anytls/session/stream.go +++ b/transport/anytls/session/stream.go @@ -53,21 +53,35 @@ func (s *Stream) Write(b []byte) (n int, err error) { return 0, os.ErrDeadlineExceeded default: } - f := newFrame(cmdPSH, s.id) - f.data = b - n, err = s.sess.writeFrame(f) + if s.dieErr != nil { + return 0, s.dieErr + } + n, err = s.sess.writeDataFrame(s.id, b) return } // Close implements net.Conn func (s *Stream) Close() error { - return s.CloseWithError(io.ErrClosedPipe) + return s.closeWithError(io.ErrClosedPipe) +} + +// closeLocally only closes Stream and don't notify remote peer +func (s *Stream) closeLocally() { + var once bool + s.dieOnce.Do(func() { + s.dieErr = net.ErrClosed + s.pipeR.Close() + once = true + }) + if once { + if s.dieHook != nil { + s.dieHook() + s.dieHook = nil + } + } } -func (s *Stream) CloseWithError(err error) error { - // if err != io.ErrClosedPipe { - // logrus.Debugln(err) - // } +func (s *Stream) closeWithError(err error) error { var once bool s.dieOnce.Do(func() { s.dieErr = err @@ -128,7 +142,7 @@ func (s *Stream) HandshakeFailure(err error) error { if once && err != nil && s.sess.peerVersion >= 2 { f := newFrame(cmdSYNACK, s.id) f.data = []byte(err.Error()) - if _, err := s.sess.writeFrame(f); err != nil { + if _, err := s.sess.writeControlFrame(f); err != nil { return err } } @@ -142,7 +156,7 @@ func (s *Stream) HandshakeSuccess() error { once = true }) if once && s.sess.peerVersion >= 2 { - if _, err := s.sess.writeFrame(newFrame(cmdSYNACK, s.id)); err != nil { + if _, err := s.sess.writeControlFrame(newFrame(cmdSYNACK, s.id)); err != nil { return err } } diff --git a/transport/gost-plugin/websocket.go b/transport/gost-plugin/websocket.go index daedb53274..fbe1ec32d9 100644 --- a/transport/gost-plugin/websocket.go +++ b/transport/gost-plugin/websocket.go @@ -22,6 +22,8 @@ type Option struct { ECHConfig *ech.Config SkipCertVerify bool Fingerprint string + Certificate string + PrivateKey string Mux bool } @@ -57,15 +59,19 @@ func NewGostWebsocket(ctx context.Context, conn net.Conn, option *Option) (net.C Headers: header, } + var err error if option.TLS { config.TLS = true - tlsConfig := &tls.Config{ - ServerName: option.Host, - InsecureSkipVerify: option.SkipCertVerify, - NextProtos: []string{"http/1.1"}, - } - var err error - config.TLSConfig, err = ca.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint) + config.TLSConfig, err = ca.GetTLSConfig(ca.Option{ + TLSConfig: &tls.Config{ + ServerName: option.Host, + InsecureSkipVerify: option.SkipCertVerify, + NextProtos: []string{"http/1.1"}, + }, + Fingerprint: option.Fingerprint, + Certificate: option.Certificate, + PrivateKey: option.PrivateKey, + }) if err != nil { return nil, err } @@ -75,7 +81,6 @@ func NewGostWebsocket(ctx context.Context, conn net.Conn, option *Option) (net.C } } - var err error conn, err = vmess.StreamWebsocketConn(ctx, conn, config) if err != nil { return nil, err diff --git a/transport/kcptun/LICENSE.md b/transport/kcptun/LICENSE.md new file mode 100644 index 0000000000..b0c255f150 --- /dev/null +++ b/transport/kcptun/LICENSE.md @@ -0,0 +1,9 @@ +The MIT License (MIT) + +Copyright (c) 2016 xtaci + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/transport/kcptun/client.go b/transport/kcptun/client.go new file mode 100644 index 0000000000..46731ca962 --- /dev/null +++ b/transport/kcptun/client.go @@ -0,0 +1,171 @@ +package kcptun + +import ( + "context" + "net" + "sync" + "time" + + "github.com/metacubex/mihomo/log" + + "github.com/metacubex/kcp-go" + "github.com/metacubex/randv2" + "github.com/metacubex/smux" +) + +const Mode = "kcptun" + +type DialFn func(ctx context.Context) (net.PacketConn, net.Addr, error) + +type Client struct { + once sync.Once + config Config + block kcp.BlockCrypt + + ctx context.Context + cancel context.CancelFunc + + numconn uint16 + muxes []timedSession + rr uint16 + connMu sync.Mutex + + chScavenger chan timedSession +} + +func NewClient(config Config) *Client { + config.FillDefaults() + block := config.NewBlock() + + ctx, cancel := context.WithCancel(context.Background()) + + return &Client{ + config: config, + block: block, + ctx: ctx, + cancel: cancel, + } +} + +func (c *Client) Close() error { + c.cancel() + return nil +} + +func (c *Client) createConn(ctx context.Context, dial DialFn) (*smux.Session, error) { + conn, addr, err := dial(ctx) + if err != nil { + return nil, err + } + + config := c.config + convid := randv2.Uint32() + kcpconn, err := kcp.NewConn4(convid, addr, c.block, config.DataShard, config.ParityShard, true, conn) + if err != nil { + return nil, err + } + kcpconn.SetStreamMode(true) + kcpconn.SetWriteDelay(false) + kcpconn.SetNoDelay(config.NoDelay, config.Interval, config.Resend, config.NoCongestion) + kcpconn.SetWindowSize(config.SndWnd, config.RcvWnd) + kcpconn.SetMtu(config.MTU) + kcpconn.SetACKNoDelay(config.AckNodelay) + + _ = kcpconn.SetDSCP(config.DSCP) + _ = kcpconn.SetReadBuffer(config.SockBuf) + _ = kcpconn.SetWriteBuffer(config.SockBuf) + smuxConfig := smux.DefaultConfig() + smuxConfig.Version = config.SmuxVer + smuxConfig.MaxReceiveBuffer = config.SmuxBuf + smuxConfig.MaxStreamBuffer = config.StreamBuf + smuxConfig.KeepAliveInterval = time.Duration(config.KeepAlive) * time.Second + if smuxConfig.KeepAliveInterval >= smuxConfig.KeepAliveTimeout { + smuxConfig.KeepAliveTimeout = 3 * smuxConfig.KeepAliveInterval + } + + if err := smux.VerifyConfig(smuxConfig); err != nil { + return nil, err + } + + var netConn net.Conn = kcpconn + if !config.NoComp { + netConn = NewCompStream(netConn) + } + // stream multiplex + return smux.Client(netConn, smuxConfig) +} + +func (c *Client) OpenStream(ctx context.Context, dial DialFn) (*smux.Stream, error) { + c.once.Do(func() { + // start scavenger if autoexpire is set + c.chScavenger = make(chan timedSession, 128) + if c.config.AutoExpire > 0 { + go scavenger(c.ctx, c.chScavenger, &c.config) + } + + c.numconn = uint16(c.config.Conn) + c.muxes = make([]timedSession, c.config.Conn) + c.rr = uint16(0) + }) + + c.connMu.Lock() + idx := c.rr % c.numconn + + // do auto expiration && reconnection + if c.muxes[idx].session == nil || c.muxes[idx].session.IsClosed() || + (c.config.AutoExpire > 0 && time.Now().After(c.muxes[idx].expiryDate)) { + var err error + c.muxes[idx].session, err = c.createConn(ctx, dial) + if err != nil { + c.connMu.Unlock() + return nil, err + } + c.muxes[idx].expiryDate = time.Now().Add(time.Duration(c.config.AutoExpire) * time.Second) + if c.config.AutoExpire > 0 { // only when autoexpire set + c.chScavenger <- c.muxes[idx] + } + + } + c.rr++ + session := c.muxes[idx].session + c.connMu.Unlock() + + return session.OpenStream() +} + +// timedSession is a wrapper for smux.Session with expiry date +type timedSession struct { + session *smux.Session + expiryDate time.Time +} + +// scavenger goroutine is used to close expired sessions +func scavenger(ctx context.Context, ch chan timedSession, config *Config) { + ticker := time.NewTicker(scavengePeriod * time.Second) + defer ticker.Stop() + var sessionList []timedSession + for { + select { + case item := <-ch: + sessionList = append(sessionList, timedSession{ + item.session, + item.expiryDate.Add(time.Duration(config.ScavengeTTL) * time.Second)}) + case <-ticker.C: + var newList []timedSession + for k := range sessionList { + s := sessionList[k] + if s.session.IsClosed() { + log.Debugln("scavenger: session normally closed: %s", s.session.LocalAddr()) + } else if time.Now().After(s.expiryDate) { + s.session.Close() + log.Debugln("scavenger: session closed due to ttl: %s", s.session.LocalAddr()) + } else { + newList = append(newList, sessionList[k]) + } + } + sessionList = newList + case <-ctx.Done(): + return + } + } +} diff --git a/transport/kcptun/common.go b/transport/kcptun/common.go new file mode 100644 index 0000000000..ec260f06d2 --- /dev/null +++ b/transport/kcptun/common.go @@ -0,0 +1,152 @@ +package kcptun + +import ( + "crypto/sha1" + + "github.com/metacubex/mihomo/log" + + "github.com/metacubex/kcp-go" + "golang.org/x/crypto/pbkdf2" +) + +const ( + // SALT is use for pbkdf2 key expansion + SALT = "kcp-go" + // maximum supported smux version + maxSmuxVer = 2 + // scavenger check period + scavengePeriod = 5 +) + +type Config struct { + Key string `json:"key"` + Crypt string `json:"crypt"` + Mode string `json:"mode"` + Conn int `json:"conn"` + AutoExpire int `json:"autoexpire"` + ScavengeTTL int `json:"scavengettl"` + MTU int `json:"mtu"` + SndWnd int `json:"sndwnd"` + RcvWnd int `json:"rcvwnd"` + DataShard int `json:"datashard"` + ParityShard int `json:"parityshard"` + DSCP int `json:"dscp"` + NoComp bool `json:"nocomp"` + AckNodelay bool `json:"acknodelay"` + NoDelay int `json:"nodelay"` + Interval int `json:"interval"` + Resend int `json:"resend"` + NoCongestion int `json:"nc"` + SockBuf int `json:"sockbuf"` + SmuxVer int `json:"smuxver"` + SmuxBuf int `json:"smuxbuf"` + StreamBuf int `json:"streambuf"` + KeepAlive int `json:"keepalive"` +} + +func (config *Config) FillDefaults() { + if config.Key == "" { + config.Key = "it's a secrect" + } + if config.Crypt == "" { + config.Crypt = "aes" + } + if config.Mode == "" { + config.Mode = "fast" + } + if config.Conn == 0 { + config.Conn = 1 + } + if config.ScavengeTTL == 0 { + config.ScavengeTTL = 600 + } + if config.MTU == 0 { + config.MTU = 1350 + } + if config.SndWnd == 0 { + config.SndWnd = 128 + } + if config.RcvWnd == 0 { + config.RcvWnd = 512 + } + if config.DataShard == 0 { + config.DataShard = 10 + } + if config.ParityShard == 0 { + config.ParityShard = 3 + } + if config.Interval == 0 { + config.Interval = 50 + } + if config.SockBuf == 0 { + config.SockBuf = 4194304 + } + if config.SmuxVer == 0 { + config.SmuxVer = 1 + } + if config.SmuxBuf == 0 { + config.SmuxBuf = 4194304 + } + if config.StreamBuf == 0 { + config.StreamBuf = 2097152 + } + if config.KeepAlive == 0 { + config.KeepAlive = 10 + } + switch config.Mode { + case "normal": + config.NoDelay, config.Interval, config.Resend, config.NoCongestion = 0, 40, 2, 1 + case "fast": + config.NoDelay, config.Interval, config.Resend, config.NoCongestion = 0, 30, 2, 1 + case "fast2": + config.NoDelay, config.Interval, config.Resend, config.NoCongestion = 1, 20, 2, 1 + case "fast3": + config.NoDelay, config.Interval, config.Resend, config.NoCongestion = 1, 10, 2, 1 + } + + // SMUX Version check + if config.SmuxVer > maxSmuxVer { + log.Warnln("unsupported smux version: %d", config.SmuxVer) + config.SmuxVer = maxSmuxVer + } + + // Scavenge parameters check + if config.AutoExpire != 0 && config.ScavengeTTL > config.AutoExpire { + log.Warnln("WARNING: scavengettl is bigger than autoexpire, connections may race hard to use bandwidth.") + log.Warnln("Try limiting scavengettl to a smaller value.") + } +} + +func (config *Config) NewBlock() (block kcp.BlockCrypt) { + pass := pbkdf2.Key([]byte(config.Key), []byte(SALT), 4096, 32, sha1.New) + switch config.Crypt { + case "null": + block = nil + case "tea": + block, _ = kcp.NewTEABlockCrypt(pass[:16]) + case "xor": + block, _ = kcp.NewSimpleXORBlockCrypt(pass) + case "none": + block, _ = kcp.NewNoneBlockCrypt(pass) + case "aes-128": + block, _ = kcp.NewAESBlockCrypt(pass[:16]) + case "aes-192": + block, _ = kcp.NewAESBlockCrypt(pass[:24]) + case "blowfish": + block, _ = kcp.NewBlowfishBlockCrypt(pass) + case "twofish": + block, _ = kcp.NewTwofishBlockCrypt(pass) + case "cast5": + block, _ = kcp.NewCast5BlockCrypt(pass[:16]) + case "3des": + block, _ = kcp.NewTripleDESBlockCrypt(pass[:24]) + case "xtea": + block, _ = kcp.NewXTEABlockCrypt(pass[:16]) + case "salsa20": + block, _ = kcp.NewSalsa20BlockCrypt(pass) + default: + config.Crypt = "aes" + block, _ = kcp.NewAESBlockCrypt(pass) + } + return +} diff --git a/transport/kcptun/comp.go b/transport/kcptun/comp.go new file mode 100644 index 0000000000..1efdd42a40 --- /dev/null +++ b/transport/kcptun/comp.go @@ -0,0 +1,63 @@ +package kcptun + +import ( + "net" + "time" + + "github.com/golang/snappy" +) + +// CompStream is a net.Conn wrapper that compresses data using snappy +type CompStream struct { + conn net.Conn + w *snappy.Writer + r *snappy.Reader +} + +func (c *CompStream) Read(p []byte) (n int, err error) { + return c.r.Read(p) +} + +func (c *CompStream) Write(p []byte) (n int, err error) { + if _, err := c.w.Write(p); err != nil { + return 0, err + } + + if err := c.w.Flush(); err != nil { + return 0, err + } + return len(p), err +} + +func (c *CompStream) Close() error { + return c.conn.Close() +} + +func (c *CompStream) LocalAddr() net.Addr { + return c.conn.LocalAddr() +} + +func (c *CompStream) RemoteAddr() net.Addr { + return c.conn.RemoteAddr() +} + +func (c *CompStream) SetDeadline(t time.Time) error { + return c.conn.SetDeadline(t) +} + +func (c *CompStream) SetReadDeadline(t time.Time) error { + return c.conn.SetReadDeadline(t) +} + +func (c *CompStream) SetWriteDeadline(t time.Time) error { + return c.conn.SetWriteDeadline(t) +} + +// NewCompStream creates a new stream that compresses data using snappy +func NewCompStream(conn net.Conn) *CompStream { + c := new(CompStream) + c.conn = conn + c.w = snappy.NewBufferedWriter(conn) + c.r = snappy.NewReader(conn) + return c +} diff --git a/transport/kcptun/doc.go b/transport/kcptun/doc.go new file mode 100644 index 0000000000..e0801d2853 --- /dev/null +++ b/transport/kcptun/doc.go @@ -0,0 +1,5 @@ +// Package kcptun copy and modify from: +// https://github.com/xtaci/kcptun/tree/52492c72592627d0005cbedbc4ba37fc36a95c3f +// adopt for mihomo +// without SM4,QPP,tcpraw support +package kcptun diff --git a/transport/kcptun/server.go b/transport/kcptun/server.go new file mode 100644 index 0000000000..5fcd440c8f --- /dev/null +++ b/transport/kcptun/server.go @@ -0,0 +1,79 @@ +package kcptun + +import ( + "net" + "time" + + "github.com/metacubex/kcp-go" + "github.com/metacubex/smux" +) + +type Server struct { + config Config + block kcp.BlockCrypt +} + +func NewServer(config Config) *Server { + config.FillDefaults() + block := config.NewBlock() + + return &Server{ + config: config, + block: block, + } +} + +func (s *Server) Serve(pc net.PacketConn, handler func(net.Conn)) error { + lis, err := kcp.ServeConn(s.block, s.config.DataShard, s.config.ParityShard, pc) + if err != nil { + return err + } + defer lis.Close() + _ = lis.SetDSCP(s.config.DSCP) + _ = lis.SetReadBuffer(s.config.SockBuf) + _ = lis.SetWriteBuffer(s.config.SockBuf) + for { + conn, err := lis.AcceptKCP() + if err != nil { + return err + } + conn.SetStreamMode(true) + conn.SetWriteDelay(false) + conn.SetNoDelay(s.config.NoDelay, s.config.Interval, s.config.Resend, s.config.NoCongestion) + conn.SetMtu(s.config.MTU) + conn.SetWindowSize(s.config.SndWnd, s.config.RcvWnd) + conn.SetACKNoDelay(s.config.AckNodelay) + + var netConn net.Conn = conn + if !s.config.NoComp { + netConn = NewCompStream(netConn) + } + + go func() { + // stream multiplex + smuxConfig := smux.DefaultConfig() + smuxConfig.Version = s.config.SmuxVer + smuxConfig.MaxReceiveBuffer = s.config.SmuxBuf + smuxConfig.MaxStreamBuffer = s.config.StreamBuf + smuxConfig.KeepAliveInterval = time.Duration(s.config.KeepAlive) * time.Second + if smuxConfig.KeepAliveInterval >= smuxConfig.KeepAliveTimeout { + smuxConfig.KeepAliveTimeout = 3 * smuxConfig.KeepAliveInterval + } + + mux, err := smux.Server(netConn, smuxConfig) + if err != nil { + return + } + defer mux.Close() + + for { + stream, err := mux.AcceptStream() + if err != nil { + return + } + go handler(stream) + } + }() + + } +} diff --git a/transport/simple-obfs/tls.go b/transport/simple-obfs/tls.go index a0cbc35038..e64dde5df0 100644 --- a/transport/simple-obfs/tls.go +++ b/transport/simple-obfs/tls.go @@ -6,9 +6,9 @@ import ( "encoding/binary" "io" "net" - "time" "github.com/metacubex/mihomo/common/pool" + "github.com/metacubex/mihomo/ntp" ) const ( @@ -145,7 +145,7 @@ func makeClientHelloMsg(data []byte, server string) []byte { buf.Write([]byte{0x03, 0x03}) // random with timestamp, sid len, sid - binary.Write(buf, binary.BigEndian, uint32(time.Now().Unix())) + binary.Write(buf, binary.BigEndian, uint32(ntp.Now().Unix())) buf.Write(random) buf.WriteByte(32) buf.Write(sessionID) diff --git a/transport/sing-shadowtls/shadowtls.go b/transport/sing-shadowtls/shadowtls.go index 4f9c3b51b4..501e080f41 100644 --- a/transport/sing-shadowtls/shadowtls.go +++ b/transport/sing-shadowtls/shadowtls.go @@ -26,6 +26,8 @@ type ShadowTLSOption struct { Password string Host string Fingerprint string + Certificate string + PrivateKey string ClientFingerprint string SkipCertVerify bool Version int @@ -33,22 +35,25 @@ type ShadowTLSOption struct { } func NewShadowTLS(ctx context.Context, conn net.Conn, option *ShadowTLSOption) (net.Conn, error) { - tlsConfig := &tls.Config{ - NextProtos: option.ALPN, - MinVersion: tls.VersionTLS12, - InsecureSkipVerify: option.SkipCertVerify, - ServerName: option.Host, + tlsConfig, err := ca.GetTLSConfig(ca.Option{ + TLSConfig: &tls.Config{ + NextProtos: option.ALPN, + MinVersion: tls.VersionTLS12, + InsecureSkipVerify: option.SkipCertVerify, + ServerName: option.Host, + }, + Fingerprint: option.Fingerprint, + Certificate: option.Certificate, + PrivateKey: option.PrivateKey, + }) + if err != nil { + return nil, err } + if option.Version == 1 { tlsConfig.MaxVersion = tls.VersionTLS12 // ShadowTLS v1 only support TLS 1.2 } - var err error - tlsConfig, err = ca.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint) - if err != nil { - return nil, err - } - tlsHandshake := uTLSHandshakeFunc(tlsConfig, option.ClientFingerprint, option.Version) client, err := shadowtls.NewClient(shadowtls.ClientConfig{ Version: option.Version, diff --git a/transport/ssr/obfs/tls1.2_ticket_auth.go b/transport/ssr/obfs/tls1.2_ticket_auth.go index d5955edc34..3106ace651 100644 --- a/transport/ssr/obfs/tls1.2_ticket_auth.go +++ b/transport/ssr/obfs/tls1.2_ticket_auth.go @@ -7,9 +7,9 @@ import ( "encoding/binary" "net" "strings" - "time" "github.com/metacubex/mihomo/common/pool" + "github.com/metacubex/mihomo/ntp" "github.com/metacubex/mihomo/transport/ssr/tools" "github.com/metacubex/randv2" @@ -182,7 +182,7 @@ func packData(buf *bytes.Buffer, data []byte) { } func (t *tls12Ticket) packAuthData(buf *bytes.Buffer) { - binary.Write(buf, binary.BigEndian, uint32(time.Now().Unix())) + binary.Write(buf, binary.BigEndian, uint32(ntp.Now().Unix())) tools.AppendRandBytes(buf, 18) buf.Write(t.hmacSHA1(buf.Bytes()[buf.Len()-22:])[:10]) } diff --git a/transport/ssr/protocol/base.go b/transport/ssr/protocol/base.go index 798701777e..8f03f60971 100644 --- a/transport/ssr/protocol/base.go +++ b/transport/ssr/protocol/base.go @@ -8,10 +8,10 @@ import ( "encoding/base64" "encoding/binary" "sync" - "time" "github.com/metacubex/mihomo/common/pool" "github.com/metacubex/mihomo/log" + "github.com/metacubex/mihomo/ntp" "github.com/metacubex/mihomo/transport/shadowsocks/core" "github.com/metacubex/randv2" @@ -49,7 +49,7 @@ func (a *authData) next() *authData { } func (a *authData) putAuthData(buf *bytes.Buffer) { - binary.Write(buf, binary.LittleEndian, uint32(time.Now().Unix())) + binary.Write(buf, binary.LittleEndian, uint32(ntp.Now().Unix())) buf.Write(a.clientID[:]) binary.Write(buf, binary.LittleEndian, a.connectionID) } @@ -57,7 +57,7 @@ func (a *authData) putAuthData(buf *bytes.Buffer) { func (a *authData) putEncryptedData(b *bytes.Buffer, userKey []byte, paddings [2]int, salt string) error { encrypt := pool.Get(16) defer pool.Put(encrypt) - binary.LittleEndian.PutUint32(encrypt, uint32(time.Now().Unix())) + binary.LittleEndian.PutUint32(encrypt, uint32(ntp.Now().Unix())) copy(encrypt[4:], a.clientID[:]) binary.LittleEndian.PutUint32(encrypt[8:], a.connectionID) binary.LittleEndian.PutUint16(encrypt[12:], uint16(paddings[0])) diff --git a/transport/tuic/server.go b/transport/tuic/server.go index 98c9ee3db1..d9d6439ec1 100644 --- a/transport/tuic/server.go +++ b/transport/tuic/server.go @@ -87,7 +87,11 @@ func (s *serverHandler) handle() { _ = s.handleMessage() }() - <-s.quicConn.HandshakeComplete() + select { + case <-s.quicConn.HandshakeComplete(): // this chan maybe not closed if handshake never complete + case <-time.After(s.quicConn.Config().HandshakeIdleTimeout): // HandshakeIdleTimeout in real conn.Config() never be zero + } + time.AfterFunc(s.AuthenticationTimeout, func() { if s.v4Handler != nil { if s.v4Handler.AuthOk() { diff --git a/transport/v2ray-plugin/websocket.go b/transport/v2ray-plugin/websocket.go index 983698c774..b2e37926e9 100644 --- a/transport/v2ray-plugin/websocket.go +++ b/transport/v2ray-plugin/websocket.go @@ -21,6 +21,8 @@ type Option struct { ECHConfig *ech.Config SkipCertVerify bool Fingerprint string + Certificate string + PrivateKey string Mux bool V2rayHttpUpgrade bool V2rayHttpUpgradeFastOpen bool @@ -43,15 +45,19 @@ func NewV2rayObfs(ctx context.Context, conn net.Conn, option *Option) (net.Conn, Headers: header, } + var err error if option.TLS { config.TLS = true - tlsConfig := &tls.Config{ - ServerName: option.Host, - InsecureSkipVerify: option.SkipCertVerify, - NextProtos: []string{"http/1.1"}, - } - var err error - config.TLSConfig, err = ca.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint) + config.TLSConfig, err = ca.GetTLSConfig(ca.Option{ + TLSConfig: &tls.Config{ + ServerName: option.Host, + InsecureSkipVerify: option.SkipCertVerify, + NextProtos: []string{"http/1.1"}, + }, + Fingerprint: option.Fingerprint, + Certificate: option.Certificate, + PrivateKey: option.PrivateKey, + }) if err != nil { return nil, err } @@ -61,7 +67,6 @@ func NewV2rayObfs(ctx context.Context, conn net.Conn, option *Option) (net.Conn, } } - var err error conn, err = vmess.StreamWebsocketConn(ctx, conn, config) if err != nil { return nil, err diff --git a/transport/vless/addons.go b/transport/vless/addons.go new file mode 100644 index 0000000000..05aa7646ed --- /dev/null +++ b/transport/vless/addons.go @@ -0,0 +1,82 @@ +package vless + +import ( + "bytes" + "encoding/binary" + "fmt" + "io" +) + +func ReadAddons(data []byte) (*Addons, error) { + reader := bytes.NewReader(data) + var addons Addons + for reader.Len() > 0 { + tag, err := binary.ReadUvarint(reader) + if err != nil { + return nil, err + } + number, typ := int32(tag>>3), int8(tag&7) + switch typ { + case 0: // VARINT + _, err = binary.ReadUvarint(reader) + if err != nil { + return nil, err + } + case 5: // I32 + var i32 [4]byte + _, err = io.ReadFull(reader, i32[:]) + if err != nil { + return nil, err + } + case 1: // I64 + var i64 [8]byte + _, err = io.ReadFull(reader, i64[:]) + if err != nil { + return nil, err + } + case 2: // LEN + var bytesLen uint64 + bytesLen, err = binary.ReadUvarint(reader) + if err != nil { + return nil, err + } + bytesData := make([]byte, bytesLen) + _, err = io.ReadFull(reader, bytesData) + if err != nil { + return nil, err + } + switch number { + case 1: + addons.Flow = string(bytesData) + case 2: + addons.Seed = bytesData + } + default: // group (3,4) has been deprecated we unneeded support + return nil, fmt.Errorf("unknown protobuf message tag: %v", tag) + } + } + return &addons, nil +} + +func WriteAddons(addons *Addons) []byte { + var writer bytes.Buffer + if len(addons.Flow) > 0 { + WriteUvarint(&writer, (1<<3)|2) // (field << 3) bit-or wire_type encoded as uint32 varint + WriteUvarint(&writer, uint64(len(addons.Flow))) + writer.WriteString(addons.Flow) + } + if len(addons.Seed) > 0 { + WriteUvarint(&writer, (2<<3)|2) // (field << 3) bit-or wire_type encoded as uint32 varint + WriteUvarint(&writer, uint64(len(addons.Seed))) + writer.Write(addons.Seed) + } + return writer.Bytes() +} + +func WriteUvarint(writer *bytes.Buffer, x uint64) { + for x >= 0x80 { + writer.WriteByte(byte(x) | 0x80) + x >>= 7 + } + writer.WriteByte(byte(x)) +} diff --git a/transport/vless/addons_test.go b/transport/vless/addons_test.go new file mode 100644 index 0000000000..60a27a069a --- /dev/null +++ b/transport/vless/addons_test.go @@ -0,0 +1,95 @@ +package vless + +import ( + "bytes" + "strconv" + "testing" + + "google.golang.org/protobuf/proto" +) + +func TestAddons(t *testing.T) { + var tests = []struct { + flow string + seed []byte + }{ + {XRV, nil}, + {XRS, []byte{1, 2, 3}}, + {"", []byte{1, 2, 3}}, + {"", nil}, + } + + for i, test := range tests { + t.Run(strconv.Itoa(i), func(t *testing.T) { + t.Run("proto->handwritten", func(t *testing.T) { + addons := new(Addons) + addons.Flow = test.flow + addons.Seed = test.seed + + addonsBytes, err := proto.Marshal(addons) + if err != nil { + t.Errorf("error marshalling addons: %v", err) + return + } + addons, err = ReadAddons(addonsBytes) + if err != nil { + t.Errorf("error reading addons: %v", err) + return + } + + if addons.Flow != test.flow { + t.Errorf("got %v; want %v", addons.Flow, test.flow) + return + } + if !bytes.Equal(addons.Seed, test.seed) { + t.Errorf("got %v; want %v", addons.Seed, test.seed) + return + } + }) + + t.Run("handwritten->proto", func(t *testing.T) { + addons := new(Addons) + addons.Flow = test.flow + addons.Seed = test.seed + + addonsBytes := WriteAddons(addons) + err := proto.Unmarshal(addonsBytes, addons) + if err != nil { + t.Errorf("error reading addons: %v", err) + return + } + + if addons.Flow != test.flow { + t.Errorf("got %v; want %v", addons.Flow, test.flow) + return + } + if !bytes.Equal(addons.Seed, test.seed) { + t.Errorf("got %v; want %v", addons.Seed, test.seed) + return + } + }) + + t.Run("handwritten->handwritten", func(t *testing.T) { + addons := new(Addons) + addons.Flow = test.flow + addons.Seed = test.seed + + addonsBytes := WriteAddons(addons) + addons, err := ReadAddons(addonsBytes) + if err != nil { + t.Errorf("error reading addons: %v", err) + return + } + + if addons.Flow != test.flow { + t.Errorf("got %v; want %v", addons.Flow, test.flow) + return + } + if !bytes.Equal(addons.Seed, test.seed) { + t.Errorf("got %v; want %v", addons.Seed, test.seed) + return + } + }) + }) + } +} diff --git a/transport/vless/conn.go b/transport/vless/conn.go index 94ae71eee0..f43d77e180 100644 --- a/transport/vless/conn.go +++ b/transport/vless/conn.go @@ -5,7 +5,6 @@ import ( "errors" "io" "net" - "sync" "github.com/metacubex/mihomo/common/buf" N "github.com/metacubex/mihomo/common/net" @@ -16,17 +15,12 @@ import ( ) type Conn struct { - N.ExtendedWriter - N.ExtendedReader - net.Conn + N.ExtendedConn dst *DstAddr - id *uuid.UUID + id uuid.UUID addons *Addons received bool - - handshakeMutex sync.Mutex - needHandshake bool - err error + sent bool } func (vc *Conn) Read(b []byte) (int, error) { @@ -36,7 +30,7 @@ func (vc *Conn) Read(b []byte) (int, error) { } vc.received = true } - return vc.ExtendedReader.Read(b) + return vc.ExtendedConn.Read(b) } func (vc *Conn) ReadBuffer(buffer *buf.Buffer) error { @@ -46,58 +40,39 @@ func (vc *Conn) ReadBuffer(buffer *buf.Buffer) error { } vc.received = true } - return vc.ExtendedReader.ReadBuffer(buffer) + return vc.ExtendedConn.ReadBuffer(buffer) } func (vc *Conn) Write(p []byte) (int, error) { - if vc.needHandshake { - vc.handshakeMutex.Lock() - if vc.needHandshake { - vc.needHandshake = false - if vc.sendRequest(p) { - vc.handshakeMutex.Unlock() - if vc.err != nil { - return 0, vc.err - } - return len(p), vc.err - } - if vc.err != nil { - vc.handshakeMutex.Unlock() - return 0, vc.err - } + if !vc.sent { + if err := vc.sendRequest(p); err != nil { + return 0, err } - vc.handshakeMutex.Unlock() + vc.sent = true + return len(p), nil } - return vc.ExtendedWriter.Write(p) + return vc.ExtendedConn.Write(p) } func (vc *Conn) WriteBuffer(buffer *buf.Buffer) error { - if vc.needHandshake { - vc.handshakeMutex.Lock() - if vc.needHandshake { - vc.needHandshake = false - if vc.sendRequest(buffer.Bytes()) { - vc.handshakeMutex.Unlock() - return vc.err - } - if vc.err != nil { - vc.handshakeMutex.Unlock() - return vc.err - } + if !vc.sent { + if err := vc.sendRequest(buffer.Bytes()); err != nil { + return err } - vc.handshakeMutex.Unlock() + vc.sent = true + return nil } - return vc.ExtendedWriter.WriteBuffer(buffer) + return vc.ExtendedConn.WriteBuffer(buffer) } -func (vc *Conn) sendRequest(p []byte) bool { +func (vc *Conn) sendRequest(p []byte) (err error) { var addonsBytes []byte if vc.addons != nil { - addonsBytes, vc.err = proto.Marshal(vc.addons) - if vc.err != nil { - return true + addonsBytes, err = proto.Marshal(vc.addons) + if err != nil { + return } } @@ -141,15 +116,15 @@ func (vc *Conn) sendRequest(p []byte) bool { buf.Must(buf.Error(buffer.Write(p))) - _, vc.err = vc.ExtendedWriter.Write(buffer.Bytes()) - return true + _, err = vc.ExtendedConn.Write(buffer.Bytes()) + return } -func (vc *Conn) recvResponse() error { +func (vc *Conn) recvResponse() (err error) { var buffer [2]byte - _, vc.err = io.ReadFull(vc.ExtendedReader, buffer[:]) - if vc.err != nil { - return vc.err + _, err = io.ReadFull(vc.ExtendedConn, buffer[:]) + if err != nil { + return err } if buffer[0] != Version { @@ -158,29 +133,35 @@ func (vc *Conn) recvResponse() error { length := int64(buffer[1]) if length != 0 { // addon data length > 0 - io.CopyN(io.Discard, vc.ExtendedReader, length) // just discard + io.CopyN(io.Discard, vc.ExtendedConn, length) // just discard } - return nil + return } func (vc *Conn) Upstream() any { - return vc.Conn + return vc.ExtendedConn +} + +func (vc *Conn) ReaderReplaceable() bool { + return vc.received +} + +func (vc *Conn) WriterReplaceable() bool { + return vc.sent } func (vc *Conn) NeedHandshake() bool { - return vc.needHandshake + return !vc.sent } // newConn return a Conn instance func newConn(conn net.Conn, client *Client, dst *DstAddr) (net.Conn, error) { c := &Conn{ - ExtendedReader: N.NewExtendedReader(conn), - ExtendedWriter: N.NewExtendedWriter(conn), - Conn: conn, - id: client.uuid, - dst: dst, - needHandshake: true, + ExtendedConn: N.NewExtendedConn(conn), + id: client.uuid, + addons: client.Addons, + dst: dst, } if client.Addons != nil { @@ -190,7 +171,6 @@ func newConn(conn net.Conn, client *Client, dst *DstAddr) (net.Conn, error) { if err != nil { return nil, err } - c.addons = client.Addons return visionConn, nil } } diff --git a/transport/vless/encryption/client.go b/transport/vless/encryption/client.go index 465f16260f..bcfce08ed8 100644 --- a/transport/vless/encryption/client.go +++ b/transport/vless/encryption/client.go @@ -11,6 +11,7 @@ import ( "time" "github.com/metacubex/blake3" + utls "github.com/metacubex/utls" "github.com/metacubex/utls/mlkem" ) @@ -21,6 +22,8 @@ type ClientInstance struct { RelaysLength int XorMode uint32 Seconds uint32 + PaddingLens [][3]int + PaddingGaps [][3]int RWLock sync.RWMutex Expire time.Time @@ -28,15 +31,13 @@ type ClientInstance struct { Ticket []byte } -func (i *ClientInstance) Init(nfsPKeysBytes [][]byte, xorMode, seconds uint32) (err error) { +func (i *ClientInstance) Init(nfsPKeysBytes [][]byte, xorMode, seconds uint32, padding string) (err error) { if i.NfsPKeys != nil { - err = errors.New("already initialized") - return + return errors.New("already initialized") } l := len(nfsPKeysBytes) if l == 0 { - err = errors.New("empty nfsPKeysBytes") - return + return errors.New("empty nfsPKeysBytes") } i.NfsPKeys = make([]any, l) i.NfsPKeysBytes = nfsPKeysBytes @@ -58,18 +59,18 @@ func (i *ClientInstance) Init(nfsPKeysBytes [][]byte, xorMode, seconds uint32) ( i.RelaysLength -= 32 i.XorMode = xorMode i.Seconds = seconds - return + return ParsePadding(padding, &i.PaddingLens, &i.PaddingGaps) } func (i *ClientInstance) Handshake(conn net.Conn) (*CommonConn, error) { if i.NfsPKeys == nil { return nil, errors.New("uninitialized") } - c := &CommonConn{Conn: conn} + c := NewCommonConn(conn, utls.HasAESGCMHardwareSupport()) ivAndRealysLength := 16 + i.RelaysLength pfsKeyExchangeLength := 18 + 1184 + 32 + 16 - paddingLength := int(randBetween(100, 1000)) + paddingLength, paddingLens, paddingGaps := CreatPadding(i.PaddingLens, i.PaddingGaps) clientHello := make([]byte, ivAndRealysLength+pfsKeyExchangeLength+paddingLength) iv := clientHello[:16] @@ -107,18 +108,18 @@ func (i *ClientInstance) Handshake(conn net.Conn) (*CommonConn, error) { lastCTR.XORKeyStream(relays[index:], i.Hash32s[j+1][:]) relays = relays[index+32:] } - nfsGCM := NewGCM(iv, nfsKey) + nfsAEAD := NewAEAD(iv, nfsKey, c.UseAES) if i.Seconds > 0 { i.RWLock.RLock() if time.Now().Before(i.Expire) { c.Client = i c.UnitedKey = append(i.PfsKey, nfsKey...) // different unitedKey for each connection - nfsGCM.Seal(clientHello[:ivAndRealysLength], nil, EncodeLength(32), nil) - nfsGCM.Seal(clientHello[:ivAndRealysLength+18], nil, i.Ticket, nil) + nfsAEAD.Seal(clientHello[:ivAndRealysLength], nil, EncodeLength(32), nil) + nfsAEAD.Seal(clientHello[:ivAndRealysLength+18], nil, i.Ticket, nil) i.RWLock.RUnlock() c.PreWrite = clientHello[:ivAndRealysLength+18+32] - c.GCM = NewGCM(clientHello[ivAndRealysLength+18:ivAndRealysLength+18+32], c.UnitedKey) + c.AEAD = NewAEAD(clientHello[ivAndRealysLength+18:ivAndRealysLength+18+32], c.UnitedKey, c.UseAES) if i.XorMode == 2 { c.Conn = NewXorConn(conn, NewCTR(c.UnitedKey, iv), nil, len(c.PreWrite), 16) } @@ -128,26 +129,34 @@ func (i *ClientInstance) Handshake(conn net.Conn) (*CommonConn, error) { } pfsKeyExchange := clientHello[ivAndRealysLength : ivAndRealysLength+pfsKeyExchangeLength] - nfsGCM.Seal(pfsKeyExchange[:0], nil, EncodeLength(pfsKeyExchangeLength-18), nil) + nfsAEAD.Seal(pfsKeyExchange[:0], nil, EncodeLength(pfsKeyExchangeLength-18), nil) mlkem768DKey, _ := mlkem.GenerateKey768() x25519SKey, _ := ecdh.X25519().GenerateKey(rand.Reader) pfsPublicKey := append(mlkem768DKey.EncapsulationKey().Bytes(), x25519SKey.PublicKey().Bytes()...) - nfsGCM.Seal(pfsKeyExchange[:18], nil, pfsPublicKey, nil) + nfsAEAD.Seal(pfsKeyExchange[:18], nil, pfsPublicKey, nil) padding := clientHello[ivAndRealysLength+pfsKeyExchangeLength:] - nfsGCM.Seal(padding[:0], nil, EncodeLength(paddingLength-18), nil) - nfsGCM.Seal(padding[:18], nil, padding[18:paddingLength-16], nil) + nfsAEAD.Seal(padding[:0], nil, EncodeLength(paddingLength-18), nil) + nfsAEAD.Seal(padding[:18], nil, padding[18:paddingLength-16], nil) - if _, err := conn.Write(clientHello); err != nil { - return nil, err + paddingLens[0] = ivAndRealysLength + pfsKeyExchangeLength + paddingLens[0] + for i, l := range paddingLens { // sends padding in a fragmented way, to create variable traffic pattern, before inner VLESS flow takes control + if l > 0 { + if _, err := conn.Write(clientHello[:l]); err != nil { + return nil, err + } + clientHello = clientHello[l:] + } + if len(paddingGaps) > i { + time.Sleep(paddingGaps[i]) + } } - // padding can be sent in a fragmented way, to create variable traffic pattern, before inner VLESS flow takes control encryptedPfsPublicKey := make([]byte, 1088+32+16) if _, err := io.ReadFull(conn, encryptedPfsPublicKey); err != nil { return nil, err } - nfsGCM.Open(encryptedPfsPublicKey[:0], MaxNonce, encryptedPfsPublicKey, nil) + nfsAEAD.Open(encryptedPfsPublicKey[:0], MaxNonce, encryptedPfsPublicKey, nil) mlkem768Key, err := mlkem768DKey.Decapsulate(encryptedPfsPublicKey[:1088]) if err != nil { return nil, err @@ -164,14 +173,14 @@ func (i *ClientInstance) Handshake(conn net.Conn) (*CommonConn, error) { copy(pfsKey, mlkem768Key) copy(pfsKey[32:], x25519Key) c.UnitedKey = append(pfsKey, nfsKey...) - c.GCM = NewGCM(pfsPublicKey, c.UnitedKey) - c.PeerGCM = NewGCM(encryptedPfsPublicKey[:1088+32], c.UnitedKey) + c.AEAD = NewAEAD(pfsPublicKey, c.UnitedKey, c.UseAES) + c.PeerAEAD = NewAEAD(encryptedPfsPublicKey[:1088+32], c.UnitedKey, c.UseAES) encryptedTicket := make([]byte, 32) if _, err := io.ReadFull(conn, encryptedTicket); err != nil { return nil, err } - if _, err := c.PeerGCM.Open(encryptedTicket[:0], nil, encryptedTicket, nil); err != nil { + if _, err := c.PeerAEAD.Open(encryptedTicket[:0], nil, encryptedTicket, nil); err != nil { return nil, err } seconds := DecodeLength(encryptedTicket) @@ -188,7 +197,7 @@ func (i *ClientInstance) Handshake(conn net.Conn) (*CommonConn, error) { if _, err := io.ReadFull(conn, encryptedLength); err != nil { return nil, err } - if _, err := c.PeerGCM.Open(encryptedLength[:0], nil, encryptedLength, nil); err != nil { + if _, err := c.PeerAEAD.Open(encryptedLength[:0], nil, encryptedLength, nil); err != nil { return nil, err } length := DecodeLength(encryptedLength[:2]) diff --git a/transport/vless/encryption/client_test.go b/transport/vless/encryption/client_test.go new file mode 100644 index 0000000000..793d01919e --- /dev/null +++ b/transport/vless/encryption/client_test.go @@ -0,0 +1,21 @@ +package encryption + +import ( + "fmt" + "runtime" + "testing" + + utls "github.com/metacubex/utls" +) + +func TestHasAESGCMHardwareSupport(t *testing.T) { + fmt.Println("HasAESGCMHardwareSupport:", utls.HasAESGCMHardwareSupport()) + + if runtime.GOARCH == "arm64" && runtime.GOOS == "darwin" { + // It should be supported starting from Apple Silicon M1 + // https://github.com/golang/go/blob/go1.25.0/src/internal/cpu/cpu_arm64_darwin.go#L26-L30 + if !utls.HasAESGCMHardwareSupport() { + t.Errorf("For ARM64 Darwin platforms (excluding iOS), AES GCM hardware acceleration should always be available.") + } + } +} diff --git a/transport/vless/encryption/common.go b/transport/vless/encryption/common.go index 1b2aa18201..8a71f7193b 100644 --- a/transport/vless/encryption/common.go +++ b/transport/vless/encryption/common.go @@ -4,55 +4,68 @@ import ( "bytes" "crypto/aes" "crypto/cipher" - "crypto/rand" "errors" "fmt" "io" - "math/big" "net" + "strconv" "strings" "time" + "github.com/metacubex/mihomo/common/pool" + "github.com/metacubex/blake3" + "github.com/metacubex/randv2" + "golang.org/x/crypto/chacha20poly1305" ) type CommonConn struct { net.Conn + UseAES bool Client *ClientInstance UnitedKey []byte PreWrite []byte - GCM *GCM + AEAD *AEAD + PeerAEAD *AEAD PeerPadding []byte - PeerGCM *GCM + rawInput bytes.Buffer // PeerInBytes input bytes.Reader // PeerCache } +func NewCommonConn(conn net.Conn, useAES bool) *CommonConn { + return &CommonConn{ + Conn: conn, + UseAES: useAES, + } +} + func (c *CommonConn) Write(b []byte) (int, error) { if len(b) == 0 { return 0, nil } - var data []byte + outBytes := pool.Get(5 + 8192 + 16) + defer pool.Put(outBytes) for n := 0; n < len(b); { b := b[n:] if len(b) > 8192 { b = b[:8192] // for avoiding another copy() in peer's Read() } n += len(b) - data = make([]byte, 5+len(b)+16) - EncodeHeader(data, len(b)+16) + headerAndData := outBytes[:5+len(b)+16] + EncodeHeader(headerAndData, len(b)+16) max := false - if bytes.Equal(c.GCM.Nonce[:], MaxNonce) { + if bytes.Equal(c.AEAD.Nonce[:], MaxNonce) { max = true } - c.GCM.Seal(data[:5], nil, b, data[:5]) + c.AEAD.Seal(headerAndData[:5], nil, b, headerAndData[:5]) if max { - c.GCM = NewGCM(data[5:], c.UnitedKey) + c.AEAD = NewAEAD(headerAndData, c.UnitedKey, c.UseAES) } if c.PreWrite != nil { - data = append(c.PreWrite, data...) + headerAndData = append(c.PreWrite, headerAndData...) c.PreWrite = nil } - if _, err := c.Conn.Write(data); err != nil { + if _, err := c.Conn.Write(headerAndData); err != nil { return 0, err } } @@ -63,12 +76,12 @@ func (c *CommonConn) Read(b []byte) (int, error) { if len(b) == 0 { return 0, nil } - if c.PeerGCM == nil { // client's 0-RTT + if c.PeerAEAD == nil { // client's 0-RTT serverRandom := make([]byte, 16) if _, err := io.ReadFull(c.Conn, serverRandom); err != nil { return 0, err } - c.PeerGCM = NewGCM(serverRandom, c.UnitedKey) + c.PeerAEAD = NewAEAD(serverRandom, c.UnitedKey, c.UseAES) if xorConn, ok := c.Conn.(*XorConn); ok { xorConn.PeerCTR = NewCTR(c.UnitedKey, serverRandom) } @@ -77,7 +90,7 @@ func (c *CommonConn) Read(b []byte) (int, error) { if _, err := io.ReadFull(c.Conn, c.PeerPadding); err != nil { return 0, err } - if _, err := c.PeerGCM.Open(c.PeerPadding[:0], nil, c.PeerPadding, nil); err != nil { + if _, err := c.PeerAEAD.Open(c.PeerPadding[:0], nil, c.PeerPadding, nil); err != nil { return 0, err } c.PeerPadding = nil @@ -85,9 +98,13 @@ func (c *CommonConn) Read(b []byte) (int, error) { if c.input.Len() > 0 { return c.input.Read(b) } - h, l, err := ReadAndDecodeHeader(c.Conn) // l: 17~17000 + peerHeader := [5]byte{} + if _, err := io.ReadFull(c.Conn, peerHeader[:]); err != nil { + return 0, err + } + l, err := DecodeHeader(peerHeader[:]) // l: 17~17000 if err != nil { - if c.Client != nil && strings.HasPrefix(err.Error(), "invalid header: ") { // client's 0-RTT + if c.Client != nil && errors.Is(err, ErrInvalidHeader) { // client's 0-RTT c.Client.RWLock.Lock() if bytes.HasPrefix(c.UnitedKey, c.Client.PfsKey) { c.Client.Expire = time.Now() // expired @@ -98,7 +115,10 @@ func (c *CommonConn) Read(b []byte) (int, error) { return 0, err } c.Client = nil - peerData := make([]byte, l) + if c.rawInput.Cap() < l { + c.rawInput.Grow(l) // no need to use sync.Pool, because we are always reading + } + peerData := c.rawInput.Bytes()[:l] if _, err := io.ReadFull(c.Conn, peerData); err != nil { return 0, err } @@ -106,13 +126,13 @@ func (c *CommonConn) Read(b []byte) (int, error) { if len(dst) <= len(b) { dst = b[:len(dst)] // avoids another copy() } - var newGCM *GCM - if bytes.Equal(c.PeerGCM.Nonce[:], MaxNonce) { - newGCM = NewGCM(peerData, c.UnitedKey) + var newAEAD *AEAD + if bytes.Equal(c.PeerAEAD.Nonce[:], MaxNonce) { + newAEAD = NewAEAD(append(peerHeader[:], peerData...), c.UnitedKey, c.UseAES) } - _, err = c.PeerGCM.Open(dst[:0], nil, peerData, h) - if newGCM != nil { - c.PeerGCM = newGCM + _, err = c.PeerAEAD.Open(dst[:0], nil, peerData, peerHeader[:]) + if newAEAD != nil { + c.PeerAEAD = newAEAD } if err != nil { return 0, err @@ -124,28 +144,32 @@ func (c *CommonConn) Read(b []byte) (int, error) { return len(dst), nil } -type GCM struct { +type AEAD struct { cipher.AEAD Nonce [12]byte } -func NewGCM(ctx, key []byte) *GCM { +func NewAEAD(ctx, key []byte, useAES bool) *AEAD { k := make([]byte, 32) blake3.DeriveKey(k, string(ctx), key) - block, _ := aes.NewCipher(k) - aead, _ := cipher.NewGCM(block) - return &GCM{AEAD: aead} - //chacha20poly1305.New() + var aead cipher.AEAD + if useAES { + block, _ := aes.NewCipher(k) + aead, _ = cipher.NewGCM(block) + } else { + aead, _ = chacha20poly1305.New(k) + } + return &AEAD{AEAD: aead} } -func (a *GCM) Seal(dst, nonce, plaintext, additionalData []byte) []byte { +func (a *AEAD) Seal(dst, nonce, plaintext, additionalData []byte) []byte { if nonce == nil { nonce = IncreaseNonce(a.Nonce[:]) } return a.AEAD.Seal(dst, nonce, plaintext, additionalData) } -func (a *GCM) Open(dst, nonce, ciphertext, additionalData []byte) ([]byte, error) { +func (a *AEAD) Open(dst, nonce, ciphertext, additionalData []byte) ([]byte, error) { if nonce == nil { nonce = IncreaseNonce(a.Nonce[:]) } @@ -180,41 +204,93 @@ func EncodeHeader(h []byte, l int) { h[4] = byte(l) } +var ErrInvalidHeader = errors.New("invalid header") + func DecodeHeader(h []byte) (l int, err error) { l = int(h[3])<<8 | int(h[4]) if h[0] != 23 || h[1] != 3 || h[2] != 3 { l = 0 } if l < 17 || l > 17000 { // TODO: TLSv1.3 max length - err = fmt.Errorf("invalid header: %v", h[:5]) // DO NOT CHANGE: relied by client's Read() + err = fmt.Errorf("%w: %v", ErrInvalidHeader, h[:5]) // DO NOT CHANGE: relied by client's Read() } return } -func ReadAndDecodeHeader(conn net.Conn) (h []byte, l int, err error) { - h = make([]byte, 5) - if _, err = io.ReadFull(conn, h); err != nil { +func ParsePadding(padding string, paddingLens, paddingGaps *[][3]int) (err error) { + if padding == "" { return } - l, err = DecodeHeader(h) + maxLen := 0 + for i, s := range strings.Split(padding, ".") { + x := strings.Split(s, "-") + if len(x) < 3 || x[0] == "" || x[1] == "" || x[2] == "" { + return errors.New("invalid padding lenth/gap parameter: " + s) + } + y := [3]int{} + if y[0], err = strconv.Atoi(x[0]); err != nil { + return + } + if y[1], err = strconv.Atoi(x[1]); err != nil { + return + } + if y[2], err = strconv.Atoi(x[2]); err != nil { + return + } + if i == 0 && (y[0] < 100 || y[1] < 18+17 || y[2] < 18+17) { + return errors.New("first padding length must not be smaller than 35") + } + if i%2 == 0 { + *paddingLens = append(*paddingLens, y) + maxLen += max(y[1], y[2]) + } else { + *paddingGaps = append(*paddingGaps, y) + } + } + if maxLen > 18+65535 { + return errors.New("total padding length must not be larger than 65553") + } return } -func ReadAndDiscardPaddings(conn net.Conn) (h []byte, l int, err error) { - for { - if h, l, err = ReadAndDecodeHeader(conn); err != nil { - return +func CreatPadding(paddingLens, paddingGaps [][3]int) (length int, lens []int, gaps []time.Duration) { + if len(paddingLens) == 0 { + paddingLens = [][3]int{{100, 111, 1111}, {50, 0, 3333}} + paddingGaps = [][3]int{{75, 0, 111}} + } + for _, y := range paddingLens { + l := 0 + if y[0] >= int(randBetween(0, 100)) { + l = int(randBetween(int64(y[1]), int64(y[2]))) } - if _, err = io.ReadFull(conn, make([]byte, l)); err != nil { - return + lens = append(lens, l) + length += l + } + for _, y := range paddingGaps { + g := 0 + if y[0] >= int(randBetween(0, 100)) { + g = int(randBetween(int64(y[1]), int64(y[2]))) } + gaps = append(gaps, time.Duration(g)*time.Millisecond) } + return +} + +func max[T ~int | ~uint | ~int64 | ~uint64 | ~int32 | ~uint32 | ~int16 | ~uint16 | ~int8 | ~uint8](a, b T) T { + if a > b { + return a + } + return b } func randBetween(from int64, to int64) int64 { if from == to { return from } - bigInt, _ := rand.Int(rand.Reader, big.NewInt(to-from)) - return from + bigInt.Int64() + + if to < from { + from, to = to, from + } + + return from + randv2.Int64N(to-from) } diff --git a/transport/vless/encryption/doc.go b/transport/vless/encryption/doc.go index 177839c64a..32a0023f82 100644 --- a/transport/vless/encryption/doc.go +++ b/transport/vless/encryption/doc.go @@ -20,4 +20,11 @@ // https://github.com/XTLS/Xray-core/commit/ad7140641c44239c9dcdc3d7215ea639b1f0841c // https://github.com/XTLS/Xray-core/commit/0199dea39988a1a1b846d0bf8598631bade40902 // https://github.com/XTLS/Xray-core/commit/fce1195b60f48ca18a953dbd5c7d991869de9a5e +// https://github.com/XTLS/Xray-core/commit/b0b220985c9c1bc832665458d5fd6e0c287b67ae +// https://github.com/XTLS/Xray-core/commit/82ea7a3cc5ff23280b87e3052f0f83b04f0267fa +// https://github.com/XTLS/Xray-core/commit/e8b02cd6649f14889841e8ab8ee6b2acca71dbe6 +// https://github.com/XTLS/Xray-core/commit/6768a22f676c9121cfc9dc4f51181a8a07837c8d +// https://github.com/XTLS/Xray-core/commit/4c6fd94d97159f5a3e740ba6dd2d9b65e3ed320c +// https://github.com/XTLS/Xray-core/commit/19f890729656bc923ae3dee8426168c93b8ee9c2 +// https://github.com/XTLS/Xray-core/commit/cbade89ab11af26ba1e480a3688a6c205fa3c3f8 package encryption diff --git a/transport/vless/encryption/factory.go b/transport/vless/encryption/factory.go index c344fbf1b1..951bf3b118 100644 --- a/transport/vless/encryption/factory.go +++ b/transport/vless/encryption/factory.go @@ -34,7 +34,12 @@ func NewClient(encryption string) (*ClientInstance, error) { return nil, fmt.Errorf("invaild vless encryption value: %s", encryption) } var nfsPKeysBytes [][]byte + var paddings []string for _, r := range s[3:] { + if len(r) < 20 { + paddings = append(paddings, r) + continue + } b, err := base64.RawURLEncoding.DecodeString(r) if err != nil { return nil, fmt.Errorf("invaild vless encryption value: %s", encryption) @@ -44,8 +49,9 @@ func NewClient(encryption string) (*ClientInstance, error) { } nfsPKeysBytes = append(nfsPKeysBytes, b) } + padding := strings.Join(paddings, ".") client := &ClientInstance{} - if err := client.Init(nfsPKeysBytes, xorMode, seconds); err != nil { + if err := client.Init(nfsPKeysBytes, xorMode, seconds, padding); err != nil { return nil, fmt.Errorf("failed to use encryption: %w", err) } return client, nil @@ -71,20 +77,27 @@ func NewServer(decryption string) (*ServerInstance, error) { default: return nil, fmt.Errorf("invaild vless decryption value: %s", decryption) } - var seconds uint32 - if s[2] != "1rtt" { - t := strings.TrimSuffix(s[2], "s") - if t == s[0] { - return nil, fmt.Errorf("invaild vless decryption value: %s", decryption) - } - i, err := strconv.Atoi(t) + t := strings.SplitN(strings.TrimSuffix(s[2], "s"), "-", 2) + i, err := strconv.Atoi(t[0]) + if err != nil { + return nil, fmt.Errorf("invaild vless decryption value: %s", decryption) + } + secondsFrom := int64(i) + secondsTo := int64(0) + if len(t) == 2 { + i, err = strconv.Atoi(t[1]) if err != nil { return nil, fmt.Errorf("invaild vless decryption value: %s", decryption) } - seconds = uint32(i) + secondsTo = int64(i) } var nfsSKeysBytes [][]byte + var paddings []string for _, r := range s[3:] { + if len(r) < 20 { + paddings = append(paddings, r) + continue + } b, err := base64.RawURLEncoding.DecodeString(r) if err != nil { return nil, fmt.Errorf("invaild vless decryption value: %s", decryption) @@ -94,8 +107,9 @@ func NewServer(decryption string) (*ServerInstance, error) { } nfsSKeysBytes = append(nfsSKeysBytes, b) } + padding := strings.Join(paddings, ".") server := &ServerInstance{} - if err := server.Init(nfsSKeysBytes, xorMode, seconds); err != nil { + if err := server.Init(nfsSKeysBytes, xorMode, secondsFrom, secondsTo, padding); err != nil { return nil, fmt.Errorf("failed to use decryption: %w", err) } return server, nil diff --git a/transport/vless/encryption/server.go b/transport/vless/encryption/server.go index 36ce399ab5..7118569888 100644 --- a/transport/vless/encryption/server.go +++ b/transport/vless/encryption/server.go @@ -17,7 +17,6 @@ import ( ) type ServerSession struct { - Expire time.Time PfsKey []byte NfsKeys sync.Map } @@ -28,22 +27,25 @@ type ServerInstance struct { Hash32s [][32]byte RelaysLength int XorMode uint32 - Seconds uint32 + SecondsFrom int64 + SecondsTo int64 + PaddingLens [][3]int + PaddingGaps [][3]int RWLock sync.RWMutex - Sessions map[[16]byte]*ServerSession Closed bool + Lasts map[int64][16]byte + Tickets [][16]byte + Sessions map[[16]byte]*ServerSession } -func (i *ServerInstance) Init(nfsSKeysBytes [][]byte, xorMode, seconds uint32) (err error) { +func (i *ServerInstance) Init(nfsSKeysBytes [][]byte, xorMode uint32, secondsFrom, secondsTo int64, padding string) (err error) { if i.NfsSKeys != nil { - err = errors.New("already initialized") - return + return errors.New("already initialized") } l := len(nfsSKeysBytes) if l == 0 { - err = errors.New("empty nfsSKeysBytes") - return + return errors.New("empty nfsSKeysBytes") } i.NfsSKeys = make([]any, l) i.NfsPKeysBytes = make([][]byte, l) @@ -66,8 +68,15 @@ func (i *ServerInstance) Init(nfsSKeysBytes [][]byte, xorMode, seconds uint32) ( } i.RelaysLength -= 32 i.XorMode = xorMode - if seconds > 0 { - i.Seconds = seconds + i.SecondsFrom = secondsFrom + i.SecondsTo = secondsTo + err = ParsePadding(padding, &i.PaddingLens, &i.PaddingGaps) + if err != nil { + return + } + if i.SecondsFrom > 0 || i.SecondsTo > 0 { + i.Lasts = make(map[int64][16]byte) + i.Tickets = make([][16]byte, 0, 1024) i.Sessions = make(map[[16]byte]*ServerSession) go func() { for { @@ -77,10 +86,17 @@ func (i *ServerInstance) Init(nfsSKeysBytes [][]byte, xorMode, seconds uint32) ( i.RWLock.Unlock() return } - now := time.Now() - for ticket, session := range i.Sessions { - if now.After(session.Expire) { + minute := time.Now().Unix() / 60 + last := i.Lasts[minute] + delete(i.Lasts, minute) + delete(i.Lasts, minute-1) // for insurance + if last != [16]byte{} { + for j, ticket := range i.Tickets { delete(i.Sessions, ticket) + if ticket == last { + i.Tickets = i.Tickets[j+1:] + break + } } } i.RWLock.Unlock() @@ -97,16 +113,19 @@ func (i *ServerInstance) Close() (err error) { return } -func (i *ServerInstance) Handshake(conn net.Conn) (*CommonConn, error) { +func (i *ServerInstance) Handshake(conn net.Conn, fallback *[]byte) (*CommonConn, error) { if i.NfsSKeys == nil { return nil, errors.New("uninitialized") } - c := &CommonConn{Conn: conn} + c := NewCommonConn(conn, true) ivAndRelays := make([]byte, 16+i.RelaysLength) if _, err := io.ReadFull(conn, ivAndRelays); err != nil { return nil, err } + if fallback != nil { + *fallback = append(*fallback, ivAndRelays...) + } iv := ivAndRelays[:16] relays := ivAndRelays[16:] var nfsKey []byte @@ -120,13 +139,16 @@ func (i *ServerInstance) Handshake(conn net.Conn) (*CommonConn, error) { index = 1088 } if i.XorMode > 0 { - NewCTR(i.NfsPKeysBytes[j], iv).XORKeyStream(relays, relays[:index]) // we don't use buggy elligator, because we have PSK :) + NewCTR(i.NfsPKeysBytes[j], iv).XORKeyStream(relays, relays[:index]) // we don't use buggy elligator2, because we have PSK :) } if k, ok := k.(*ecdh.PrivateKey); ok { publicKey, err := ecdh.X25519().NewPublicKey(relays[:index]) if err != nil { return nil, err } + if publicKey.Bytes()[31] > 127 { // we just don't want the observer can change even one bit without breaking the connection, though it has nothing to do with security + return nil, errors.New("the highest bit of the last byte of the peer-sent X25519 public key is not 0") + } nfsKey, err = k.ECDH(publicKey) if err != nil { return nil, err @@ -150,26 +172,37 @@ func (i *ServerInstance) Handshake(conn net.Conn) (*CommonConn, error) { } relays = relays[32:] } - nfsGCM := NewGCM(iv, nfsKey) + nfsAEAD := NewAEAD(iv, nfsKey, c.UseAES) encryptedLength := make([]byte, 18) if _, err := io.ReadFull(conn, encryptedLength); err != nil { return nil, err } - if _, err := nfsGCM.Open(encryptedLength[:0], nil, encryptedLength, nil); err != nil { - return nil, err + if fallback != nil { + *fallback = append(*fallback, encryptedLength...) + } + decryptedLength := make([]byte, 2) + if _, err := nfsAEAD.Open(decryptedLength[:0], nil, encryptedLength, nil); err != nil { + c.UseAES = !c.UseAES + nfsAEAD = NewAEAD(iv, nfsKey, c.UseAES) + if _, err := nfsAEAD.Open(decryptedLength[:0], nil, encryptedLength, nil); err != nil { + return nil, err + } + } + if fallback != nil { + *fallback = nil } - length := DecodeLength(encryptedLength[:2]) + length := DecodeLength(decryptedLength) if length == 32 { - if i.Seconds == 0 { + if i.SecondsFrom == 0 && i.SecondsTo == 0 { return nil, errors.New("0-RTT is not allowed") } encryptedTicket := make([]byte, 32) if _, err := io.ReadFull(conn, encryptedTicket); err != nil { return nil, err } - ticket, err := nfsGCM.Open(nil, nil, encryptedTicket, nil) + ticket, err := nfsAEAD.Open(nil, nil, encryptedTicket, nil) if err != nil { return nil, err } @@ -177,7 +210,7 @@ func (i *ServerInstance) Handshake(conn net.Conn) (*CommonConn, error) { s := i.Sessions[[16]byte(ticket)] i.RWLock.RUnlock() if s == nil { - noises := make([]byte, randBetween(1268, 2268)) // matches 1-RTT's server hello length for "random", though it is not important, just for example + noises := make([]byte, randBetween(1279, 2279)) // matches 1-RTT's server hello length for "random", though it is not important, just for example var err error for err == nil { rand.Read(noises) @@ -192,8 +225,8 @@ func (i *ServerInstance) Handshake(conn net.Conn) (*CommonConn, error) { c.UnitedKey = append(s.PfsKey, nfsKey...) // the same nfsKey links the upload & download (prevents server -> client's another request) c.PreWrite = make([]byte, 16) rand.Read(c.PreWrite) // always trust yourself, not the client (also prevents being parsed as TLS thus causing false interruption for "native" and "xorpub") - c.GCM = NewGCM(c.PreWrite, c.UnitedKey) - c.PeerGCM = NewGCM(encryptedTicket, c.UnitedKey) // unchangeable ctx (prevents server -> server), and different ctx length for upload / download (prevents client -> client) + c.AEAD = NewAEAD(c.PreWrite, c.UnitedKey, c.UseAES) + c.PeerAEAD = NewAEAD(encryptedTicket, c.UnitedKey, c.UseAES) // unchangeable ctx (prevents server -> server), and different ctx length for upload / download (prevents client -> client) if i.XorMode == 2 { c.Conn = NewXorConn(conn, NewCTR(c.UnitedKey, c.PreWrite), NewCTR(c.UnitedKey, iv), 16, 0) // it doesn't matter if the attacker sends client's iv back to the client } @@ -207,7 +240,7 @@ func (i *ServerInstance) Handshake(conn net.Conn) (*CommonConn, error) { if _, err := io.ReadFull(conn, encryptedPfsPublicKey); err != nil { return nil, err } - if _, err := nfsGCM.Open(encryptedPfsPublicKey[:0], nil, encryptedPfsPublicKey, nil); err != nil { + if _, err := nfsAEAD.Open(encryptedPfsPublicKey[:0], nil, encryptedPfsPublicKey, nil); err != nil { return nil, err } mlkem768EKey, err := mlkem.NewEncapsulationKey768(encryptedPfsPublicKey[:1184]) @@ -229,53 +262,66 @@ func (i *ServerInstance) Handshake(conn net.Conn) (*CommonConn, error) { copy(pfsKey[32:], x25519Key) pfsPublicKey := append(encapsulatedPfsKey, x25519SKey.PublicKey().Bytes()...) c.UnitedKey = append(pfsKey, nfsKey...) - c.GCM = NewGCM(pfsPublicKey, c.UnitedKey) - c.PeerGCM = NewGCM(encryptedPfsPublicKey[:1184+32], c.UnitedKey) - ticket := make([]byte, 16) - rand.Read(ticket) - copy(ticket, EncodeLength(int(i.Seconds*4/5))) + c.AEAD = NewAEAD(pfsPublicKey, c.UnitedKey, c.UseAES) + c.PeerAEAD = NewAEAD(encryptedPfsPublicKey[:1184+32], c.UnitedKey, c.UseAES) + + ticket := [16]byte{} + rand.Read(ticket[:]) + var seconds int64 + if i.SecondsTo == 0 { + seconds = i.SecondsFrom * randBetween(50, 100) / 100 + } else { + seconds = randBetween(i.SecondsFrom, i.SecondsTo) + } + copy(ticket[:], EncodeLength(int(seconds))) + if seconds > 0 { + i.RWLock.Lock() + i.Lasts[(time.Now().Unix()+max(i.SecondsFrom, i.SecondsTo))/60+2] = ticket + i.Tickets = append(i.Tickets, ticket) + i.Sessions[ticket] = &ServerSession{PfsKey: pfsKey} + i.RWLock.Unlock() + } pfsKeyExchangeLength := 1088 + 32 + 16 encryptedTicketLength := 32 - paddingLength := int(randBetween(100, 1000)) + paddingLength, paddingLens, paddingGaps := CreatPadding(i.PaddingLens, i.PaddingGaps) serverHello := make([]byte, pfsKeyExchangeLength+encryptedTicketLength+paddingLength) - nfsGCM.Seal(serverHello[:0], MaxNonce, pfsPublicKey, nil) - c.GCM.Seal(serverHello[:pfsKeyExchangeLength], nil, ticket, nil) + nfsAEAD.Seal(serverHello[:0], MaxNonce, pfsPublicKey, nil) + c.AEAD.Seal(serverHello[:pfsKeyExchangeLength], nil, ticket[:], nil) padding := serverHello[pfsKeyExchangeLength+encryptedTicketLength:] - c.GCM.Seal(padding[:0], nil, EncodeLength(paddingLength-18), nil) - c.GCM.Seal(padding[:18], nil, padding[18:paddingLength-16], nil) - - if _, err := conn.Write(serverHello); err != nil { - return nil, err - } - // padding can be sent in a fragmented way, to create variable traffic pattern, before inner VLESS flow takes control + c.AEAD.Seal(padding[:0], nil, EncodeLength(paddingLength-18), nil) + c.AEAD.Seal(padding[:18], nil, padding[18:paddingLength-16], nil) - if i.Seconds > 0 { - i.RWLock.Lock() - i.Sessions[[16]byte(ticket)] = &ServerSession{ - Expire: time.Now().Add(time.Duration(i.Seconds) * time.Second), - PfsKey: pfsKey, + paddingLens[0] = pfsKeyExchangeLength + encryptedTicketLength + paddingLens[0] + for i, l := range paddingLens { // sends padding in a fragmented way, to create variable traffic pattern, before inner VLESS flow takes control + if l > 0 { + if _, err := conn.Write(serverHello[:l]); err != nil { + return nil, err + } + serverHello = serverHello[l:] + } + if len(paddingGaps) > i { + time.Sleep(paddingGaps[i]) } - i.RWLock.Unlock() } // important: allows client sends padding slowly, eliminating 1-RTT's traffic pattern if _, err := io.ReadFull(conn, encryptedLength); err != nil { return nil, err } - if _, err := nfsGCM.Open(encryptedLength[:0], nil, encryptedLength, nil); err != nil { + if _, err := nfsAEAD.Open(encryptedLength[:0], nil, encryptedLength, nil); err != nil { return nil, err } encryptedPadding := make([]byte, DecodeLength(encryptedLength[:2])) if _, err := io.ReadFull(conn, encryptedPadding); err != nil { return nil, err } - if _, err := nfsGCM.Open(encryptedPadding[:0], nil, encryptedPadding, nil); err != nil { + if _, err := nfsAEAD.Open(encryptedPadding[:0], nil, encryptedPadding, nil); err != nil { return nil, err } if i.XorMode == 2 { - c.Conn = NewXorConn(conn, NewCTR(c.UnitedKey, ticket), NewCTR(c.UnitedKey, iv), 0, 0) + c.Conn = NewXorConn(conn, NewCTR(c.UnitedKey, ticket[:]), NewCTR(c.UnitedKey, iv), 0, 0) } return c, nil } diff --git a/transport/vless/vision/conn.go b/transport/vless/vision/conn.go index 0c300e05d1..4e62b1536c 100644 --- a/transport/vless/vision/conn.go +++ b/transport/vless/vision/conn.go @@ -2,12 +2,12 @@ package vision import ( "bytes" - "crypto/subtle" "encoding/binary" "errors" "fmt" "io" "net" + "unsafe" "github.com/metacubex/mihomo/common/buf" N "github.com/metacubex/mihomo/common/net" @@ -24,21 +24,20 @@ type Conn struct { net.Conn // should be *vless.Conn N.ExtendedReader N.ExtendedWriter - userUUID *uuid.UUID + userUUID uuid.UUID - // tlsConn and it's internal variables - tlsConn net.Conn // maybe [*tls.Conn] or other tls-like conn + // [*tls.Conn] or other tls-like [net.Conn]'s internal variables netConn net.Conn // tlsConn.NetConn() input *bytes.Reader // &tlsConn.input or nil rawInput *bytes.Buffer // &tlsConn.rawInput or nil - needHandshake bool packetsToFilter int isTLS bool isTLS12orAbove bool enableXTLS bool cipher uint16 remainingServerHello uint16 + readRemainingBuffer *buf.Buffer readRemainingContent int readRemainingPadding int readProcess bool @@ -46,46 +45,73 @@ type Conn struct { readLastCommand byte writeFilterApplicationData bool writeDirect bool + writeOnceUserUUID []byte } func (vc *Conn) Read(b []byte) (int, error) { if vc.readProcess { buffer := buf.With(b) err := vc.ReadBuffer(buffer) + if unsafe.SliceData(buffer.Bytes()) != unsafe.SliceData(b) { // buffer.Bytes() not at the beginning of b + copy(b, buffer.Bytes()) + } return buffer.Len(), err } return vc.ExtendedReader.Read(b) } func (vc *Conn) ReadBuffer(buffer *buf.Buffer) error { - toRead := buffer.FreeBytes() + if vc.readRemainingBuffer != nil { + _, err := buffer.ReadOnceFrom(vc.readRemainingBuffer) + if vc.readRemainingBuffer.IsEmpty() { + vc.readRemainingBuffer.Release() + vc.readRemainingBuffer = nil + } + return err + } if vc.readRemainingContent > 0 { - if vc.readRemainingContent < buffer.FreeLen() { - toRead = toRead[:vc.readRemainingContent] + readSize := xrayBufSize // at least read xrayBufSize + if buffer.FreeLen() > readSize { // input buffer larger than xrayBufSize, read as much as possible + readSize = buffer.FreeLen() + } + if readSize > vc.readRemainingContent { // don't read out of bounds + readSize = vc.readRemainingContent } - n, err := vc.ExtendedReader.Read(toRead) - buffer.Truncate(n) + + readBuffer := buffer + if buffer.FreeLen() < readSize { + readBuffer = buf.NewSize(readSize) + vc.readRemainingBuffer = readBuffer + } + n, err := vc.ExtendedReader.Read(readBuffer.FreeBytes()[:readSize]) + readBuffer.Truncate(n) vc.readRemainingContent -= n - vc.FilterTLS(toRead) + vc.FilterTLS(readBuffer.Bytes()) + if vc.readRemainingBuffer != nil { + innerErr := vc.ReadBuffer(buffer) // back to top but not losing err + if err != nil { + err = innerErr + } + } return err } if vc.readRemainingPadding > 0 { - _, err := io.CopyN(io.Discard, vc.ExtendedReader, int64(vc.readRemainingPadding)) + n, err := io.CopyN(io.Discard, vc.ExtendedReader, int64(vc.readRemainingPadding)) if err != nil { return err } - vc.readRemainingPadding = 0 + vc.readRemainingPadding -= int(n) } if vc.readProcess { switch vc.readLastCommand { case commandPaddingContinue: //if vc.isTLS || vc.packetsToFilter > 0 { - headerUUIDLen := 0 - if vc.readFilterUUID { - headerUUIDLen = uuid.Size + need := PaddingHeaderLen + if !vc.readFilterUUID { + need = PaddingHeaderLen - uuid.Size } var header []byte - if need := headerUUIDLen + PaddingHeaderLen - uuid.Size; buffer.FreeLen() < need { + if buffer.FreeLen() < need { header = make([]byte, need) } else { header = buffer.FreeBytes()[:need] @@ -96,9 +122,8 @@ func (vc *Conn) ReadBuffer(buffer *buf.Buffer) error { } if vc.readFilterUUID { vc.readFilterUUID = false - if subtle.ConstantTimeCompare(vc.userUUID.Bytes(), header[:uuid.Size]) != 1 { - err = fmt.Errorf("XTLS Vision server responded unknown UUID: %s", - uuid.FromBytesOrNil(header[:uuid.Size]).String()) + if !bytes.Equal(vc.userUUID.Bytes(), header[:uuid.Size]) { + err = fmt.Errorf("XTLS Vision server responded unknown UUID: %s", uuid.FromBytesOrNil(header[:uuid.Size])) log.Errorln(err.Error()) return err } @@ -125,6 +150,7 @@ func (vc *Conn) ReadBuffer(buffer *buf.Buffer) error { } if vc.input.Len() == 0 { needReturn = true + *vc.input = bytes.Reader{} // full reset vc.input = nil } else { // buffer is full return nil @@ -139,6 +165,7 @@ func (vc *Conn) ReadBuffer(buffer *buf.Buffer) error { } needReturn = true if vc.rawInput.Len() == 0 { + *vc.rawInput = bytes.Buffer{} // full reset vc.rawInput = nil } } @@ -167,36 +194,19 @@ func (vc *Conn) Write(p []byte) (int, error) { } func (vc *Conn) WriteBuffer(buffer *buf.Buffer) (err error) { - if vc.needHandshake { - vc.needHandshake = false + if vc.writeFilterApplicationData { if buffer.IsEmpty() { - ApplyPadding(buffer, commandPaddingContinue, vc.userUUID, true) // we do a long padding to hide vless header - } else { - vc.FilterTLS(buffer.Bytes()) - ApplyPadding(buffer, commandPaddingContinue, vc.userUUID, vc.isTLS) - } - err = vc.ExtendedWriter.WriteBuffer(buffer) - if err != nil { - buffer.Release() - return err + ApplyPadding(buffer, commandPaddingContinue, &vc.writeOnceUserUUID, true) // we do a long padding to hide vless header + return vc.ExtendedWriter.WriteBuffer(buffer) } - err = vc.checkTLSVersion() - if err != nil { - buffer.Release() - return err - } - vc.tlsConn = nil - return nil - } - if vc.writeFilterApplicationData { vc.FilterTLS(buffer.Bytes()) buffers := vc.ReshapeBuffer(buffer) applyPadding := true for i, buffer := range buffers { command := commandPaddingContinue if applyPadding { - if vc.isTLS && buffer.Len() > 6 && bytes.Equal(buffer.To(3), tlsApplicationDataStart) { + if vc.isTLS && buffer.Len() > 6 && bytes.Equal(tlsApplicationDataStart, buffer.To(3)) { command = commandPaddingEnd if vc.enableXTLS { command = commandPaddingDirect @@ -209,7 +219,7 @@ func (vc *Conn) WriteBuffer(buffer *buf.Buffer) (err error) { vc.writeFilterApplicationData = false applyPadding = false } - ApplyPadding(buffer, command, nil, vc.isTLS) + ApplyPadding(buffer, command, &vc.writeOnceUserUUID, vc.isTLS) } err = vc.ExtendedWriter.WriteBuffer(buffer) @@ -232,7 +242,7 @@ func (vc *Conn) WriteBuffer(buffer *buf.Buffer) (err error) { } func (vc *Conn) FrontHeadroom() int { - if vc.readFilterUUID { + if vc.readFilterUUID || vc.writeOnceUserUUID != nil { return PaddingHeaderLen } return PaddingHeaderLen - uuid.Size @@ -243,7 +253,11 @@ func (vc *Conn) RearHeadroom() int { } func (vc *Conn) NeedHandshake() bool { - return vc.needHandshake + return vc.writeOnceUserUUID != nil +} + +func (vc *Conn) NeedAdditionalReadDeadline() bool { + return true } func (vc *Conn) Upstream() any { diff --git a/transport/vless/vision/padding.go b/transport/vless/vision/padding.go index 710f64c217..ac6de886dd 100644 --- a/transport/vless/vision/padding.go +++ b/transport/vless/vision/padding.go @@ -5,8 +5,8 @@ import ( "encoding/binary" "github.com/metacubex/mihomo/common/buf" + N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/log" - N "github.com/metacubex/sing/common/network" "github.com/gofrs/uuid/v5" "github.com/metacubex/randv2" @@ -20,7 +20,7 @@ const ( commandPaddingDirect byte = 0x02 ) -func ApplyPadding(buffer *buf.Buffer, command byte, userUUID *uuid.UUID, paddingTLS bool) { +func ApplyPadding(buffer *buf.Buffer, command byte, userUUID *[]byte, paddingTLS bool) { contentLen := int32(buffer.Len()) var paddingLen int32 if contentLen < 900 { @@ -35,25 +35,33 @@ func ApplyPadding(buffer *buf.Buffer, command byte, userUUID *uuid.UUID, padding binary.BigEndian.PutUint16(buffer.ExtendHeader(2), uint16(paddingLen)) binary.BigEndian.PutUint16(buffer.ExtendHeader(2), uint16(contentLen)) buffer.ExtendHeader(1)[0] = command - if userUUID != nil { - copy(buffer.ExtendHeader(uuid.Size), userUUID.Bytes()) + if userUUID != nil && *userUUID != nil { + copy(buffer.ExtendHeader(uuid.Size), *userUUID) + *userUUID = nil } buffer.Extend(int(paddingLen)) log.Debugln("XTLS Vision write padding: command=%d, payloadLen=%d, paddingLen=%d", command, contentLen, paddingLen) } +const xrayBufSize = 8192 + func (vc *Conn) ReshapeBuffer(buffer *buf.Buffer) []*buf.Buffer { - const xrayBufSize = 8192 - if buffer.Len() <= xrayBufSize-PaddingHeaderLen { + const bufferLimit = xrayBufSize - PaddingHeaderLen + if buffer.Len() < bufferLimit { return []*buf.Buffer{buffer} } - cutAt := bytes.LastIndex(buffer.Bytes(), tlsApplicationDataStart) - if cutAt == -1 { - cutAt = xrayBufSize / 2 + options := N.NewReadWaitOptions(nil, vc) + var buffers []*buf.Buffer + for buffer.Len() >= bufferLimit { + cutAt := bytes.LastIndex(buffer.Bytes(), tlsApplicationDataStart) + if cutAt < 21 || cutAt > bufferLimit { + cutAt = xrayBufSize / 2 + } + buffer2 := options.NewBuffer() // ensure the new buffer can send used in vc.WriteBuffer + buf.Must(buf.Error(buffer2.ReadFullFrom(buffer, cutAt))) + buffers = append(buffers, buffer2) } - buffer2 := N.NewReadWaitOptions(nil, vc).NewBuffer() // ensure the new buffer can send used in vc.WriteBuffer - buffer2.Write(buffer.From(cutAt)) - buffer.Truncate(cutAt) - return []*buf.Buffer{buffer, buffer2} + buffers = append(buffers, buffer) + return buffers } diff --git a/transport/vless/vision/vision.go b/transport/vless/vision/vision.go index f9158ca4e3..e9786981eb 100644 --- a/transport/vless/vision/vision.go +++ b/transport/vless/vision/vision.go @@ -1,4 +1,6 @@ // Package vision implements VLESS flow `xtls-rprx-vision` introduced by Xray-core. +// +// same logic as https://github.com/XTLS/Xray-core/blob/v25.9.11/proxy/proxy.go package vision import ( @@ -12,53 +14,87 @@ import ( N "github.com/metacubex/mihomo/common/net" tlsC "github.com/metacubex/mihomo/component/tls" + "github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/transport/vless/encryption" "github.com/gofrs/uuid/v5" ) +var ErrNotHandshakeComplete = errors.New("tls connection not handshake complete") var ErrNotTLS13 = errors.New("XTLS Vision based on TLS 1.3 outer connection") -func NewConn(conn net.Conn, tlsConn net.Conn, userUUID *uuid.UUID) (*Conn, error) { +func NewConn(conn net.Conn, tlsConn net.Conn, userUUID uuid.UUID) (*Conn, error) { c := &Conn{ ExtendedReader: N.NewExtendedReader(conn), ExtendedWriter: N.NewExtendedWriter(conn), Conn: conn, userUUID: userUUID, - tlsConn: tlsConn, - packetsToFilter: 6, - needHandshake: true, + packetsToFilter: 8, readProcess: true, readFilterUUID: true, writeFilterApplicationData: true, + writeOnceUserUUID: userUUID.Bytes(), } var t reflect.Type var p unsafe.Pointer - switch underlying := tlsConn.(type) { - case *gotls.Conn: - //log.Debugln("type tls") - c.netConn = underlying.NetConn() - t = reflect.TypeOf(underlying).Elem() - p = unsafe.Pointer(underlying) - case *tlsC.Conn: - //log.Debugln("type *tlsC.Conn") - c.netConn = underlying.NetConn() - t = reflect.TypeOf(underlying).Elem() - p = unsafe.Pointer(underlying) - case *tlsC.UConn: - //log.Debugln("type *tlsC.UConn") - c.netConn = underlying.NetConn() - t = reflect.TypeOf(underlying.Conn).Elem() - //log.Debugln("t:%v", t) - p = unsafe.Pointer(underlying.Conn) - case *encryption.CommonConn: - //log.Debugln("type *encryption.CommonConn") - c.netConn = underlying.Conn - t = reflect.TypeOf(underlying).Elem() - p = unsafe.Pointer(underlying) - default: - return nil, fmt.Errorf(`failed to use vision, maybe "security" is not "tls" or "utls"`) + var upstream any = tlsConn + for { + switch underlying := upstream.(type) { + case *gotls.Conn: + //log.Debugln("type tls") + tlsConn = underlying + c.netConn = underlying.NetConn() + t = reflect.TypeOf(underlying).Elem() + p = unsafe.Pointer(underlying) + break + case *tlsC.Conn: + //log.Debugln("type *tlsC.Conn") + tlsConn = underlying + c.netConn = underlying.NetConn() + t = reflect.TypeOf(underlying).Elem() + p = unsafe.Pointer(underlying) + break + case *tlsC.UConn: + //log.Debugln("type *tlsC.UConn") + tlsConn = underlying + c.netConn = underlying.NetConn() + t = reflect.TypeOf(underlying.Conn).Elem() + //log.Debugln("t:%v", t) + p = unsafe.Pointer(underlying.Conn) + break + case *encryption.CommonConn: + //log.Debugln("type *encryption.CommonConn") + tlsConn = underlying + c.netConn = underlying.Conn + t = reflect.TypeOf(underlying).Elem() + p = unsafe.Pointer(underlying) + break + } + if u, ok := upstream.(N.ReaderWithUpstream); !ok || !u.ReaderReplaceable() { // must replaceable + break + } + if u, ok := upstream.(N.WithUpstreamReader); ok { + upstream = u.UpstreamReader() + continue + } + if u, ok := upstream.(N.WithUpstream); ok { + upstream = u.Upstream() + continue + } + } + if t == nil || p == nil { + log.Warnln("vision: not a valid supported TLS connection: %s", reflect.TypeOf(tlsConn)) + return nil, fmt.Errorf(`failed to use vision, maybe "tls" is not enable and "encryption" is empty`) } + + if err := checkTLSVersion(tlsConn); err != nil { + if errors.Is(err, ErrNotHandshakeComplete) { + log.Warnln("vision: TLS connection not handshake complete: %s", reflect.TypeOf(tlsConn)) + } else { + return nil, err + } + } + if i, ok := t.FieldByName("input"); ok { c.input = (*bytes.Reader)(unsafe.Add(p, i.Offset)) } @@ -68,18 +104,30 @@ func NewConn(conn net.Conn, tlsConn net.Conn, userUUID *uuid.UUID) (*Conn, error return c, nil } -func (vc *Conn) checkTLSVersion() error { - switch underlying := vc.tlsConn.(type) { +func checkTLSVersion(tlsConn net.Conn) error { + switch underlying := tlsConn.(type) { case *gotls.Conn: - if underlying.ConnectionState().Version != gotls.VersionTLS13 { + state := underlying.ConnectionState() + if !state.HandshakeComplete { + return ErrNotHandshakeComplete + } + if state.Version != gotls.VersionTLS13 { return ErrNotTLS13 } case *tlsC.Conn: - if underlying.ConnectionState().Version != tlsC.VersionTLS13 { + state := underlying.ConnectionState() + if !state.HandshakeComplete { + return ErrNotHandshakeComplete + } + if state.Version != tlsC.VersionTLS13 { return ErrNotTLS13 } case *tlsC.UConn: - if underlying.ConnectionState().Version != tlsC.VersionTLS13 { + state := underlying.ConnectionState() + if !state.HandshakeComplete { + return ErrNotHandshakeComplete + } + if state.Version != tlsC.VersionTLS13 { return ErrNotTLS13 } } diff --git a/transport/vless/vless.go b/transport/vless/vless.go index 9fb54f9205..4e99b9ba68 100644 --- a/transport/vless/vless.go +++ b/transport/vless/vless.go @@ -42,7 +42,7 @@ type DstAddr struct { // Client is vless connection generator type Client struct { - uuid *uuid.UUID + uuid uuid.UUID Addons *Addons } @@ -63,7 +63,7 @@ func NewClient(uuidStr string, addons *Addons) (*Client, error) { } return &Client{ - uuid: &uid, + uuid: uid, Addons: addons, }, nil } diff --git a/transport/vmess/tls.go b/transport/vmess/tls.go index 3bfcb46ac5..7239ebd238 100644 --- a/transport/vmess/tls.go +++ b/transport/vmess/tls.go @@ -15,6 +15,8 @@ type TLSConfig struct { Host string SkipCertVerify bool FingerPrint string + Certificate string + PrivateKey string ClientFingerprint string NextProtos []string ECH *ech.Config @@ -26,14 +28,16 @@ type ECHConfig struct { } func StreamTLSConn(ctx context.Context, conn net.Conn, cfg *TLSConfig) (net.Conn, error) { - tlsConfig := &tls.Config{ - ServerName: cfg.Host, - InsecureSkipVerify: cfg.SkipCertVerify, - NextProtos: cfg.NextProtos, - } - - var err error - tlsConfig, err = ca.GetSpecifiedFingerprintTLSConfig(tlsConfig, cfg.FingerPrint) + tlsConfig, err := ca.GetTLSConfig(ca.Option{ + TLSConfig: &tls.Config{ + ServerName: cfg.Host, + InsecureSkipVerify: cfg.SkipCertVerify, + NextProtos: cfg.NextProtos, + }, + Fingerprint: cfg.FingerPrint, + Certificate: cfg.Certificate, + PrivateKey: cfg.PrivateKey, + }) if err != nil { return nil, err } diff --git a/tunnel/statistic/manager.go b/tunnel/statistic/manager.go index 90a34b6d0f..9db4601e14 100644 --- a/tunnel/statistic/manager.go +++ b/tunnel/statistic/manager.go @@ -6,8 +6,7 @@ import ( "github.com/metacubex/mihomo/common/atomic" "github.com/metacubex/mihomo/common/xsync" - - "github.com/shirou/gopsutil/v4/process" + "github.com/metacubex/mihomo/component/memory" ) var DefaultManager *Manager @@ -20,7 +19,7 @@ func init() { downloadBlip: atomic.NewInt64(0), uploadTotal: atomic.NewInt64(0), downloadTotal: atomic.NewInt64(0), - process: &process.Process{Pid: int32(os.Getpid())}, + pid: int32(os.Getpid()), } go DefaultManager.handle() @@ -34,7 +33,7 @@ type Manager struct { downloadBlip atomic.Int64 uploadTotal atomic.Int64 downloadTotal atomic.Int64 - process *process.Process + pid int32 memory uint64 } @@ -93,7 +92,7 @@ func (m *Manager) Snapshot() *Snapshot { } func (m *Manager) updateMemory() { - stat, err := m.process.MemoryInfo() + stat, err := memory.GetMemoryInfo(m.pid) if err != nil { return }