From 51d67a4ee8f9baa63a74ab48e1470d1d01c382c4 Mon Sep 17 00:00:00 2001 From: Rath Rene Date: Sun, 1 Oct 2023 23:47:50 +0200 Subject: [PATCH] rejection http-response, ca certs --- config_example.yml | 9 +- docs/source/info/modes.rst | 199 +++++++++++++++++++++++++++++++++++ docs/source/info/rules.rst | 2 + lib/cnf/cnf_file/validate.go | 5 + lib/cnf/config.go | 1 + lib/main/service.go | 5 - lib/proc/fwd/http.go | 43 ++++++-- lib/proc/fwd/main.go | 23 ++-- lib/proc/parse/conn_log.go | 9 +- lib/proc/parse/main.go | 12 ++- lib/rcv/http.go | 22 ++-- lib/u/helpers.go | 39 +++++++ 12 files changed, 329 insertions(+), 40 deletions(-) create mode 100644 docs/source/info/modes.rst diff --git a/config_example.yml b/config_example.yml index 140afc0..39c43c6 100644 --- a/config_example.yml +++ b/config_example.yml @@ -11,14 +11,15 @@ service: tproxy: false - mode: 'proxyproto' port: 4129 - # - mode: 'http' # not yet implemented - # port: 4130 - # - mode: 'https' # not yet implemented - # port: 4131 + - mode: 'http' + port: 4130 + - mode: 'https' # not yet implemented + port: 4131 # - mode: 'socks5' # not yet implemented # port: 4132 certs: + caPublic: '/tmp/calamary.ca.crt' serverPublic: '/tmp/calamary.crt' serverPrivate: '/tmp/calamary.key' interceptPublic: '/tmp/calamary.subca.crt' diff --git a/docs/source/info/modes.rst b/docs/source/info/modes.rst new file mode 100644 index 0000000..41e424e --- /dev/null +++ b/docs/source/info/modes.rst @@ -0,0 +1,199 @@ +.. _modes: + +.. include:: ../_inc/head.rst + +.. include:: ../_inc/in_progress.rst + +##### +Modes +##### + +Transparent +########### + +Behaviour +========= + +DNAT - TCP (plaintext) +---------------------- + +**Server** + +.. code-block:: bash + + 2023-10-01 23:43:01 | INFO | 192.168.11.104 => 135.181.170.219:80 | Accept + + +**Client** + +.. code-block:: bash + + curl http://superstes.eu -v + * Trying 135.181.170.219:80... + * Connected to superstes.eu (135.181.170.219) port 80 (#0) + > GET / HTTP/1.1 + > Host: superstes.eu + ... + < + + 301 Moved Permanently + +

301 Moved Permanently

+
nginx
+ + + * Connection #0 to host superstes.eu left intact + + +DNAT - TLS +---------- + +**Server** + +.. code-block:: bash + + 2023-10-01 23:43:09 | INFO | 192.168.11.104 => 135.181.170.219:443 | Accept + + +**Client** + +.. code-block:: bash + + host@calamary$ curl https://superstes.eu -v + + * Trying 135.181.170.219:443... + * Connected to superstes.eu (135.181.170.219) port 443 (#0) + ... + < HTTP/2 302 + < server: nginx + ... + < + + 302 Found + +

302 Found

+
nginx
+ + + * Connection #0 to host superstes.eu left intact + + +HTTP Proxy +########## + +Behaviour +========= + +HTTP +---- + +**Server** + +.. code-block:: bash + + 2023-10-01 23:40:34 | INFO | 127.0.0.1 => 135.181.170.219:80 | Accept + + +**Client** + +.. code-block:: bash + + host@calamary$ http_proxy=http://localhost:4130 curl http://superstes.eu -v + + * Uses proxy env variable http_proxy == 'http://localhost:4130' + * Trying 127.0.0.1:4130... + * Connected to (nil) (127.0.0.1) port 4130 (#0) + > GET http://superstes.eu/ HTTP/1.1 + > Host: superstes.eu + > User-Agent: curl/7.81.0 + > Accept: */* + > Proxy-Connection: Keep-Alive + > + ... + < + + 301 Moved Permanently + +

301 Moved Permanently

