Repository: XTLS/Xray-core

Issue:
`Expect: 100-continue` is not proper handled in http proxy
### Integrity requirements

- [x] I confirm that I have read the documentation, understand the meaning of all the configuration items I wrote, and did not pile up seemingly useful options or default values.
- [x] I provided the complete config and logs, rather than just providing the truncated parts based on my own judgment.
- [x] I searched issues and did not find any similar issues.
- [x] The problem can be successfully reproduced in the latest Release

### Description

Reproduce with `Xray 25.3.6 (Xray, Penetrates Everything.) 2cba2c4 (go1.24.1 windows/amd64)`
I notice this when use http_proxy with git and [GCM](https://github.com/git-ecosystem/git-credential-manager) try to do OAUTH2 with an local instance of gitea(auth will never complete)
Not sure, but maybe simple remove `Expect: 100-continue` from `proxy/http/server.go` and don't send it to server make it works(allow client to fallback as if the server not support 100-continue)

The http stream look like the below in the case(add `<-` to all response lines for better view), the http_proxy will only forward the first `HTTP/1.1 100 Continue` but not the latter `HTTP/1.1 400 Bad Request`( or 200 or others in really instance)
```
POST /login/oauth/access_token HTTP/1.1
Host: 127.0.0.1:3000
User-Agent: curl/8.12.1
Accept: */*
Content-Type: application/x-www-form-urlencoded
Content-Length: 2097152
Expect: 100-continue


<-HTTP/1.1 100 Continue


{req_body}
<-HTTP/1.1 400 Bad Request
<-Cache-Control: max-age=0, private, must-revalidate, no-transform
<-Content-Type: application/json;charset=utf-8
<-Set-Cookie: ...
<-Set-Cookie: ...
<-X-Frame-Options: SAMEORIGIN
<-Date: ...
<-Content-Length: 122
<-
<-{resp_body}
<-
```

### Reproduction Method

It can easily be reproduce with curl and an local instance of [gitea](https://github.com/go-gitea/gitea/releases)
`127.0.0.1`->freedom, `127.0.0.1:8888`->http inbound, no server required
gitea can be simple run gitea and do an first time init(http://127.0.0.1:3000/) with sqlite, and test with the below code
It can also be done with git bash for windows
```sh
# generate an file >= 1M and not too large to triggle `100-continue` with curl, but not too large as it may triggle CONN RST
# without http_proxy 
$ export http_proxy=
$ dd if=/dev/random of=/tmp/temp.dat bs=1M count=2
$ curl -X POST "http://127.0.0.1:3000/login/oauth/access_token" -H "Content-Type: application/x-www-form-urlencoded" -T /tmp/temp.dat
{"error":"unsupported_grant_type","error_description":"Only refresh_token or authorization_code grant type is supported"}
# with http_proxy 
$ export http_proxy=http://127.0.0.1:8888/
$ curl -X POST "http://127.0.0.1:3000/login/oauth/access_token" -H "Content-Type: application/x-www-form-urlencoded" -T /tmp/temp.dat
# no response and timeout after 1 min
curl: (52) Empty reply from server
```


### Client config

Not related, it can be reproduce by use http proxy to access 127.0.0.1(direct)

### Server config

N/A

### Client log

Not related

### Server log

N/A

Discussion:
@Fangliding: 尝试 #4547  我自己写的简单100 continue 服务器通过了测试

Solution patch:
diff --git a/app/dns/nameserver.go b/app/dns/nameserver.go
index 774d87c3..1a5ab3b6 100644
--- a/app/dns/nameserver.go
+++ b/app/dns/nameserver.go
@@ -30,13 +30,13 @@ type Client struct {
 	clientIP           net.IP
 	skipFallback       bool
 	domains            []string
-	expectIPs          []*router.GeoIPMatcher
+	expectedIPs        []*router.GeoIPMatcher
 	allowUnexpectedIPs bool
 	tag                string
 	timeoutMs          time.Duration
 }
 
-var errExpectedIPNonMatch = errors.New("expectIPs not match")
+var errExpectedIPNonMatch = errors.New("expectedIPs not match")
 
 // NewServer creates a name server object according to the network destination url.
 func NewServer(ctx context.Context, dest net.Destination, dispatcher routing.Dispatcher, queryStrategy QueryStrategy) (Server, error) {
@@ -165,18 +165,16 @@ func NewClient(
 			}
 		}
 
-		var timeoutMs time.Duration
+		var timeoutMs = 4000 * time.Millisecond
 		if ns.TimeoutMs > 0 {
 			timeoutMs = time.Duration(ns.TimeoutMs) * time.Millisecond
-		} else {
-			timeoutMs = 4000 * time.Millisecond
 		}
-
+		
 		client.server = server
 		client.clientIP = clientIP
 		client.skipFallback = ns.SkipFallback
 		client.domains = rules
-		client.expectIPs = matchers
+		client.expectedIPs = matchers
 		client.allowUnexpectedIPs = ns.AllowUnexpectedIPs
 		client.tag = ns.Tag
 		client.timeoutMs = timeoutMs
@@ -212,12 +210,12 @@ func (c *Client) QueryIP(ctx context.Context, domain string, option dns.IPOption
 
 // MatchExpectedIPs matches queried domain IPs with expected IPs and returns matched ones.
 func (c *Client) MatchExpectedIPs(domain string, ips []net.IP) ([]net.IP, error) {
-	if len(c.expectIPs) == 0 {
+	if len(c.expectedIPs) == 0 {
 		return ips, nil
 	}
 	newIps := []net.IP{}
 	for _, ip := range ips {
-		for _, matcher := range c.expectIPs {
+		for _, matcher := range c.expectedIPs {
 			if matcher.Match(ip) {
 				newIps = append(newIps, ip)
 				break
@@ -230,7 +228,7 @@ func (c *Client) MatchExpectedIPs(domain string, ips []net.IP) ([]net.IP, error)
 		}
 		return nil, errExpectedIPNonMatch
 	}
-	errors.LogDebug(context.Background(), "domain ", domain, " expectIPs ", newIps, " matched at server ", c.Name())
+	errors.LogDebug(context.Background(), "domain ", domain, " expectedIPs ", newIps, " matched at server ", c.Name())
 	return newIps, nil
 }
 
diff --git a/go.mod b/go.mod
index 456b3ab4..57bf8b99 100644
--- a/go.mod
+++ b/go.mod
@@ -28,7 +28,7 @@ require (
 	golang.org/x/sys v0.31.0
 	golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
 	google.golang.org/grpc v1.71.0
-	google.golang.org/protobuf v1.36.5
+	google.golang.org/protobuf v1.36.6
 	gvisor.dev/gvisor v0.0.0-20240320123526-dc6abceb7ff0
 	h12.io/socks v1.0.3
 	lukechampine.com/blake3 v1.4.0
diff --git a/go.sum b/go.sum
index a43edf73..aacd27ca 100644
--- a/go.sum
+++ b/go.sum
@@ -147,8 +147,8 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:
 google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
 google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
 google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
-google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
-google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
+google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/infra/conf/dns.go b/infra/conf/dns.go
index 20051447..607cbf07 100644
--- a/infra/conf/dns.go
+++ b/infra/conf/dns.go
@@ -17,7 +17,8 @@ type NameServerConfig struct {
 	Port               uint16     `json:"port"`
 	SkipFallback       bool       `json:"skipFallback"`
 	Domains            []string   `json:"domains"`
-	ExpectIPs          StringList `json:"expectIps"`
+	ExpectedIPs        StringList `json:"expectedIPs"`
+	ExpectIPs          StringList `json:"expectIPs"`
 	QueryStrategy      string     `json:"queryStrategy"`
 	AllowUnexpectedIPs bool       `json:"allowUnexpectedIps"`
 	Tag                string     `json:"tag"`
@@ -37,7 +38,8 @@ func (c *NameServerConfig) UnmarshalJSON(data []byte) error {
 		Port               uint16     `json:"port"`
 		SkipFallback       bool       `json:"skipFallback"`
 		Domains            []string   `json:"domains"`
-		ExpectIPs          StringList `json:"expectIps"`
+		ExpectedIPs        StringList `json:"expectedIPs"`
+		ExpectIPs          StringList `json:"expectIPs"`
 		QueryStrategy      string     `json:"queryStrategy"`
 		AllowUnexpectedIPs bool       `json:"allowUnexpectedIps"`
 		Tag                string     `json:"tag"`
@@ -49,6 +51,7 @@ func (c *NameServerConfig) UnmarshalJSON(data []byte) error {
 		c.Port = advanced.Port
 		c.SkipFallback = advanced.SkipFallback
 		c.Domains = advanced.Domains
+		c.ExpectedIPs = advanced.ExpectedIPs
 		c.ExpectIPs = advanced.ExpectIPs
 		c.QueryStrategy = advanced.QueryStrategy
 		c.AllowUnexpectedIPs = advanced.AllowUnexpectedIPs
@@ -101,9 +104,13 @@ func (c *NameServerConfig) Build() (*dns.NameServer, error) {
 		})
 	}
 
-	geoipList, err := ToCidrList(c.ExpectIPs)
+	var expectedIPs = c.ExpectedIPs
+	if len(expectedIPs) == 0 {
+		expectedIPs = c.ExpectIPs
+	}
+	geoipList, err := ToCidrList(expectedIPs)
 	if err != nil {
-		return nil, errors.New("invalid IP rule: ", c.ExpectIPs).Base(err)
+		return nil, errors.New("invalid IP rule: ", expectedIPs).Base(err)
 	}
 
 	var myClientIP []byte
diff --git a/proxy/http/server.go b/proxy/http/server.go
index 24708e69..3721dd47 100644
--- a/proxy/http/server.go
+++ b/proxy/http/server.go
@@ -294,7 +294,7 @@ func (s *Server) handlePlainHTTP(ctx context.Context, request *http.Request, wri
 
 	responseDone := func() error {
 		responseReader := bufio.NewReaderSize(&buf.BufferedReader{Reader: link.Reader}, buf.Size)
-		response, err := http.ReadResponse(responseReader, request)
+		response, err := readResponseAndHandle100Continue(responseReader, request, writer)
 		if err == nil {
 			http_proto.RemoveHopByHopHeaders(response.Header)
 			if response.ContentLength >= 0 {
@@ -338,6 +338,38 @@ func (s *Server) handlePlainHTTP(ctx context.Context, request *http.Request, wri
 	return result
 }
 
+// Sometimes, server might send 1xx response to client
+// it should not be processed by http proxy handler, just forward it to client
+func readResponseAndHandle100Continue(r *bufio.Reader, req *http.Request, writer io.Writer) (*http.Response, error) {
+	// have a little look of response
+	peekBytes, err := r.Peek(56)
+	if err == nil || err == bufio.ErrBufferFull {
+		str := string(peekBytes)
+		ResponseLine := strings.Split(str, "\r\n")[0]
+		_, status, _ := strings.Cut(ResponseLine, " ")
+		// only handle 1xx response
+		if strings.HasPrefix(status, "1") {
+			ResponseHeader1xx := []byte{}
+			// read until \r\n\r\n (end of http response header)
+			for {
+				data, err := r.ReadSlice('\n')
+				if err != nil {
+					return nil, errors.New("failed to read http 1xx response").Base(err)
+				}
+				ResponseHeader1xx = append(ResponseHeader1xx, data...)
+				if bytes.Equal(ResponseHeader1xx[len(ResponseHeader1xx)-4:], []byte{'\r', '\n', '\r', '\n'}) {
+					break
+				}
+				if len(ResponseHeader1xx) > 1024 {
+					return nil, errors.New("too big http 1xx response")
+				}
+			}
+			writer.Write(ResponseHeader1xx)
+		}
+	}
+	return http.ReadResponse(r, req)
+}
+
 func init() {
 	common.Must(common.RegisterConfig((*ServerConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
 		return NewServer(ctx, config.(*ServerConfig))



Test patch:
diff --git a/app/dns/dns_test.go b/app/dns/dns_test.go
index ab985384..4bdc9ae3 100644
--- a/app/dns/dns_test.go
+++ b/app/dns/dns_test.go
@@ -785,7 +785,7 @@ func TestLocalDomain(t *testing.T) {
 		}
 	}
 
-	{ // Will match dotless:localhost, but not expectIPs: 127.0.0.2, 127.0.0.3, then matches at dotless:
+	{ // Will match dotless:localhost, but not expectedIPs: 127.0.0.2, 127.0.0.3, then matches at dotless:
 		ips, _, err := client.LookupIP("localhost", feature_dns.IPOption{
 			IPv4Enable: true,
 			IPv6Enable: true,
@@ -800,7 +800,7 @@ func TestLocalDomain(t *testing.T) {
 		}
 	}
 
-	{ // Will match dotless:localhost, and expectIPs: 127.0.0.2, 127.0.0.3
+	{ // Will match dotless:localhost, and expectedIPs: 127.0.0.2, 127.0.0.3
 		ips, _, err := client.LookupIP("localhost-a", feature_dns.IPOption{
 			IPv4Enable: true,
 			IPv6Enable: true,
@@ -815,7 +815,7 @@ func TestLocalDomain(t *testing.T) {
 		}
 	}
 
-	{ // Will match dotless:localhost, and expectIPs: 127.0.0.2, 127.0.0.3
+	{ // Will match dotless:localhost, and expectedIPs: 127.0.0.2, 127.0.0.3
 		ips, _, err := client.LookupIP("localhost-b", feature_dns.IPOption{
 			IPv4Enable: true,
 			IPv6Enable: true,