From 7bf815b77edadef03dbf6c0bdfc324153b90414b Mon Sep 17 00:00:00 2001 From: YEVHENII SHCHERBINA Date: Fri, 19 Dec 2025 21:50:56 +0000 Subject: [PATCH 1/6] feat: implementation of non-transparent proxy --- proxy/proxy.go | 246 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 246 insertions(+) diff --git a/proxy/proxy.go b/proxy/proxy.go index 86f9e2e..47cd7b2 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -218,6 +218,11 @@ func (p *Server) handleHTTPConnection(conn net.Conn) { return } + if req.Method == http.MethodConnect { + p.handleCONNECT(conn, req) + return + } + p.logger.Debug("🌐 HTTP Request: %s %s", req.Method, req.URL.String()) p.processHTTPRequest(conn, req, false) } @@ -423,6 +428,247 @@ For more help: https://github.com/coder/boundary p.logger.Debug("Successfully wrote to connection") } +// handleCONNECT handles HTTP CONNECT requests for tunneling +func (p *Server) handleCONNECT(conn net.Conn, req *http.Request) { + // Extract target from CONNECT request + // CONNECT requests have the target in req.Host (format: hostname:port) + target := req.Host + if target == "" { + target = req.URL.Host + } + + p.logger.Debug("🔌 CONNECT request", "target", target) + + // Check if target is allowed + // Use "CONNECT" as method and target as the URL for evaluation + result := p.ruleEngine.Evaluate("CONNECT", target) + + // Audit the CONNECT request + p.auditor.AuditRequest(audit.Request{ + Method: "CONNECT", + URL: target, + Host: target, + Allowed: result.Allowed, + Rule: result.Rule, + }) + + if !result.Allowed { + p.logger.Debug("CONNECT request blocked", "target", target) + p.writeBlockedCONNECTResponse(conn, target) + return + } + + // Send 200 Connection established response + response := "HTTP/1.1 200 Connection established\r\n\r\n" + _, err := conn.Write([]byte(response)) + if err != nil { + p.logger.Error("Failed to send CONNECT response", "error", err) + return + } + + p.logger.Debug("CONNECT tunnel established", "target", target) + + // Handle the tunnel - decrypt TLS and process each HTTP request + p.handleCONNECTTunnel(conn, target) +} + +// handleCONNECTTunnel handles the tunnel after CONNECT is established +// It decrypts TLS traffic and processes each HTTP request separately +func (p *Server) handleCONNECTTunnel(conn net.Conn, target string) { + defer func() { + err := conn.Close() + if err != nil { + p.logger.Error("Failed to close CONNECT tunnel", "error", err) + } + }() + + // Wrap connection with TLS server to decrypt traffic + tlsConn := tls.Server(conn, p.tlsConfig) + + // Perform TLS handshake + if err := tlsConn.Handshake(); err != nil { + p.logger.Error("TLS handshake failed in CONNECT tunnel", "error", err) + return + } + + p.logger.Debug("✅ TLS handshake successful in CONNECT tunnel") + + // Process HTTP requests in a loop + reader := bufio.NewReader(tlsConn) + for { + // Read HTTP request from tunnel + req, err := http.ReadRequest(reader) + if err != nil { + if err == io.EOF { + p.logger.Debug("CONNECT tunnel closed by client") + break + } + p.logger.Error("Failed to read HTTP request from CONNECT tunnel", "error", err) + break + } + + p.logger.Debug("🔒 HTTP Request in CONNECT tunnel", "method", req.Method, "url", req.URL.String(), "target", target) + + // Process this request - check if allowed and forward to target + p.processTunnelRequest(tlsConn, req, target) + } +} + +// processTunnelRequest processes a single HTTP request from the CONNECT tunnel +func (p *Server) processTunnelRequest(conn net.Conn, req *http.Request, targetHost string) { + // Check if request should be allowed + // Use the original request URL but evaluate against rules + urlStr := req.Host + req.URL.String() + result := p.ruleEngine.Evaluate(req.Method, urlStr) + + // Audit the request + p.auditor.AuditRequest(audit.Request{ + Method: req.Method, + URL: req.URL.String(), + Host: req.Host, + Allowed: result.Allowed, + Rule: result.Rule, + }) + + if !result.Allowed { + p.logger.Debug("Request in CONNECT tunnel blocked", "method", req.Method, "url", urlStr) + p.writeBlockedResponse(conn, req) + return + } + + // Forward request to target + // The target is the original CONNECT target, but we use the request's host/path + p.forwardTunnelRequest(conn, req, targetHost) +} + +// forwardTunnelRequest forwards a request from the tunnel to the target +func (p *Server) forwardTunnelRequest(conn net.Conn, req *http.Request, targetHost string) { + // Create HTTP client + client := &http.Client{ + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse // Don't follow redirects + }, + } + + // Parse target host to get hostname and port + hostname := targetHost + port := "443" // Default HTTPS port + if strings.Contains(targetHost, ":") { + parts := strings.Split(targetHost, ":") + hostname = parts[0] + port = parts[1] + } + + // Determine scheme based on port + scheme := "https" + if port == "80" { + scheme = "http" + } + + // Build target URL using the request's path but the CONNECT target's host + targetURL := &url.URL{ + Scheme: scheme, + Host: targetHost, + Path: req.URL.Path, + RawQuery: req.URL.RawQuery, + } + + var body = req.Body + if req.Method == http.MethodGet || req.Method == http.MethodHead { + body = nil + } + + newReq, err := http.NewRequest(req.Method, targetURL.String(), body) + if err != nil { + p.logger.Error("can't create HTTP request for tunnel", "error", err) + return + } + + // Copy headers + for name, values := range req.Header { + // Skip connection-specific headers + if strings.ToLower(name) == "connection" || strings.ToLower(name) == "proxy-connection" { + continue + } + for _, value := range values { + newReq.Header.Add(name, value) + } + } + + // Make request to destination + resp, err := client.Do(newReq) + if err != nil { + p.logger.Error("Failed to forward request from CONNECT tunnel", "error", err) + return + } + + p.logger.Debug("Response from target", "status", resp.StatusCode, "target", targetHost) + + // Read the body and set Content-Length + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + p.logger.Error("can't read response body from tunnel", "error", err) + return + } + resp.Header.Set("Content-Length", strconv.Itoa(len(bodyBytes))) + resp.ContentLength = int64(len(bodyBytes)) + err = resp.Body.Close() + if err != nil { + p.logger.Error("Failed to close response body", "error", err) + return + } + resp.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) + + // Normalize to HTTP/1.1 + resp.Proto = "HTTP/1.1" + resp.ProtoMajor = 1 + resp.ProtoMinor = 1 + + // Write response back to tunnel + err = resp.Write(conn) + if err != nil { + p.logger.Error("Failed to write response to CONNECT tunnel", "error", err) + return + } + + p.logger.Debug("Successfully forwarded response in CONNECT tunnel") +} + +// writeBlockedCONNECTResponse writes a blocked response for CONNECT requests +func (p *Server) writeBlockedCONNECTResponse(conn net.Conn, target string) { + resp := &http.Response{ + Status: "403 Forbidden", + StatusCode: http.StatusForbidden, + Proto: "HTTP/1.1", + ProtoMajor: 1, + ProtoMinor: 1, + Header: make(http.Header), + Body: nil, + ContentLength: 0, + } + + resp.Header.Set("Content-Type", "text/plain") + + body := fmt.Sprintf(`🚫 CONNECT Request Blocked by Boundary + +Target: %s + +To allow this CONNECT request, restart boundary with: + --allow "domain=%s" + +For more help: https://github.com/coder/boundary +`, target, target) + + resp.Body = io.NopCloser(strings.NewReader(body)) + resp.ContentLength = int64(len(body)) + + err := resp.Write(conn) + if err != nil { + p.logger.Error("Failed to write blocked CONNECT response", "error", err) + return + } +} + // connectionWrapper lets us "unread" the peeked byte type connectionWrapper struct { net.Conn From 37a1861b683e516dce2c0b24d126e6068f355753 Mon Sep 17 00:00:00 2001 From: YEVHENII SHCHERBINA Date: Sat, 20 Dec 2025 16:05:32 +0000 Subject: [PATCH 2/6] fix: minor fix --- proxy/connect.go | 254 +++++++++++++++++++++++++++++++++++++++++++++++ proxy/proxy.go | 241 -------------------------------------------- 2 files changed, 254 insertions(+), 241 deletions(-) create mode 100644 proxy/connect.go diff --git a/proxy/connect.go b/proxy/connect.go new file mode 100644 index 0000000..2acf78f --- /dev/null +++ b/proxy/connect.go @@ -0,0 +1,254 @@ +package proxy + +import ( + "bufio" + "bytes" + "crypto/tls" + "fmt" + "io" + "net" + "net/http" + "net/url" + "strconv" + "strings" + + "github.com/coder/boundary/audit" +) + +// handleCONNECT handles HTTP CONNECT requests for tunneling +func (p *Server) handleCONNECT(conn net.Conn, req *http.Request) { + // Extract target from CONNECT request + // CONNECT requests have the target in req.Host (format: hostname:port) + target := req.Host + if target == "" { + target = req.URL.Host + } + + p.logger.Debug("🔌 CONNECT request", "target", target) + + // Check if target is allowed + // Use "CONNECT" as method and target as the URL for evaluation + result := p.ruleEngine.Evaluate("CONNECT", target) + + // Audit the CONNECT request + p.auditor.AuditRequest(audit.Request{ + Method: "CONNECT", + URL: target, + Host: target, + Allowed: result.Allowed, + Rule: result.Rule, + }) + + if !result.Allowed { + p.logger.Debug("CONNECT request blocked", "target", target) + p.writeBlockedCONNECTResponse(conn, target) + return + } + + // Send 200 Connection established response + response := "HTTP/1.1 200 Connection established\r\n\r\n" + _, err := conn.Write([]byte(response)) + if err != nil { + p.logger.Error("Failed to send CONNECT response", "error", err) + return + } + + p.logger.Debug("CONNECT tunnel established", "target", target) + + // Handle the tunnel - decrypt TLS and process each HTTP request + p.handleCONNECTTunnel(conn, target) +} + +// handleCONNECTTunnel handles the tunnel after CONNECT is established +// It decrypts TLS traffic and processes each HTTP request separately +func (p *Server) handleCONNECTTunnel(conn net.Conn, target string) { + defer func() { + err := conn.Close() + if err != nil { + p.logger.Error("Failed to close CONNECT tunnel", "error", err) + } + }() + + // Wrap connection with TLS server to decrypt traffic + tlsConn := tls.Server(conn, p.tlsConfig) + + // Perform TLS handshake + if err := tlsConn.Handshake(); err != nil { + p.logger.Error("TLS handshake failed in CONNECT tunnel", "error", err) + return + } + + p.logger.Debug("✅ TLS handshake successful in CONNECT tunnel") + + // Process HTTP requests in a loop + reader := bufio.NewReader(tlsConn) + for { + // Read HTTP request from tunnel + req, err := http.ReadRequest(reader) + if err != nil { + if err == io.EOF { + p.logger.Debug("CONNECT tunnel closed by client") + break + } + p.logger.Error("Failed to read HTTP request from CONNECT tunnel", "error", err) + break + } + + p.logger.Debug("🔒 HTTP Request in CONNECT tunnel", "method", req.Method, "url", req.URL.String(), "target", target) + + // Process this request - check if allowed and forward to target + p.processTunnelRequest(tlsConn, req, target) + } +} + +// processTunnelRequest processes a single HTTP request from the CONNECT tunnel +func (p *Server) processTunnelRequest(conn net.Conn, req *http.Request, targetHost string) { + // Check if request should be allowed + // Use the original request URL but evaluate against rules + urlStr := req.Host + req.URL.String() + result := p.ruleEngine.Evaluate(req.Method, urlStr) + + // Audit the request + p.auditor.AuditRequest(audit.Request{ + Method: req.Method, + URL: req.URL.String(), + Host: req.Host, + Allowed: result.Allowed, + Rule: result.Rule, + }) + + if !result.Allowed { + p.logger.Debug("Request in CONNECT tunnel blocked", "method", req.Method, "url", urlStr) + p.writeBlockedResponse(conn, req) + return + } + + // Forward request to target + // The target is the original CONNECT target, but we use the request's host/path + p.forwardTunnelRequest(conn, req, targetHost) +} + +// forwardTunnelRequest forwards a request from the tunnel to the target +func (p *Server) forwardTunnelRequest(conn net.Conn, req *http.Request, targetHost string) { + // Create HTTP client + client := &http.Client{ + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse // Don't follow redirects + }, + } + + // Determine scheme based on port + port := "443" // Default HTTPS port + if strings.Contains(targetHost, ":") { + parts := strings.Split(targetHost, ":") + port = parts[1] + } + + scheme := "https" + if port == "80" { + scheme = "http" + } + + // Build target URL using the request's path but the CONNECT target's host + targetURL := &url.URL{ + Scheme: scheme, + Host: targetHost, + Path: req.URL.Path, + RawQuery: req.URL.RawQuery, + } + + var body = req.Body + if req.Method == http.MethodGet || req.Method == http.MethodHead { + body = nil + } + + newReq, err := http.NewRequest(req.Method, targetURL.String(), body) + if err != nil { + p.logger.Error("can't create HTTP request for tunnel", "error", err) + return + } + + // Copy headers + for name, values := range req.Header { + // Skip connection-specific headers + if strings.ToLower(name) == "connection" || strings.ToLower(name) == "proxy-connection" { + continue + } + for _, value := range values { + newReq.Header.Add(name, value) + } + } + + // Make request to destination + resp, err := client.Do(newReq) + if err != nil { + p.logger.Error("Failed to forward request from CONNECT tunnel", "error", err) + return + } + + p.logger.Debug("Response from target", "status", resp.StatusCode, "target", targetHost) + + // Read the body and set Content-Length + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + p.logger.Error("can't read response body from tunnel", "error", err) + return + } + resp.Header.Set("Content-Length", strconv.Itoa(len(bodyBytes))) + resp.ContentLength = int64(len(bodyBytes)) + err = resp.Body.Close() + if err != nil { + p.logger.Error("Failed to close response body", "error", err) + return + } + resp.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) + + // Normalize to HTTP/1.1 + resp.Proto = "HTTP/1.1" + resp.ProtoMajor = 1 + resp.ProtoMinor = 1 + + // Write response back to tunnel + err = resp.Write(conn) + if err != nil { + p.logger.Error("Failed to write response to CONNECT tunnel", "error", err) + return + } + + p.logger.Debug("Successfully forwarded response in CONNECT tunnel") +} + +// writeBlockedCONNECTResponse writes a blocked response for CONNECT requests +func (p *Server) writeBlockedCONNECTResponse(conn net.Conn, target string) { + resp := &http.Response{ + Status: "403 Forbidden", + StatusCode: http.StatusForbidden, + Proto: "HTTP/1.1", + ProtoMajor: 1, + ProtoMinor: 1, + Header: make(http.Header), + Body: nil, + ContentLength: 0, + } + + resp.Header.Set("Content-Type", "text/plain") + + body := fmt.Sprintf(`🚫 CONNECT Request Blocked by Boundary + +Target: %s + +To allow this CONNECT request, restart boundary with: + --allow "domain=%s" + +For more help: https://github.com/coder/boundary +`, target, target) + + resp.Body = io.NopCloser(strings.NewReader(body)) + resp.ContentLength = int64(len(body)) + + err := resp.Write(conn) + if err != nil { + p.logger.Error("Failed to write blocked CONNECT response", "error", err) + return + } +} diff --git a/proxy/proxy.go b/proxy/proxy.go index 47cd7b2..9be9b5e 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -428,247 +428,6 @@ For more help: https://github.com/coder/boundary p.logger.Debug("Successfully wrote to connection") } -// handleCONNECT handles HTTP CONNECT requests for tunneling -func (p *Server) handleCONNECT(conn net.Conn, req *http.Request) { - // Extract target from CONNECT request - // CONNECT requests have the target in req.Host (format: hostname:port) - target := req.Host - if target == "" { - target = req.URL.Host - } - - p.logger.Debug("🔌 CONNECT request", "target", target) - - // Check if target is allowed - // Use "CONNECT" as method and target as the URL for evaluation - result := p.ruleEngine.Evaluate("CONNECT", target) - - // Audit the CONNECT request - p.auditor.AuditRequest(audit.Request{ - Method: "CONNECT", - URL: target, - Host: target, - Allowed: result.Allowed, - Rule: result.Rule, - }) - - if !result.Allowed { - p.logger.Debug("CONNECT request blocked", "target", target) - p.writeBlockedCONNECTResponse(conn, target) - return - } - - // Send 200 Connection established response - response := "HTTP/1.1 200 Connection established\r\n\r\n" - _, err := conn.Write([]byte(response)) - if err != nil { - p.logger.Error("Failed to send CONNECT response", "error", err) - return - } - - p.logger.Debug("CONNECT tunnel established", "target", target) - - // Handle the tunnel - decrypt TLS and process each HTTP request - p.handleCONNECTTunnel(conn, target) -} - -// handleCONNECTTunnel handles the tunnel after CONNECT is established -// It decrypts TLS traffic and processes each HTTP request separately -func (p *Server) handleCONNECTTunnel(conn net.Conn, target string) { - defer func() { - err := conn.Close() - if err != nil { - p.logger.Error("Failed to close CONNECT tunnel", "error", err) - } - }() - - // Wrap connection with TLS server to decrypt traffic - tlsConn := tls.Server(conn, p.tlsConfig) - - // Perform TLS handshake - if err := tlsConn.Handshake(); err != nil { - p.logger.Error("TLS handshake failed in CONNECT tunnel", "error", err) - return - } - - p.logger.Debug("✅ TLS handshake successful in CONNECT tunnel") - - // Process HTTP requests in a loop - reader := bufio.NewReader(tlsConn) - for { - // Read HTTP request from tunnel - req, err := http.ReadRequest(reader) - if err != nil { - if err == io.EOF { - p.logger.Debug("CONNECT tunnel closed by client") - break - } - p.logger.Error("Failed to read HTTP request from CONNECT tunnel", "error", err) - break - } - - p.logger.Debug("🔒 HTTP Request in CONNECT tunnel", "method", req.Method, "url", req.URL.String(), "target", target) - - // Process this request - check if allowed and forward to target - p.processTunnelRequest(tlsConn, req, target) - } -} - -// processTunnelRequest processes a single HTTP request from the CONNECT tunnel -func (p *Server) processTunnelRequest(conn net.Conn, req *http.Request, targetHost string) { - // Check if request should be allowed - // Use the original request URL but evaluate against rules - urlStr := req.Host + req.URL.String() - result := p.ruleEngine.Evaluate(req.Method, urlStr) - - // Audit the request - p.auditor.AuditRequest(audit.Request{ - Method: req.Method, - URL: req.URL.String(), - Host: req.Host, - Allowed: result.Allowed, - Rule: result.Rule, - }) - - if !result.Allowed { - p.logger.Debug("Request in CONNECT tunnel blocked", "method", req.Method, "url", urlStr) - p.writeBlockedResponse(conn, req) - return - } - - // Forward request to target - // The target is the original CONNECT target, but we use the request's host/path - p.forwardTunnelRequest(conn, req, targetHost) -} - -// forwardTunnelRequest forwards a request from the tunnel to the target -func (p *Server) forwardTunnelRequest(conn net.Conn, req *http.Request, targetHost string) { - // Create HTTP client - client := &http.Client{ - CheckRedirect: func(req *http.Request, via []*http.Request) error { - return http.ErrUseLastResponse // Don't follow redirects - }, - } - - // Parse target host to get hostname and port - hostname := targetHost - port := "443" // Default HTTPS port - if strings.Contains(targetHost, ":") { - parts := strings.Split(targetHost, ":") - hostname = parts[0] - port = parts[1] - } - - // Determine scheme based on port - scheme := "https" - if port == "80" { - scheme = "http" - } - - // Build target URL using the request's path but the CONNECT target's host - targetURL := &url.URL{ - Scheme: scheme, - Host: targetHost, - Path: req.URL.Path, - RawQuery: req.URL.RawQuery, - } - - var body = req.Body - if req.Method == http.MethodGet || req.Method == http.MethodHead { - body = nil - } - - newReq, err := http.NewRequest(req.Method, targetURL.String(), body) - if err != nil { - p.logger.Error("can't create HTTP request for tunnel", "error", err) - return - } - - // Copy headers - for name, values := range req.Header { - // Skip connection-specific headers - if strings.ToLower(name) == "connection" || strings.ToLower(name) == "proxy-connection" { - continue - } - for _, value := range values { - newReq.Header.Add(name, value) - } - } - - // Make request to destination - resp, err := client.Do(newReq) - if err != nil { - p.logger.Error("Failed to forward request from CONNECT tunnel", "error", err) - return - } - - p.logger.Debug("Response from target", "status", resp.StatusCode, "target", targetHost) - - // Read the body and set Content-Length - bodyBytes, err := io.ReadAll(resp.Body) - if err != nil { - p.logger.Error("can't read response body from tunnel", "error", err) - return - } - resp.Header.Set("Content-Length", strconv.Itoa(len(bodyBytes))) - resp.ContentLength = int64(len(bodyBytes)) - err = resp.Body.Close() - if err != nil { - p.logger.Error("Failed to close response body", "error", err) - return - } - resp.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) - - // Normalize to HTTP/1.1 - resp.Proto = "HTTP/1.1" - resp.ProtoMajor = 1 - resp.ProtoMinor = 1 - - // Write response back to tunnel - err = resp.Write(conn) - if err != nil { - p.logger.Error("Failed to write response to CONNECT tunnel", "error", err) - return - } - - p.logger.Debug("Successfully forwarded response in CONNECT tunnel") -} - -// writeBlockedCONNECTResponse writes a blocked response for CONNECT requests -func (p *Server) writeBlockedCONNECTResponse(conn net.Conn, target string) { - resp := &http.Response{ - Status: "403 Forbidden", - StatusCode: http.StatusForbidden, - Proto: "HTTP/1.1", - ProtoMajor: 1, - ProtoMinor: 1, - Header: make(http.Header), - Body: nil, - ContentLength: 0, - } - - resp.Header.Set("Content-Type", "text/plain") - - body := fmt.Sprintf(`🚫 CONNECT Request Blocked by Boundary - -Target: %s - -To allow this CONNECT request, restart boundary with: - --allow "domain=%s" - -For more help: https://github.com/coder/boundary -`, target, target) - - resp.Body = io.NopCloser(strings.NewReader(body)) - resp.ContentLength = int64(len(body)) - - err := resp.Write(conn) - if err != nil { - p.logger.Error("Failed to write blocked CONNECT response", "error", err) - return - } -} - // connectionWrapper lets us "unread" the peeked byte type connectionWrapper struct { net.Conn From 2021471152ed03f25af21b2098313927d4f6c05f Mon Sep 17 00:00:00 2001 From: YEVHENII SHCHERBINA Date: Sat, 20 Dec 2025 17:45:46 +0000 Subject: [PATCH 3/6] fix: minor fix --- proxy/connect.go | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/proxy/connect.go b/proxy/connect.go index 2acf78f..356577d 100644 --- a/proxy/connect.go +++ b/proxy/connect.go @@ -137,10 +137,12 @@ func (p *Server) forwardTunnelRequest(conn net.Conn, req *http.Request, targetHo }, } - // Determine scheme based on port + // Extract hostname and port from targetHost + hostname := targetHost port := "443" // Default HTTPS port if strings.Contains(targetHost, ":") { parts := strings.Split(targetHost, ":") + hostname = parts[0] port = parts[1] } @@ -150,9 +152,10 @@ func (p *Server) forwardTunnelRequest(conn net.Conn, req *http.Request, targetHo } // Build target URL using the request's path but the CONNECT target's host + // URL.Host can include port for connection, but Host header should not targetURL := &url.URL{ Scheme: scheme, - Host: targetHost, + Host: targetHost, // Include port for connection Path: req.URL.Path, RawQuery: req.URL.RawQuery, } @@ -168,10 +171,15 @@ func (p *Server) forwardTunnelRequest(conn net.Conn, req *http.Request, targetHo return } - // Copy headers + // Set Host header to just the hostname (without port) + // The Host header should not include the port number for HTTPS + newReq.Host = hostname + + // Copy headers (but skip Host since we set it explicitly above) for name, values := range req.Header { - // Skip connection-specific headers - if strings.ToLower(name) == "connection" || strings.ToLower(name) == "proxy-connection" { + // Skip connection-specific headers and Host header + lowerName := strings.ToLower(name) + if lowerName == "connection" || lowerName == "proxy-connection" || lowerName == "host" { continue } for _, value := range values { From 1c5c45cab11c9023af4f0d073d251e5894173b60 Mon Sep 17 00:00:00 2001 From: YEVHENII SHCHERBINA Date: Sat, 20 Dec 2025 18:01:01 +0000 Subject: [PATCH 4/6] test: uncomment CONNECT tests --- e2e_tests/landjail/landjail_test.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/e2e_tests/landjail/landjail_test.go b/e2e_tests/landjail/landjail_test.go index 900a85e..5d6a975 100644 --- a/e2e_tests/landjail/landjail_test.go +++ b/e2e_tests/landjail/landjail_test.go @@ -29,11 +29,11 @@ func TestLandjail(t *testing.T) { }) // Test allowed HTTPS request - // t.Run("HTTPSRequestThroughBoundary", func(t *testing.T) { - // expectedResponse := `{"message":"👋"} - //` - // lt.ExpectAllowed("https://dev.coder.com/api/v2", expectedResponse) - // }) + t.Run("HTTPSRequestThroughBoundary", func(t *testing.T) { + expectedResponse := `{"message":"👋"} +` + lt.ExpectAllowed("https://dev.coder.com/api/v2", expectedResponse) + }) // Test blocked HTTP request t.Run("HTTPBlockedDomainTest", func(t *testing.T) { @@ -41,7 +41,7 @@ func TestLandjail(t *testing.T) { }) // Test blocked HTTPS request - //t.Run("HTTPSBlockedDomainTest", func(t *testing.T) { - // lt.ExpectDeny("https://example.com") - //}) + t.Run("HTTPSBlockedDomainTest", func(t *testing.T) { + lt.ExpectDeny("https://example.com") + }) } From 45cd657565a35403f60a98d2f2108731b0f07aaa Mon Sep 17 00:00:00 2001 From: YEVHENII SHCHERBINA Date: Sat, 20 Dec 2025 19:31:22 +0000 Subject: [PATCH 5/6] fix: remove unnecessary check for CONNECT domain --- proxy/connect.go | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/proxy/connect.go b/proxy/connect.go index 356577d..842e6b2 100644 --- a/proxy/connect.go +++ b/proxy/connect.go @@ -26,25 +26,6 @@ func (p *Server) handleCONNECT(conn net.Conn, req *http.Request) { p.logger.Debug("🔌 CONNECT request", "target", target) - // Check if target is allowed - // Use "CONNECT" as method and target as the URL for evaluation - result := p.ruleEngine.Evaluate("CONNECT", target) - - // Audit the CONNECT request - p.auditor.AuditRequest(audit.Request{ - Method: "CONNECT", - URL: target, - Host: target, - Allowed: result.Allowed, - Rule: result.Rule, - }) - - if !result.Allowed { - p.logger.Debug("CONNECT request blocked", "target", target) - p.writeBlockedCONNECTResponse(conn, target) - return - } - // Send 200 Connection established response response := "HTTP/1.1 200 Connection established\r\n\r\n" _, err := conn.Write([]byte(response)) From b991172a4a1b572d7f004b7429f87cc2326a9491 Mon Sep 17 00:00:00 2001 From: YEVHENII SHCHERBINA Date: Sat, 20 Dec 2025 19:42:01 +0000 Subject: [PATCH 6/6] fix: remove unnecessary code --- proxy/connect.go | 36 ------------------------------------ 1 file changed, 36 deletions(-) diff --git a/proxy/connect.go b/proxy/connect.go index 842e6b2..da4ce08 100644 --- a/proxy/connect.go +++ b/proxy/connect.go @@ -4,7 +4,6 @@ import ( "bufio" "bytes" "crypto/tls" - "fmt" "io" "net" "net/http" @@ -206,38 +205,3 @@ func (p *Server) forwardTunnelRequest(conn net.Conn, req *http.Request, targetHo p.logger.Debug("Successfully forwarded response in CONNECT tunnel") } - -// writeBlockedCONNECTResponse writes a blocked response for CONNECT requests -func (p *Server) writeBlockedCONNECTResponse(conn net.Conn, target string) { - resp := &http.Response{ - Status: "403 Forbidden", - StatusCode: http.StatusForbidden, - Proto: "HTTP/1.1", - ProtoMajor: 1, - ProtoMinor: 1, - Header: make(http.Header), - Body: nil, - ContentLength: 0, - } - - resp.Header.Set("Content-Type", "text/plain") - - body := fmt.Sprintf(`🚫 CONNECT Request Blocked by Boundary - -Target: %s - -To allow this CONNECT request, restart boundary with: - --allow "domain=%s" - -For more help: https://github.com/coder/boundary -`, target, target) - - resp.Body = io.NopCloser(strings.NewReader(body)) - resp.ContentLength = int64(len(body)) - - err := resp.Write(conn) - if err != nil { - p.logger.Error("Failed to write blocked CONNECT response", "error", err) - return - } -}