+
nginx
+ + + * Connection #0 to host (nil) left intact + +HTTPS +----- + +**Server** + +.. code-block:: bash + + 2023-10-01 23:40:43 | INFO | 127.0.0.1 => 135.181.170.219:443 | Accept + + +**Client** + +.. code-block:: bash + + host@calamary$ https_proxy=http://localhost:4130 curl https://superstes.eu -v + + * Uses proxy env variable https_proxy == 'http://localhost:4130' + * Trying 127.0.0.1:4130... + * Connected to (nil) (127.0.0.1) port 4130 (#0) + * allocate connect buffer! + * Establish HTTP proxy tunnel to superstes.eu:443 + > CONNECT superstes.eu:443 HTTP/1.1 + > Host: superstes.eu:443 + > User-Agent: curl/7.81.0 + > Proxy-Connection: Keep-Alive + > + < HTTP/1.1 200 OK + < Content-Length: 0 + * Ignoring Content-Length in CONNECT 200 response + < + * Proxy replied 200 to CONNECT request + * CONNECT phase completed! + ... + > GET / HTTP/2 + > Host: superstes.eu + > user-agent: curl/7.81.0 + > accept: */* + > + ... + < HTTP/2 302 + < server: nginx + ... + < + * TLSv1.2 (IN), TLS header, Supplemental data (23): + + 302 Found + +

302 Found

