0

I have the following piece of code:

l, err := tls.Listen("tcp", "localhost:0", cfg)
dieIf(err)
c, err := l.Accept()
dieIf(err)
err = c.(*tls.Conn).Handshake()
dieIf(err)

It works just fine, but I'd like to intercept tls.Conn's reads and writes.

I thought about doing this:

type MitmConn struct {
  net.Conn
}

func (self *MitmConn) Read(b []byte) (n int, err error) {
  ...
}

func (self *MitmConn) Write(b []byte) (n int, err error) {
  ...
}

l, err := tls.Listen("tcp", "localhost:0", cfg)
dieIf(err)
c, err := l.Accept()
dieIf(err)
c = &MitmConn{c}

But then, this would fail:

// panic: interface conversion: net.Conn is *MitmConn, not *tls.Conn
err = c.(*tls.Conn).Handshake()
dieIf(err)

Any ideas?

solidak
  • 5,033
  • 3
  • 31
  • 33

2 Answers2

2
package main

import "crypto/tls"

func dieIf(err error) {
    if err != nil {
        panic(err)
    }
}

type mitmConn struct {
    *tls.Conn
}

func (mc *mitmConn) Read(b []byte) (n int, err error) {
  return 0, nil
}

func (mc *mitmConn) Write(b []byte) (n int, err error) {
  return 0, nil
}

func main() {
    l, err := tls.Listen("tcp", "localhost:0", nil)
    dieIf(err)
    c, err := l.Accept()
    dieIf(err)
    mc := mitmConn{c.(*tls.Conn)}
    err = mc.Handshake()
    dieIf(err)
}

Please consider unlearning naming the receivers self (and also this).

kostix
  • 51,517
  • 14
  • 93
  • 176
  • Thanks for the tip and the response. This doesn't intercept the handshake's writes and reads, though. It just intercepts any explicit reading or writing (AKA mc.Write() and mc.Read()), but not the activity happening inside (tls.Conn.Handshake()) – solidak Sep 29 '21 at 06:47
  • @solidak, I'm afraid, unfortunately, you won't be able to intercept reads and writes during the handshake: specifically because there's no "inheritance in the C++ sense": even if you embed a value of the correct type in your "MitM type" so that it compiles, the `Handshake` method gets promoted from the embedded type to the enclosing type, but when that method gets called on a value of the enclosing type, the receiver passed to that method will be of type `*tls.Conn`—that is, the value of the embedded type, not the enclosing type. – kostix Sep 29 '21 at 09:40
  • @solidak, specifically see [this section of the spec](https://golang.org/ref/spec#Struct_types): «If S contains an embedded field *T, the method sets of S and *S both include promoted _methods with receiver `T` or `*T`»_ (emphasis mine). Basically this means that when you call `Handshake` on an instance of your "MitM type", it really gets called on the embedded field of type `*tls.Conn`, and hence `Read` and `Write` will be called on that same receiver. – kostix Sep 29 '21 at 09:43
  • I hear. You're correct. I had to do a "hacky" way by basically breaking open tls.Accept(). The example I had is not transferable easily to other constructs, though. Seriously thanks, @kostix, for the great direction and time you've put here! – solidak Sep 29 '21 at 12:16
0

@kostix's answer suffices for most cases, but if you want to intercept tls.Conn.Handshake()'s reads and writes, you need to inject your mitmConn wrapper one level beneath.

Here's a working example:

package main

import "crypto/tls"

func dieIf(err error) {
    if err != nil {
        panic(err)
    }
}

type mitmConn struct {
    // Since we went one level beneath, no need for *tls.Conn here
    net.Conn
}

func (mc *mitmConn) Read(b []byte) (n int, err error) {
  return 0, nil
}

func (mc *mitmConn) Write(b []byte) (n int, err error) {
  return 0, nil
}

func main() {
    // Don't use tls.Listen
    l, err := net.Listen("tcp", "localhost:0")
    dieIf(err)
    c, err := l.Accept()
    dieIf(err)
    // Make a new tls.Conn. 
    // After the tls.Server() call, the nesting of Conn 
    // objects looks will be:
    // - *tls.Conn
    //  - mitmConn
    //   - *net.TCPConn
    c = tls.Server(mitmConn{c}, nil)
    // Since tls.Server returns a net.Conn, not a *tls.Conn, we need to cast
    err = c.(*tls.Conn).Handshake()
    dieIf(err)
}
solidak
  • 5,033
  • 3
  • 31
  • 33