diff --git a/constant/proxy.go b/constant/proxy.go index f2ca2c8e..6dbee726 100644 --- a/constant/proxy.go +++ b/constant/proxy.go @@ -13,6 +13,7 @@ const ( TypeShadowsocks = "shadowsocks" TypeVMess = "vmess" TypeTrojan = "trojan" + TypeNaive = "naive" ) const ( diff --git a/constant/quic.go b/constant/quic.go new file mode 100644 index 00000000..8df79d16 --- /dev/null +++ b/constant/quic.go @@ -0,0 +1,5 @@ +//go:build with_quic + +package constant + +const QUIC_AVAILABLE = true diff --git a/constant/quic_stub.go b/constant/quic_stub.go new file mode 100644 index 00000000..e91b1379 --- /dev/null +++ b/constant/quic_stub.go @@ -0,0 +1,5 @@ +//go:build !with_quic + +package constant + +const QUIC_AVAILABLE = false diff --git a/go.mod b/go.mod index 20871989..d7002e8b 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/gorilla/websocket v1.5.0 github.com/hashicorp/yamux v0.1.1 github.com/logrusorgru/aurora v2.0.3+incompatible + github.com/lucas-clemente/quic-go v0.28.1 github.com/oschwald/maxminddb-golang v1.10.0 github.com/sagernet/sing v0.0.0-20220808004927-21369d10810d github.com/sagernet/sing-dns v0.0.0-20220803121532-9e1ffb850d91 @@ -23,7 +24,7 @@ require ( github.com/xtaci/smux v1.5.16 go.uber.org/atomic v1.9.0 golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa - golang.org/x/net v0.0.0-20220809012201-f428fae20770 + golang.org/x/net v0.0.0-20220809184613-07c6da5e1ced golang.org/x/sys v0.0.0-20220808155132-1c4a2a72c664 ) @@ -36,7 +37,6 @@ require ( github.com/google/btree v1.0.1 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/klauspost/cpuid/v2 v2.0.12 // indirect - github.com/lucas-clemente/quic-go v0.28.1 // indirect github.com/marten-seemann/qpack v0.2.1 // indirect github.com/marten-seemann/qtls-go1-16 v0.1.5 // indirect github.com/marten-seemann/qtls-go1-17 v0.1.2 // indirect diff --git a/go.sum b/go.sum index f22e06f8..1fa83389 100644 --- a/go.sum +++ b/go.sum @@ -247,8 +247,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220809012201-f428fae20770 h1:dIi4qVdvjZEjiMDv7vhokAZNGnz3kepwuXqFKYDdDMs= -golang.org/x/net v0.0.0-20220809012201-f428fae20770/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20220809184613-07c6da5e1ced h1:3dYNDff0VT5xj+mbj2XucFst9WKk6PdGOrb9n+SbIvw= +golang.org/x/net v0.0.0-20220809184613-07c6da5e1ced/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= diff --git a/inbound/builder.go b/inbound/builder.go index a505068f..ee51d67e 100644 --- a/inbound/builder.go +++ b/inbound/builder.go @@ -35,6 +35,8 @@ func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, o return NewVMess(ctx, router, logger, options.Tag, options.VMessOptions) case C.TypeTrojan: return NewTrojan(ctx, router, logger, options.Tag, options.TrojanOptions) + case C.TypeNaive: + return NewNaive(ctx, router, logger, options.Tag, options.NaiveOptions) default: return nil, E.New("unknown inbound type: ", options.Type) } diff --git a/inbound/naive.go b/inbound/naive.go new file mode 100644 index 00000000..ef3fb36b --- /dev/null +++ b/inbound/naive.go @@ -0,0 +1,398 @@ +package inbound + +import ( + "context" + "encoding/base64" + "encoding/binary" + "io" + "math/rand" + "net" + "net/http" + "net/netip" + "os" + "runtime" + "strings" + "time" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-dns" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/auth" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio" + E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/rw" +) + +var _ adapter.Inbound = (*Naive)(nil) + +type Naive struct { + ctx context.Context + router adapter.Router + logger log.ContextLogger + tag string + listenOptions option.ListenOptions + network []string + authenticator auth.Authenticator + tlsConfig *TLSConfig + httpServer *http.Server + h3Server any +} + +var ( + ErrNaiveTLSRequired = E.New("TLS required") + ErrNaiveMissingUsers = E.New("missing users") +) + +func NewNaive(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.NaiveInboundOptions) (*Naive, error) { + inbound := &Naive{ + ctx: ctx, + router: router, + logger: logger, + tag: tag, + listenOptions: options.ListenOptions, + network: options.Network.Build(), + authenticator: auth.NewAuthenticator(options.Users), + } + if options.TLS == nil || !options.TLS.Enabled { + return nil, ErrNaiveTLSRequired + } + if len(options.Users) == 0 { + return nil, ErrNaiveMissingUsers + } + tlsConfig, err := NewTLSConfig(logger, common.PtrValueOrDefault(options.TLS)) + if err != nil { + return nil, err + } + inbound.tlsConfig = tlsConfig + return inbound, nil +} + +func (n *Naive) Type() string { + return C.TypeNaive +} + +func (n *Naive) Tag() string { + return n.tag +} + +func (n *Naive) Start() error { + err := n.tlsConfig.Start() + if err != nil { + return E.Cause(err, "create TLS config") + } + + n.httpServer = &http.Server{ + Handler: n, + TLSConfig: n.tlsConfig.Config(), + } + + var listenAddr string + if nAddr := netip.Addr(n.listenOptions.Listen); nAddr.IsValid() { + if n.listenOptions.ListenPort != 0 { + listenAddr = M.SocksaddrFrom(netip.Addr(n.listenOptions.Listen), n.listenOptions.ListenPort).String() + } else { + listenAddr = net.JoinHostPort(nAddr.String(), ":https") + } + } else if n.listenOptions.ListenPort != 0 { + listenAddr = ":" + F.ToString(n.listenOptions.ListenPort) + } else { + listenAddr = ":https" + } + + if common.Contains(n.network, N.NetworkTCP) { + tcpListener, err := net.Listen(M.NetworkFromNetAddr("tcp", netip.Addr(n.listenOptions.Listen)), listenAddr) + if err != nil { + return err + } + n.logger.Info("tcp server started at ", tcpListener.Addr()) + go func() { + sErr := n.httpServer.ServeTLS(tcpListener, "", "") + if sErr == http.ErrServerClosed { + } else if sErr != nil { + n.logger.Error("http server serve error: ", sErr) + } + }() + } + + if common.Contains(n.network, N.NetworkUDP) { + err = n.configureHTTP3Listener(listenAddr) + if !C.QUIC_AVAILABLE && len(n.network) > 1 { + log.Warn(E.Cause(err, "naive http3 disabled")) + } else if err != nil { + return err + } + } + + return nil +} + +func (n *Naive) Close() error { + return common.Close( + common.PtrOrNil(n.httpServer), + n.h3Server, + common.PtrOrNil(n.tlsConfig), + ) +} + +func (n *Naive) ServeHTTP(writer http.ResponseWriter, request *http.Request) { + ctx := log.ContextWithNewID(request.Context()) + if request.Method != "CONNECT" { + n.logger.ErrorContext(ctx, "bad request: not connect") + rejectHTTP(writer, http.StatusBadRequest) + return + } else if request.Header.Get("Padding") == "" { + n.logger.ErrorContext(ctx, "bad request: missing padding") + rejectHTTP(writer, http.StatusBadRequest) + return + } + var authOk bool + authorization := request.Header.Get("Proxy-Authorization") + if strings.HasPrefix(authorization, "BASIC ") || strings.HasPrefix(authorization, "Basic ") { + userPassword, _ := base64.URLEncoding.DecodeString(authorization[6:]) + userPswdArr := strings.SplitN(string(userPassword), ":", 2) + authOk = n.authenticator.Verify(userPswdArr[0], userPswdArr[1]) + if authOk { + ctx = auth.ContextWithUser(ctx, userPswdArr[0]) + } + } + if !authOk { + n.logger.ErrorContext(ctx, "bad request: authorization failed") + rejectHTTP(writer, http.StatusProxyAuthRequired) + return + } + writer.Header().Set("Padding", generateNaivePaddingHeader()) + writer.WriteHeader(http.StatusOK) + writer.(http.Flusher).Flush() + + if request.ProtoMajor == 1 { + n.logger.ErrorContext(ctx, "bad request: http1") + rejectHTTP(writer, http.StatusBadRequest) + return + } + + hostPort := request.URL.Host + if hostPort == "" { + hostPort = request.Host + } + source := M.ParseSocksaddr(request.RemoteAddr) + destination := M.ParseSocksaddr(hostPort) + n.newConnection(ctx, &naivePaddingConn{reader: request.Body, writer: writer, flusher: writer.(http.Flusher)}, source, destination) +} + +func (n *Naive) newConnection(ctx context.Context, conn net.Conn, source, destination M.Socksaddr) { + var metadata adapter.InboundContext + metadata.Inbound = n.tag + metadata.InboundType = C.TypeNaive + metadata.SniffEnabled = n.listenOptions.SniffEnabled + metadata.SniffOverrideDestination = n.listenOptions.SniffOverrideDestination + metadata.DomainStrategy = dns.DomainStrategy(n.listenOptions.DomainStrategy) + metadata.Network = N.NetworkTCP + metadata.Source = source + metadata.Destination = destination + hErr := n.router.RouteConnection(ctx, conn, metadata) + if hErr != nil { + conn.Close() + NewError(n.logger, ctx, E.Cause(hErr, "process connection from ", metadata.Source)) + } +} + +func rejectHTTP(writer http.ResponseWriter, statusCode int) { + hijacker, ok := writer.(http.Hijacker) + if !ok { + writer.WriteHeader(statusCode) + return + } + conn, _, err := hijacker.Hijack() + if err != nil { + writer.WriteHeader(statusCode) + return + } + if tcpConn, isTCP := common.Cast[*net.TCPConn](conn); isTCP { + tcpConn.SetLinger(0) + } + conn.Close() +} + +func generateNaivePaddingHeader() string { + paddingLen := rand.Intn(32) + 30 + padding := make([]byte, paddingLen) + bits := rand.Uint64() + for i := 0; i < 16; i++ { + // Codes that won't be Huffman coded. + padding[i] = "!#$()+<>?@[]^`{}"[bits&15] + bits >>= 4 + } + for i := 16; i < paddingLen; i++ { + padding[i] = '~' + } + return string(padding) +} + +const kFirstPaddings = 8 + +var _ net.Conn = (*naivePaddingConn)(nil) + +type naivePaddingConn struct { + reader io.Reader + writer io.Writer + flusher http.Flusher + rAddr net.Addr + readPadding int + writePadding int + readRemaining int + paddingRemaining int +} + +func (c *naivePaddingConn) Read(p []byte) (n int, err error) { + n, err = c.read(p) + err = wrapHttpError(err) + return +} + +func (c *naivePaddingConn) read(p []byte) (n int, err error) { + if c.readRemaining > 0 { + if len(p) > c.readRemaining { + p = p[:c.readRemaining] + } + n, err = c.read(p) + if err != nil { + return + } + c.readRemaining -= n + return + } + if c.paddingRemaining > 0 { + err = rw.SkipN(c.reader, c.paddingRemaining) + if err != nil { + return + } + c.readRemaining = 0 + } + if c.readPadding < kFirstPaddings { + paddingHdr := p[:3] + _, err = io.ReadFull(c.reader, paddingHdr) + if err != nil { + return + } + originalDataSize := int(binary.BigEndian.Uint16(paddingHdr[:2])) + paddingSize := int(paddingHdr[2]) + if len(p) > originalDataSize { + p = p[:originalDataSize] + } + n, err = c.reader.Read(p) + if err != nil { + return + } + c.readPadding++ + c.readRemaining = originalDataSize - n + c.paddingRemaining = paddingSize + return + } + return c.reader.Read(p) +} + +func (c *naivePaddingConn) Write(p []byte) (n int, err error) { + n, err = c.write(p) + if err == nil { + c.flusher.Flush() + } + err = wrapHttpError(err) + return +} + +func (c *naivePaddingConn) write(p []byte) (n int, err error) { + if c.writePadding < kFirstPaddings { + paddingSize := rand.Intn(256) + _buffer := buf.Make(3 + len(p) + paddingSize) + defer runtime.KeepAlive(_buffer) + buffer := common.Dup(_buffer) + binary.BigEndian.PutUint16(buffer, uint16(len(p))) + buffer[2] = byte(paddingSize) + copy(buffer[3:], p) + _, err = c.writer.Write(buffer) + if err != nil { + return + } + c.writePadding++ + } + return c.writer.Write(p) +} + +func (c *naivePaddingConn) WriteBuffer(buffer *buf.Buffer) error { + defer buffer.Release() + if c.writePadding < kFirstPaddings { + bufferLen := buffer.Len() + paddingSize := rand.Intn(256) + header := buffer.ExtendHeader(3) + binary.BigEndian.PutUint16(header, uint16(bufferLen)) + header[2] = byte(paddingSize) + buffer.Extend(paddingSize) + c.writePadding++ + } + err := common.Error(c.writer.Write(buffer.Bytes())) + if err == nil { + c.flusher.Flush() + } + return wrapHttpError(err) +} + +func (c *naivePaddingConn) WriteTo(w io.Writer) (n int64, err error) { + if c.readPadding < kFirstPaddings { + return bufio.WriteTo0(c, w) + } + return bufio.Copy(w, c.reader) +} + +func (c *naivePaddingConn) ReadFrom(r io.Reader) (n int64, err error) { + if c.writePadding < kFirstPaddings { + return bufio.ReadFrom0(c, r) + } + return bufio.Copy(c.writer, r) +} + +func (c *naivePaddingConn) Close() error { + return common.Close( + c.reader, + c.writer, + ) +} + +func (c *naivePaddingConn) LocalAddr() net.Addr { + return nil +} + +func (c *naivePaddingConn) RemoteAddr() net.Addr { + return c.rAddr +} + +func (c *naivePaddingConn) SetDeadline(t time.Time) error { + return os.ErrInvalid +} + +func (c *naivePaddingConn) SetReadDeadline(t time.Time) error { + return os.ErrInvalid +} + +func (c *naivePaddingConn) SetWriteDeadline(t time.Time) error { + return os.ErrInvalid +} + +var http2errClientDisconnected = "client disconnected" + +func wrapHttpError(err error) error { + if err == nil { + return err + } + switch err.Error() { + case http2errClientDisconnected: + return net.ErrClosed + } + return err +} diff --git a/inbound/naive_quic.go b/inbound/naive_quic.go new file mode 100644 index 00000000..de6969ce --- /dev/null +++ b/inbound/naive_quic.go @@ -0,0 +1,40 @@ +//go:build with_quic + +package inbound + +import ( + "net" + "net/netip" + + M "github.com/sagernet/sing/common/metadata" + + "github.com/lucas-clemente/quic-go" + "github.com/lucas-clemente/quic-go/http3" +) + +func (n *Naive) configureHTTP3Listener(listenAddr string) error { + h3Server := &http3.Server{ + Port: int(n.listenOptions.ListenPort), + TLSConfig: n.tlsConfig.Config(), + Handler: n, + } + + udpListener, err := net.ListenPacket(M.NetworkFromNetAddr("udp", netip.Addr(n.listenOptions.Listen)), listenAddr) + if err != nil { + return err + } + + n.logger.Info("udp server started at ", udpListener.LocalAddr()) + + go func() { + sErr := h3Server.Serve(udpListener) + if sErr == quic.ErrServerClosed { + return + } else if sErr != nil { + n.logger.Error("http3 server serve error: ", sErr) + } + }() + + n.h3Server = h3Server + return nil +} diff --git a/inbound/naive_quic_stub.go b/inbound/naive_quic_stub.go new file mode 100644 index 00000000..d7a2a811 --- /dev/null +++ b/inbound/naive_quic_stub.go @@ -0,0 +1,9 @@ +//go:build !with_quic + +package inbound + +import E "github.com/sagernet/sing/common/exceptions" + +func (n *Naive) configureHTTP3Listener(listenAddr string) error { + return E.New("QUIC is not included in this build, rebuild with -tags with_quic") +} diff --git a/option/inbound.go b/option/inbound.go index 33fe5321..57f7ac92 100644 --- a/option/inbound.go +++ b/option/inbound.go @@ -19,6 +19,7 @@ type _Inbound struct { ShadowsocksOptions ShadowsocksInboundOptions `json:"-"` VMessOptions VMessInboundOptions `json:"-"` TrojanOptions TrojanInboundOptions `json:"-"` + NaiveOptions NaiveInboundOptions `json:"-"` } type Inbound _Inbound @@ -46,6 +47,8 @@ func (h Inbound) MarshalJSON() ([]byte, error) { v = h.VMessOptions case C.TypeTrojan: v = h.TrojanOptions + case C.TypeNaive: + v = h.NaiveOptions default: return nil, E.New("unknown inbound type: ", h.Type) } @@ -79,6 +82,8 @@ func (h *Inbound) UnmarshalJSON(bytes []byte) error { v = &h.VMessOptions case C.TypeTrojan: v = &h.TrojanOptions + case C.TypeNaive: + v = &h.NaiveOptions default: return E.New("unknown inbound type: ", h.Type) } diff --git a/option/naive.go b/option/naive.go new file mode 100644 index 00000000..e7e1a0b2 --- /dev/null +++ b/option/naive.go @@ -0,0 +1,10 @@ +package option + +import "github.com/sagernet/sing/common/auth" + +type NaiveInboundOptions struct { + ListenOptions + Users []auth.User `json:"users,omitempty"` + Network NetworkList `json:"network,omitempty"` + TLS *InboundTLSOptions `json:"tls,omitempty"` +} diff --git a/test/box_test.go b/test/box_test.go index a2edad7a..e7bc9bd4 100644 --- a/test/box_test.go +++ b/test/box_test.go @@ -8,8 +8,6 @@ import ( "github.com/sagernet/sing-box" "github.com/sagernet/sing-box/option" - "github.com/sagernet/sing/common/control" - F "github.com/sagernet/sing/common/format" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/protocol/socks" @@ -17,23 +15,6 @@ import ( "github.com/stretchr/testify/require" ) -func mkPort(t *testing.T) uint16 { - var lc net.ListenConfig - lc.Control = control.ReuseAddr() - for { - tcpListener, err := lc.Listen(context.Background(), "tcp", ":0") - require.NoError(t, err) - listenPort := M.SocksaddrFromNet(tcpListener.Addr()).Port - tcpListener.Close() - udpListener, err := lc.Listen(context.Background(), "tcp", F.ToString(":", listenPort)) - if err != nil { - continue - } - udpListener.Close() - return listenPort - } -} - func startInstance(t *testing.T, options option.Options) { var instance *box.Box var err error @@ -54,6 +35,14 @@ func startInstance(t *testing.T, options option.Options) { }) } +func testTCP(t *testing.T, clientPort uint16, testPort uint16) { + dialer := socks.NewClient(N.SystemDialer, M.ParseSocksaddrHostPort("127.0.0.1", clientPort), socks.Version5, "", "") + dialTCP := func() (net.Conn, error) { + return dialer.DialContext(context.Background(), "tcp", M.ParseSocksaddrHostPort("127.0.0.1", testPort)) + } + require.NoError(t, testPingPongWithConn(t, testPort, dialTCP)) +} + func testSuit(t *testing.T, clientPort uint16, testPort uint16) { dialer := socks.NewClient(N.SystemDialer, M.ParseSocksaddrHostPort("127.0.0.1", clientPort), socks.Version5, "", "") dialTCP := func() (net.Conn, error) { diff --git a/test/clash_test.go b/test/clash_test.go index c3a39379..3330baa8 100644 --- a/test/clash_test.go +++ b/test/clash_test.go @@ -30,6 +30,7 @@ const ( ImageShadowsocksRustClient = "ghcr.io/shadowsocks/sslocal-rust:latest" ImageV2RayCore = "v2fly/v2fly-core:latest" ImageTrojan = "trojangfw/trojan:latest" + ImageNaive = "pocat/naiveproxy:client" ) var allImages = []string{ @@ -37,6 +38,7 @@ var allImages = []string{ ImageShadowsocksRustClient, ImageV2RayCore, ImageTrojan, + ImageNaive, } var localIP = netip.MustParseAddr("127.0.0.1") diff --git a/test/config/example.org-key.pem b/test/config/example.org-key.pem deleted file mode 100644 index dbe9a3db..00000000 --- a/test/config/example.org-key.pem +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDQ+c++LkDTdaw5 -5spCu9MWMcvVdrYBZZ5qZy7DskphSUSQp25cIu34GJXVPNxtbWx1CQCmdLlwqXvo -PfUt5/pz9qsfhdAbzFduZQgGd7GTQOTJBDrAhm2+iVsQyGHHhF68muN+SgT+AtRE -sJyZoHNYtjjWEIHQ++FHEDqwUVnj6Ut99LHlyfCjOZ5+WyBiKCjyMNots/gDep7R -i4X2kMTqNMIIqPUcAaP5EQk41bJbFhKe915qN9b1dRISKFKmiWeOsxgTB/O/EaL5 -LsBYwZ/BiIMDk30aZvzRJeloasIR3z4hrKQqBfB0lfeIdiPpJIs5rXJQEiWH89ge -gplsLbfrAgMBAAECggEBAKpMGaZzDPMF/v8Ee6lcZM2+cMyZPALxa+JsCakCvyh+ -y7hSKVY+RM0cQ+YM/djTBkJtvrDniEMuasI803PAitI7nwJGSuyMXmehP6P9oKFO -jeLeZn6ETiSqzKJlmYE89vMeCevdqCnT5mW/wy5Smg0eGj0gIJpM2S3PJPSQpv9Z -ots0JXkwooJcpGWzlwPkjSouY2gDbE4Coi+jmYLNjA1k5RbggcutnUCZZkJ6yMNv -H52VjnkffpAFHRouK/YgF+5nbMyyw5YTLOyTWBq7qfBMsXynkWLU73GC/xDZa3yG -o/Ph2knXCjgLmCRessTOObdOXedjnGWIjiqF8fVboDECgYEA6x5CteYiwthDBULZ -CG5nE9VKkRHJYdArm+VjmGbzK51tKli112avmU4r3ol907+mEa4tWLkPqdZrrL49 -aHltuHizZJixJcw0rcI302ot/Ov0gkF9V55gnAQS/Kemvx9FHWm5NHdYvbObzj33 -bYRLJBtJWzYg9M8Bw9ZrUnegc/MCgYEA44kq5OSYCbyu3eaX8XHTtFhuQHNFjwl7 -Xk/Oel6PVZzmt+oOlDHnOfGSB/KpR3YXxFRngiiPZzbrOwFyPGe7HIfg03HAXiJh -ivEfrPHbQqQUI/4b44GpDy6bhNtz777ivFGYEt21vpwd89rFiye+RkqF8eL/evxO -pUayDZYvwikCgYEA07wFoZ/lkAiHmpZPsxsRcrfzFd+pto9splEWtumHdbCo3ajT -4W5VFr9iHF8/VFDT8jokFjFaXL1/bCpKTOqFl8oC68XiSkKy8gPkmFyXm5y2LhNi -GGTFZdr5alRkgttbN5i9M/WCkhvMZRhC2Xp43MRB9IUzeqNtWHqhXbvjYGcCgYEA -vTMOztviLJ6PjYa0K5lp31l0+/SeD21j/y0/VPOSHi9kjeN7EfFZAw6DTkaSShDB -fIhutYVCkSHSgfMW6XGb3gKCiW/Z9KyEDYOowicuGgDTmoYu7IOhbzVjLhtJET7Z -zJvQZ0eiW4f3RBFTF/4JMuu+6z7FD6ADSV06qx+KQNkCgYBw26iQxmT5e/4kVv8X -DzBJ1HuliKBnnzZA1YRjB4H8F6Yrq+9qur1Lurez4YlbkGV8yPFt+Iu82ViUWL28 -9T7Jgp3TOpf8qOqsWFv8HldpEZbE0Tcib4x6s+zOg/aw0ac/xOPY1sCVFB81VODP -XCar+uxMBXI1zbXqd9QdEwy4Ig== ------END PRIVATE KEY----- diff --git a/test/config/example.org.pem b/test/config/example.org.pem deleted file mode 100644 index 9b99259a..00000000 --- a/test/config/example.org.pem +++ /dev/null @@ -1,25 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIESzCCArOgAwIBAgIQIi5xRZvFZaSweWU9Y5mExjANBgkqhkiG9w0BAQsFADCB -hzEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMS4wLAYDVQQLDCVkcmVh -bWFjcm9ARHJlYW1hY3JvLmxvY2FsIChEcmVhbWFjcm8pMTUwMwYDVQQDDCxta2Nl -cnQgZHJlYW1hY3JvQERyZWFtYWNyby5sb2NhbCAoRHJlYW1hY3JvKTAeFw0yMTAz -MTcxNDQwMzZaFw0yMzA2MTcxNDQwMzZaMFkxJzAlBgNVBAoTHm1rY2VydCBkZXZl -bG9wbWVudCBjZXJ0aWZpY2F0ZTEuMCwGA1UECwwlZHJlYW1hY3JvQERyZWFtYWNy -by5sb2NhbCAoRHJlYW1hY3JvKTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC -ggEBAND5z74uQNN1rDnmykK70xYxy9V2tgFlnmpnLsOySmFJRJCnblwi7fgYldU8 -3G1tbHUJAKZ0uXCpe+g99S3n+nP2qx+F0BvMV25lCAZ3sZNA5MkEOsCGbb6JWxDI -YceEXrya435KBP4C1ESwnJmgc1i2ONYQgdD74UcQOrBRWePpS330seXJ8KM5nn5b -IGIoKPIw2i2z+AN6ntGLhfaQxOo0wgio9RwBo/kRCTjVslsWEp73Xmo31vV1EhIo -UqaJZ46zGBMH878RovkuwFjBn8GIgwOTfRpm/NEl6WhqwhHfPiGspCoF8HSV94h2 -I+kkizmtclASJYfz2B6CmWwtt+sCAwEAAaNgMF4wDgYDVR0PAQH/BAQDAgWgMBMG -A1UdJQQMMAoGCCsGAQUFBwMBMB8GA1UdIwQYMBaAFO800LQ6Pa85RH4EbMmFH6ln -F150MBYGA1UdEQQPMA2CC2V4YW1wbGUub3JnMA0GCSqGSIb3DQEBCwUAA4IBgQAP -TsF53h7bvJcUXT3Y9yZ2vnW6xr9r92tNnM1Gfo3D2Yyn9oLf2YrfJng6WZ04Fhqa -Wh0HOvE0n6yPNpm/Q7mh64DrgolZ8Ce5H4RTJDAabHU9XhEzfGSVtzRSFsz+szu1 -Y30IV+08DxxqMmNPspYdpAET2Lwyk2WhnARGiGw11CRkQCEkVEe6d702vS9UGBUz -Du6lmCYCm0SbFrZ0CGgmHSHoTcCtf3EjVam7dPg3yWiPbWjvhXxgip6hz9sCqkhG -WA5f+fPgSZ1I9U4i+uYnqjfrzwgC08RwUYordm15F6gPvXw+KVwDO8yUYQoEH0b6 -AFJtbzoAXDysvBC6kWYFFOr62EaisaEkELTS/NrPD9ux1eKbxcxHCwEtVjgC0CL6 -gAxEAQ+9maJMbrAFhsOBbGGFC+mMCGg4eEyx6+iMB0oQe0W7QFeRUAFi7Ptc/ocS -tZ9lbrfX1/wrcTTWIYWE+xH6oeb4fhs29kxjHcf2l+tQzmpl0aP3Z/bMW4BSB+w= ------END CERTIFICATE----- diff --git a/test/config/naive-quic.json b/test/config/naive-quic.json new file mode 100644 index 00000000..ec891cd8 --- /dev/null +++ b/test/config/naive-quic.json @@ -0,0 +1,6 @@ +{ + "listen": "socks://127.0.0.1:10001", + "proxy": "quic://sekai:password@example.org:10000", + "host-resolver-rules": "MAP example.org 127.0.0.1", + "log": "" +} \ No newline at end of file diff --git a/test/config/naive.json b/test/config/naive.json new file mode 100644 index 00000000..c7b21101 --- /dev/null +++ b/test/config/naive.json @@ -0,0 +1,6 @@ +{ + "listen": "socks://127.0.0.1:10001", + "proxy": "https://sekai:password@example.org:10000", + "host-resolver-rules": "MAP example.org 127.0.0.1", + "log": "" +} \ No newline at end of file diff --git a/test/docker_test.go b/test/docker_test.go index 94cc910d..bca1dbeb 100644 --- a/test/docker_test.go +++ b/test/docker_test.go @@ -10,6 +10,7 @@ import ( C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing/common/debug" F "github.com/sagernet/sing/common/format" + "github.com/sagernet/sing/common/rw" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" @@ -71,7 +72,9 @@ func startDockerContainer(t *testing.T, options DockerOptions) { if len(options.Bind) > 0 { hostOptions.Binds = []string{} for path, internalPath := range options.Bind { - path = filepath.Join("config", path) + if !rw.FileExists(path) { + path = filepath.Join("config", path) + } path, _ = filepath.Abs(path) hostOptions.Binds = append(hostOptions.Binds, path+":"+internalPath) } diff --git a/test/go.mod b/test/go.mod index b4f00d4d..60aa3cf8 100644 --- a/test/go.mod +++ b/test/go.mod @@ -14,7 +14,7 @@ require ( github.com/sagernet/sing-shadowsocks v0.0.0-20220801112336-a91eacdd01e1 github.com/spyzhov/ajson v0.7.1 github.com/stretchr/testify v1.8.0 - golang.org/x/net v0.0.0-20220809012201-f428fae20770 + golang.org/x/net v0.0.0-20220809184613-07c6da5e1ced ) require ( diff --git a/test/go.sum b/test/go.sum index aa077f70..a8a44363 100644 --- a/test/go.sum +++ b/test/go.sum @@ -277,8 +277,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220809012201-f428fae20770 h1:dIi4qVdvjZEjiMDv7vhokAZNGnz3kepwuXqFKYDdDMs= -golang.org/x/net v0.0.0-20220809012201-f428fae20770/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20220809184613-07c6da5e1ced h1:3dYNDff0VT5xj+mbj2XucFst9WKk6PdGOrb9n+SbIvw= +golang.org/x/net v0.0.0-20220809184613-07c6da5e1ced/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= diff --git a/test/mkcert.go b/test/mkcert.go new file mode 100644 index 00000000..2d35ff84 --- /dev/null +++ b/test/mkcert.go @@ -0,0 +1,88 @@ +package main + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/sha1" + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "encoding/pem" + "math/big" + "os" + "path/filepath" + "testing" + "time" + + "github.com/sagernet/sing/common/rw" + + "github.com/stretchr/testify/require" +) + +func createSelfSignedCertificate(t *testing.T, domain string) (caPem, certPem, keyPem string) { + const userAndHostname = "sekai@nekohasekai.local" + tempDir, err := os.MkdirTemp("", "sing-box-test") + require.NoError(t, err) + t.Cleanup(func() { + os.RemoveAll(tempDir) + }) + caKey, err := rsa.GenerateKey(rand.Reader, 3072) + require.NoError(t, err) + spkiASN1, err := x509.MarshalPKIXPublicKey(caKey.Public()) + var spki struct { + Algorithm pkix.AlgorithmIdentifier + SubjectPublicKey asn1.BitString + } + _, err = asn1.Unmarshal(spkiASN1, &spki) + require.NoError(t, err) + skid := sha1.Sum(spki.SubjectPublicKey.Bytes) + caTpl := &x509.Certificate{ + SerialNumber: randomSerialNumber(t), + Subject: pkix.Name{ + Organization: []string{"sing-box test CA"}, + OrganizationalUnit: []string{userAndHostname}, + CommonName: "sing-box " + userAndHostname, + }, + SubjectKeyId: skid[:], + NotAfter: time.Now().AddDate(10, 0, 0), + NotBefore: time.Now(), + KeyUsage: x509.KeyUsageCertSign, + BasicConstraintsValid: true, + IsCA: true, + MaxPathLenZero: true, + } + caCert, err := x509.CreateCertificate(rand.Reader, caTpl, caTpl, caKey.Public(), caKey) + require.NoError(t, err) + err = rw.WriteFile(filepath.Join(tempDir, "ca.pem"), pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: caCert})) + require.NoError(t, err) + key, err := rsa.GenerateKey(rand.Reader, 2048) + domainTpl := &x509.Certificate{ + SerialNumber: randomSerialNumber(t), + Subject: pkix.Name{ + Organization: []string{"sing-box test certificate"}, + OrganizationalUnit: []string{"sing-box " + userAndHostname}, + }, + NotBefore: time.Now(), NotAfter: time.Now().AddDate(0, 0, 30), + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + } + domainTpl.DNSNames = append(domainTpl.DNSNames, domain) + cert, err := x509.CreateCertificate(rand.Reader, domainTpl, caTpl, key.Public(), caKey) + require.NoError(t, err) + certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert}) + privDER, err := x509.MarshalPKCS8PrivateKey(key) + require.NoError(t, err) + privPEM := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privDER}) + err = rw.WriteFile(filepath.Join(tempDir, domain+".pem"), certPEM) + require.NoError(t, err) + err = rw.WriteFile(filepath.Join(tempDir, domain+".key.pem"), privPEM) + require.NoError(t, err) + return filepath.Join(tempDir, "ca.pem"), filepath.Join(tempDir, domain+".pem"), filepath.Join(tempDir, domain+".key.pem") +} + +func randomSerialNumber(t *testing.T) *big.Int { + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + require.NoError(t, err) + return serialNumber +} diff --git a/test/naive_test.go b/test/naive_test.go new file mode 100644 index 00000000..e67b52b5 --- /dev/null +++ b/test/naive_test.go @@ -0,0 +1,104 @@ +package main + +import ( + "net/netip" + "testing" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common/auth" + "github.com/sagernet/sing/common/network" +) + +func TestNaiveInbound(t *testing.T) { + caPem, certPem, keyPem := createSelfSignedCertificate(t, "example.org") + startInstance(t, option.Options{ + Log: &option.LogOptions{ + Level: "error", + }, + Inbounds: []option.Inbound{ + { + Type: C.TypeNaive, + NaiveOptions: option.NaiveInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: option.ListenAddress(netip.IPv4Unspecified()), + ListenPort: serverPort, + }, + Users: []auth.User{ + { + Username: "sekai", + Password: "password", + }, + }, + Network: network.NetworkTCP, + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + }, + }, + }, + }, + }) + startDockerContainer(t, DockerOptions{ + Image: ImageNaive, + Ports: []uint16{serverPort, clientPort}, + Bind: map[string]string{ + "naive.json": "/etc/naiveproxy/config.json", + caPem: "/etc/naiveproxy/ca.pem", + }, + Env: []string{ + "SSL_CERT_FILE=/etc/naiveproxy/ca.pem", + }, + }) + testTCP(t, clientPort, testPort) +} + +func TestNaiveHTTP3Inbound(t *testing.T) { + if !C.QUIC_AVAILABLE { + t.Skip("QUIC not included") + } + caPem, certPem, keyPem := createSelfSignedCertificate(t, "example.org") + startInstance(t, option.Options{ + Log: &option.LogOptions{ + Level: "error", + }, + Inbounds: []option.Inbound{ + { + Type: C.TypeNaive, + NaiveOptions: option.NaiveInboundOptions{ + ListenOptions: option.ListenOptions{ + Listen: option.ListenAddress(netip.IPv4Unspecified()), + ListenPort: serverPort, + }, + Users: []auth.User{ + { + Username: "sekai", + Password: "password", + }, + }, + Network: network.NetworkUDP, + TLS: &option.InboundTLSOptions{ + Enabled: true, + ServerName: "example.org", + CertificatePath: certPem, + KeyPath: keyPem, + }, + }, + }, + }, + }) + startDockerContainer(t, DockerOptions{ + Image: ImageNaive, + Ports: []uint16{serverPort, clientPort}, + Bind: map[string]string{ + "naive-quic.json": "/etc/naiveproxy/config.json", + caPem: "/etc/naiveproxy/ca.pem", + }, + Env: []string{ + "SSL_CERT_FILE=/etc/naiveproxy/ca.pem", + }, + }) + testTCP(t, clientPort, testPort) +} diff --git a/test/trojan_test.go b/test/trojan_test.go index f185cc75..f9d8260a 100644 --- a/test/trojan_test.go +++ b/test/trojan_test.go @@ -9,13 +9,14 @@ import ( ) func TestTrojanOutbound(t *testing.T) { + _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") startDockerContainer(t, DockerOptions{ Image: ImageTrojan, Ports: []uint16{serverPort, testPort}, Bind: map[string]string{ - "trojan.json": "/config/config.json", - "example.org.pem": "/path/to/certificate.crt", - "example.org-key.pem": "/path/to/private.key", + "trojan.json": "/config/config.json", + certPem: "/path/to/certificate.crt", + keyPem: "/path/to/private.key", }, }) startInstance(t, option.Options{ @@ -45,7 +46,7 @@ func TestTrojanOutbound(t *testing.T) { TLSOptions: &option.OutboundTLSOptions{ Enabled: true, ServerName: "example.org", - CertificatePath: "config/example.org.pem", + CertificatePath: certPem, }, }, }, @@ -55,6 +56,7 @@ func TestTrojanOutbound(t *testing.T) { } func TestTrojanSelf(t *testing.T) { + _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") startInstance(t, option.Options{ Log: &option.LogOptions{ Level: "error", @@ -87,8 +89,8 @@ func TestTrojanSelf(t *testing.T) { TLS: &option.InboundTLSOptions{ Enabled: true, ServerName: "example.org", - CertificatePath: "config/example.org.pem", - KeyPath: "config/example.org-key.pem", + CertificatePath: certPem, + KeyPath: keyPem, }, }, }, @@ -109,7 +111,7 @@ func TestTrojanSelf(t *testing.T) { TLSOptions: &option.OutboundTLSOptions{ Enabled: true, ServerName: "example.org", - CertificatePath: "config/example.org.pem", + CertificatePath: certPem, }, }, },