+
nginx
+ + + * Connection #0 to host (nil) left intact + + +HTTPS Proxy +########### + +Behaviour +========= + +tbd + +Proxy Protocol +############## + +Behaviour +========= + +tbd + +SOCKS5 +###### + +Behaviour +========= + +tbd diff --git a/docs/source/info/rules.rst b/docs/source/info/rules.rst index 9db65cc..3e054b4 100644 --- a/docs/source/info/rules.rst +++ b/docs/source/info/rules.rst @@ -39,6 +39,8 @@ Multiple matches can be defined in a single rule. The value of matches is **case-insensitive** by default. +NOTE: The HTTP host-header domain is not compared if :code:`dns` is used - as it can be modified easily. + You can define **multiple values** for each match. Matches can also be **negated** by using the :code:`!` prefix: diff --git a/lib/cnf/cnf_file/validate.go b/lib/cnf/cnf_file/validate.go index 3b17e26..0c28a6d 100644 --- a/lib/cnf/cnf_file/validate.go +++ b/lib/cnf/cnf_file/validate.go @@ -45,6 +45,11 @@ func validateListener(lncnf cnf.ServiceListener, fail bool) bool { } func validateCerts(certs cnf.ServiceCertificates, fail bool) bool { + if certs.CAPublic != "" { + if !validateCert(certs.CAPublic, true, fail) { + return false + } + } if certs.ServerPublic != "" || certs.ServerPrivate != "" { if !validateCert(certs.ServerPublic, true, fail) { return false diff --git a/lib/cnf/config.go b/lib/cnf/config.go index 6c66f77..c731dc0 100644 --- a/lib/cnf/config.go +++ b/lib/cnf/config.go @@ -64,6 +64,7 @@ type ServiceMetrics struct { } type ServiceCertificates struct { + CAPublic string `yaml:"caPublic"` ServerPublic string `yaml:"serverPublic"` ServerPrivate string `yaml:"serverPrivate"` InterceptPublic string `yaml:"interceptPublic"` diff --git a/lib/main/service.go b/lib/main/service.go index 98478df..2e569b4 100644 --- a/lib/main/service.go +++ b/lib/main/service.go @@ -59,11 +59,6 @@ func (svc *service) shutdown(cancel context.CancelFunc) { } log.Info("service", "Stopped") os.Exit(0) - /* - ctx := context.Background() - doneHTTP := httpserver.Shutdown(ctx) - <-doneHTTP - */ } func (svc *service) serve(srv rcv.Server) (err error) { diff --git a/lib/proc/fwd/http.go b/lib/proc/fwd/http.go index c0aa203..1f631b9 100644 --- a/lib/proc/fwd/http.go +++ b/lib/proc/fwd/http.go @@ -31,7 +31,10 @@ func ForwardHttp(srvCnf cnf.ServiceListener, l4Proto meta.Proto, conn net.Conn) } // todo: full parsing may not be needed if 'connect' - pktProxy, connProxyIo := parseConn(srvCnf, l4Proto, conn) + pktProxy, connProxyIo, err := parseConn(srvCnf, l4Proto, conn) + if err != nil { + return + } if pktProxy.L5.Proto != meta.ProtoL5Http { parse.LogConnError("forward", pktProxy, "Got non HTTP-Request on HTTP server") @@ -39,13 +42,14 @@ func ForwardHttp(srvCnf cnf.ServiceListener, l4Proto meta.Proto, conn net.Conn) } reqProxy := readRequest(pktProxy, connProxyIo) - if reqProxy == nil { return + } else if reqProxy.Method == http.MethodConnect { forwardConnect(srvCnf, l4Proto, conn, pktProxy, connProxyIo, reqProxy) + } else { - // plaintext HTTP is unsafe by design and should not be allowed + // plaintext HTTP is unsecure by design and should not be allowed // todo: implement generic https-redirection response forwardPlain(srvCnf, l4Proto, conn, pktProxy, connProxyIo, reqProxy) @@ -67,7 +71,11 @@ func forwardPlain( pkt.L3.DestIP = dest parse.LogConnDebug("forward", pkt, "Updated destination IP") - filterConn(pkt, conn, connIo) + if !filterConn(pkt, conn, connIo) { + proxyResp := responseReject() + proxyResp.Write(conn) + return + } send.ForwardHttp(pkt, conn, connIo, req) } @@ -87,7 +95,11 @@ func forwardConnect( return } - pkt, connIo := parseConn(srvCnf, l4Proto, conn) + pkt, connIo, err := parseConn(srvCnf, l4Proto, conn) + if err != nil { + return + } + host, port := parse.SplitHttpHost(reqProxy.Host, pkt.L5.Encrypted) pkt.L5Http = &parse.ParsedHttp{ Host: host, @@ -108,9 +120,18 @@ func forwardConnect( pkt.L3.DestIP = dest parse.LogConnDebug("forward", pkt, "Updated destination IP") - filterConn(pkt, conn, connIo) - send.Forward(pkt, conn, connIo) + if !filterConn(pkt, conn, connIo) { + if pkt.L5.Encrypted == meta.OptBoolTrue { + // http response for https will show a cryptic response ('wrong version number' vs 'eof while reading') + return + } else { + proxyResp := responseReject() + proxyResp.Write(conn) + return + } + } + send.Forward(pkt, conn, connIo) } func resolveTargetHostname(pkt parse.ParsedPacket) net.IP { @@ -150,6 +171,14 @@ func responseFailed() http.Response { } } +func responseReject() http.Response { + return http.Response{ + StatusCode: http.StatusForbidden, + ProtoMajor: 1, + ProtoMinor: 1, + } +} + func readRequest(pkt parse.ParsedPacket, connIo io.ReadWriter) *http.Request { reqRaw := bufio.NewReader(connIo) req, err := http.ReadRequest(reqRaw) diff --git a/lib/proc/fwd/main.go b/lib/proc/fwd/main.go index 804c6a1..b3b9b4f 100644 --- a/lib/proc/fwd/main.go +++ b/lib/proc/fwd/main.go @@ -23,17 +23,26 @@ func Forward(srvCnf cnf.ServiceListener, l4Proto meta.Proto, conn net.Conn) { defer metrics.CurrentConn.Dec() } - pkt, connIo := parseConn(srvCnf, l4Proto, conn) - filterConn(pkt, conn, connIo) + pkt, connIo, err := parseConn(srvCnf, l4Proto, conn) + if err != nil { + return + } + if !filterConn(pkt, conn, connIo) { + return + } send.Forward(pkt, conn, connIo) } -func parseConn(srvCnf cnf.ServiceListener, l4Proto meta.Proto, conn net.Conn) (pkt parse.ParsedPacket, connIo io.ReadWriter) { +func parseConn(srvCnf cnf.ServiceListener, l4Proto meta.Proto, conn net.Conn) (pkt parse.ParsedPacket, connIo io.ReadWriter, err error) { connIo = conn connIoBuf := new(bytes.Buffer) connIoTee := io.TeeReader(connIo, connIoBuf) - pkt = parse.Parse(srvCnf, l4Proto, conn, connIoTee) + pkt, err = parse.Parse(srvCnf, l4Proto, conn, connIoTee) + + if err != nil { + return + } // write read bytes back to stream so we can forward them connIo = u.NewReadWriter(io.MultiReader(bytes.NewReader(connIoBuf.Bytes()), connIo), connIo) @@ -41,12 +50,12 @@ func parseConn(srvCnf cnf.ServiceListener, l4Proto meta.Proto, conn net.Conn) (p return } -func filterConn(pkt parse.ParsedPacket, conn net.Conn, connIo io.ReadWriter) { +func filterConn(pkt parse.ParsedPacket, conn net.Conn, connIo io.ReadWriter) bool { if !filter.Filter(pkt) { parse.LogConnInfo("forward", pkt, "Denied") - conn.Close() - return + return false } parse.LogConnInfo("forward", pkt, "Accept") + return true } diff --git a/lib/proc/parse/conn_log.go b/lib/proc/parse/conn_log.go index 7f08f37..0d20745 100644 --- a/lib/proc/parse/conn_log.go +++ b/lib/proc/parse/conn_log.go @@ -1,9 +1,14 @@ package parse -import "github.com/superstes/calamary/log" +import ( + "github.com/superstes/calamary/cnf" + "github.com/superstes/calamary/log" +) func LogConnDebug(pkg string, pkt ParsedPacket, msg string) { - log.Conn("DEBUG", pkg, PktSrc(pkt), PktDest(pkt), msg) + if cnf.Debug() { + log.Conn("DEBUG", pkg, PktSrc(pkt), PktDest(pkt), msg) + } } func LogConnInfo(pkg string, pkt ParsedPacket, msg string) { diff --git a/lib/proc/parse/main.go b/lib/proc/parse/main.go index d1ed337..e59d69e 100644 --- a/lib/proc/parse/main.go +++ b/lib/proc/parse/main.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "net" + "strings" "time" "github.com/superstes/calamary/cnf" @@ -14,13 +15,18 @@ import ( "github.com/superstes/calamary/u" ) -func Parse(srvCnf cnf.ServiceListener, l4Proto meta.Proto, conn net.Conn, connIo io.Reader) (pkt ParsedPacket) { - // get packet L5-header +func Parse(srvCnf cnf.ServiceListener, l4Proto meta.Proto, conn net.Conn, connIo io.Reader) (pkt ParsedPacket, err error) { conn.SetReadDeadline(time.Now().Add(u.Timeout(cnf.C.Service.Timeout.Process))) + + // get packet L5-header var hdr [cnf.BYTES_HDR_L5]byte n, err := io.ReadFull(connIo, hdr[:]) if err != nil { log.Warn("parse", fmt.Sprintf("Error parsing L5Header: %v", err)) + if strings.Contains(fmt.Sprintf("%v", err), "first record does not look like a TLS handshake") { + // plain http received by https mode + return + } } connIo = io.MultiReader(bytes.NewReader(hdr[:n]), connIo) // write header back to stream @@ -68,7 +74,7 @@ func parseTcp(srvCnf cnf.ServiceListener, conn net.Conn, connIo io.Reader, hdr [ // destination address var dstIpPort net.Addr - if srvCnf.TProxy { + if srvCnf.Mode != meta.ListenModeTransparent || srvCnf.TProxy { dstIpPort = conn.LocalAddr() } else { diff --git a/lib/rcv/http.go b/lib/rcv/http.go index 75a999e..1ce4c8b 100644 --- a/lib/rcv/http.go +++ b/lib/rcv/http.go @@ -21,19 +21,17 @@ func newServerHttpTcp(addr string, lncnf cnf.ServiceListener) (Server, error) { } func newServerHttpsTcp(addr string, lncnf cnf.ServiceListener) (Server, error) { + keyPair, err := tls.LoadX509KeyPair( + cnf.C.Service.Certs.ServerPublic, + cnf.C.Service.Certs.ServerPrivate, + ) + if err != nil { + return Server{}, fmt.Errorf("Failed to load certificates") + } tlsCnf := &tls.Config{ - MinVersion: tls.VersionTLS11, - NextProtos: []string{"h2", "http/1.1"}, - /* - CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256}, - PreferServerCipherSuites: true, - CipherSuites: []uint16{ - tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, - tls.TLS_RSA_WITH_AES_256_GCM_SHA384, - tls.TLS_RSA_WITH_AES_256_CBC_SHA, - }, - */ + MinVersion: tls.VersionTLS10, + NextProtos: []string{"http/1.1"}, + Certificates: []tls.Certificate{keyPair}, } ln, err := tls.Listen( "tcp", diff --git a/lib/u/helpers.go b/lib/u/helpers.go index 861e751..9017df8 100644 --- a/lib/u/helpers.go +++ b/lib/u/helpers.go @@ -2,8 +2,10 @@ package u import ( "context" + "crypto/x509" "fmt" "net" + "os" "strings" "time" @@ -148,3 +150,40 @@ func FormatIPv6(ip string) string { } return ip } + +func TrustedCAs() *x509.CertPool { + cafile := cnf.C.Service.Certs.CAPublic + caCertPool := x509.NewCertPool() + if cafile != "" { + cas, err := os.ReadFile(cafile) + if err != nil { + log.ErrorS("util", fmt.Sprintf("Failed to load trusted CAs from file %v", cafile)) + return caCertPool + } + caCertPool.AppendCertsFromPEM(cas) + } + return caCertPool +} + +/* +Usage example: + + buf := make([]byte, 512) + conn.Read(buf) + u.DumpToFile(buf) +*/ +func DumpToFile(data []byte) { + file := fmt.Sprintf("/tmp/calamary_dump_%v.bin", time.Now().Unix()) + dump, err := os.Create(file) + if err != nil { + fmt.Println(err) + return + } + defer dump.Close() + _, err = dump.Write(data) + if err != nil { + fmt.Println(err) + return + } + log.Info("util", "Dump written to file: "+file) +}