GithubHelp home page GithubHelp logo

pires / go-proxyproto Goto Github PK

View Code? Open in Web Editor NEW
464.0 12.0 106.0 253 KB

A Go library implementation of the PROXY protocol, versions 1 and 2.

License: Apache License 2.0

Go 100.00%
proxy-protocol haproxy golang

go-proxyproto's Introduction

go-proxyproto

Actions Status Coverage Status Go Report Card

A Go library implementation of the PROXY protocol, versions 1 and 2, which provides, as per specification:

(...) a convenient way to safely transport connection information such as a client's address across multiple layers of NAT or TCP proxies. It is designed to require little changes to existing components and to limit the performance impact caused by the processing of the transported information.

This library is to be used in one of or both proxy clients and proxy servers that need to support said protocol. Both protocol versions, 1 (text-based) and 2 (binary-based) are supported.

Installation

$ go get -u github.com/pires/go-proxyproto

Usage

Client

package main

import (
	"io"
	"log"
	"net"

	proxyproto "github.com/pires/go-proxyproto"
)

func chkErr(err error) {
	if err != nil {
		log.Fatalf("Error: %s", err.Error())
	}
}

func main() {
	// Dial some proxy listener e.g. https://github.com/mailgun/proxyproto
	target, err := net.ResolveTCPAddr("tcp", "127.0.0.1:2319")
	chkErr(err)

	conn, err := net.DialTCP("tcp", nil, target)
	chkErr(err)

	defer conn.Close()

	// Create a proxyprotocol header or use HeaderProxyFromAddrs() if you
	// have two conn's
	header := &proxyproto.Header{
		Version:            1,
		Command:            proxyproto.PROXY,
		TransportProtocol:  proxyproto.TCPv4,
		SourceAddr: &net.TCPAddr{
			IP:   net.ParseIP("10.1.1.1"),
			Port: 1000,
		},
		DestinationAddr: &net.TCPAddr{
			IP:   net.ParseIP("20.2.2.2"),
			Port: 2000,
		},
	}
	// After the connection was created write the proxy headers first
	_, err = header.WriteTo(conn)
	chkErr(err)
	// Then your data... e.g.:
	_, err = io.WriteString(conn, "HELO")
	chkErr(err)
}

Server

package main

import (
	"log"
	"net"

	proxyproto "github.com/pires/go-proxyproto"
)

func main() {
	// Create a listener
	addr := "localhost:9876"
	list, err := net.Listen("tcp", addr)
	if err != nil {
		log.Fatalf("couldn't listen to %q: %q\n", addr, err.Error())
	}

	// Wrap listener in a proxyproto listener
	proxyListener := &proxyproto.Listener{Listener: list}
	defer proxyListener.Close()

	// Wait for a connection and accept it
	conn, err := proxyListener.Accept()
	defer conn.Close()

	// Print connection details
	if conn.LocalAddr() == nil {
		log.Fatal("couldn't retrieve local address")
	}
	log.Printf("local address: %q", conn.LocalAddr().String())

	if conn.RemoteAddr() == nil {
		log.Fatal("couldn't retrieve remote address")
	}
	log.Printf("remote address: %q", conn.RemoteAddr().String())
}

HTTP Server

package main

import (
	"net"
	"net/http"
	"time"

	"github.com/pires/go-proxyproto"
)

func main() {
	server := http.Server{
		Addr: ":8080",
	}

	ln, err := net.Listen("tcp", server.Addr)
	if err != nil {
		panic(err)
	}

	proxyListener := &proxyproto.Listener{
		Listener:          ln,
		ReadHeaderTimeout: 10 * time.Second,
	}
	defer proxyListener.Close()

	server.Serve(proxyListener)
}

Special notes

AWS

AWS Network Load Balancer (NLB) does not push the PPV2 header until the client starts sending the data. This is a problem if your server speaks first. e.g. SMTP, FTP, SSH etc.

By default, NLB target group attribute proxy_protocol_v2.client_to_server.header_placement has the value on_first_ack_with_payload. You need to contact AWS support to change it to on_first_ack, instead.

Just to be clear, you need this fix only if your server is designed to speak first.

go-proxyproto's People

Contributors

antoniomika avatar astromechza avatar bohanyang avatar databus23 avatar dependabot[bot] avatar dgl avatar drakkan avatar emersion avatar freeaqingme avatar guysv avatar igor-kupczynski avatar igorwwwwwwwwwwwwwwwwwwww avatar jeremysf avatar kayrus avatar kmala avatar mikey179 avatar mschneider82 avatar navossoc avatar nicmue avatar pires avatar stu-elastic avatar timwolla avatar unmarshal avatar viruthagiri avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

go-proxyproto's Issues

TLV.Length is redundant

We already have len(TLV.Value). Having an extra Length field allows for a potential mismatch between these two sources of truth. This isn't that of a big deal when parsing (because the library will set both correctly), however this is more of an issue when formatting a TLV (because the caller needs to set both correctly).

Data race in SetDeadline

Hello!

I have a test that runs with the -race flag that started to fail with v0.6.1. I've created a minimal repro here: https://github.com/jjiang-stripe/proxyproto-race/blob/master/main.go

That example is just running a super basic SSH server and client and it looks like Go's SSH server's running into a race when trying to update deadlines in its readLoop and kexLoop goroutines:

$ go run -race main.go 
==================
WARNING: DATA RACE
Write at 0x00c0000da058 by goroutine 27:
  github.com/pires/go-proxyproto.(*Conn).SetDeadline()
      /Users/jjiang/go/pkg/mod/github.com/pires/[email protected]/protocol.go:218 +0x4f
  github.com/gliderlabs/ssh.(*serverConn).updateDeadline()
      /Users/jjiang/go/pkg/mod/github.com/gliderlabs/[email protected]/conn.go:53 +0x2f9
  github.com/gliderlabs/ssh.(*serverConn).Write()
      /Users/jjiang/go/pkg/mod/github.com/gliderlabs/[email protected]/conn.go:18 +0x44
  bufio.(*Writer).Flush()
      /usr/local/Cellar/go/1.17.2/libexec/src/bufio/bufio.go:607 +0xf0
  golang.org/x/crypto/ssh.(*connectionState).writePacket()
      /Users/jjiang/go/pkg/mod/golang.org/x/[email protected]/ssh/transport.go:182 +0x12b
  golang.org/x/crypto/ssh.(*transport).writePacket()
      /Users/jjiang/go/pkg/mod/golang.org/x/[email protected]/ssh/transport.go:172 +0x98
  golang.org/x/crypto/ssh.(*handshakeTransport).pushPacket()
      /Users/jjiang/go/pkg/mod/golang.org/x/[email protected]/ssh/handshake.go:223 +0x84d
  golang.org/x/crypto/ssh.(*handshakeTransport).sendKexInit()
      /Users/jjiang/go/pkg/mod/golang.org/x/[email protected]/ssh/handshake.go:472 +0x84e
  golang.org/x/crypto/ssh.(*handshakeTransport).kexLoop()
      /Users/jjiang/go/pkg/mod/golang.org/x/[email protected]/ssh/handshake.go:278 +0x77d
  golang.org/x/crypto/ssh.newServerTransport·dwrap·10()
      /Users/jjiang/go/pkg/mod/golang.org/x/[email protected]/ssh/handshake.go:143 +0x39

Previous write at 0x00c0000da058 by goroutine 25:
  github.com/pires/go-proxyproto.(*Conn).SetDeadline()
      /Users/jjiang/go/pkg/mod/github.com/pires/[email protected]/protocol.go:218 +0x4f
  github.com/gliderlabs/ssh.(*serverConn).updateDeadline()
      /Users/jjiang/go/pkg/mod/github.com/gliderlabs/[email protected]/conn.go:53 +0x2f9
  github.com/gliderlabs/ssh.(*serverConn).Read()
      /Users/jjiang/go/pkg/mod/github.com/gliderlabs/[email protected]/conn.go:27 +0x44
  bufio.(*Reader).Read()
      /usr/local/Cellar/go/1.17.2/libexec/src/bufio/bufio.go:227 +0x4da
  io.ReadAtLeast()
      /usr/local/Cellar/go/1.17.2/libexec/src/io/io.go:328 +0xdd
  io.ReadFull()
      /usr/local/Cellar/go/1.17.2/libexec/src/io/io.go:347 +0x8d
  golang.org/x/crypto/ssh.(*streamPacketCipher).readCipherPacket()
      /Users/jjiang/go/pkg/mod/golang.org/x/[email protected]/ssh/cipher.go:153 +0x55
  golang.org/x/crypto/ssh.(*connectionState).readPacket()
      /Users/jjiang/go/pkg/mod/golang.org/x/[email protected]/ssh/transport.go:130 +0xa1
  golang.org/x/crypto/ssh.(*transport).readPacket()
      /Users/jjiang/go/pkg/mod/golang.org/x/[email protected]/ssh/transport.go:114 +0x4a
  golang.org/x/crypto/ssh.(*handshakeTransport).readOnePacket()
      /Users/jjiang/go/pkg/mod/golang.org/x/[email protected]/ssh/handshake.go:374 +0x71
  golang.org/x/crypto/ssh.(*handshakeTransport).readLoop()
      /Users/jjiang/go/pkg/mod/golang.org/x/[email protected]/ssh/handshake.go:197 +0x4a
  golang.org/x/crypto/ssh.newServerTransport·dwrap·9()
      /Users/jjiang/go/pkg/mod/golang.org/x/[email protected]/ssh/handshake.go:142 +0x39

Goroutine 27 (running) created at:
  golang.org/x/crypto/ssh.newServerTransport()
      /Users/jjiang/go/pkg/mod/golang.org/x/[email protected]/ssh/handshake.go:143 +0x238
  golang.org/x/crypto/ssh.(*connection).serverHandshake()
      /Users/jjiang/go/pkg/mod/golang.org/x/[email protected]/ssh/server.go:249 +0x665
  golang.org/x/crypto/ssh.NewServerConn()
      /Users/jjiang/go/pkg/mod/golang.org/x/[email protected]/ssh/server.go:206 +0x239
  github.com/gliderlabs/ssh.(*Server).HandleConn()
      /Users/jjiang/go/pkg/mod/github.com/gliderlabs/[email protected]/server.go:281 +0x3b1
  github.com/gliderlabs/ssh.(*Server).Serve·dwrap·12()
      /Users/jjiang/go/pkg/mod/github.com/gliderlabs/[email protected]/server.go:258 +0x58

Goroutine 25 (running) created at:
  golang.org/x/crypto/ssh.newServerTransport()
      /Users/jjiang/go/pkg/mod/golang.org/x/[email protected]/ssh/handshake.go:142 +0x1d0
  golang.org/x/crypto/ssh.(*connection).serverHandshake()
      /Users/jjiang/go/pkg/mod/golang.org/x/[email protected]/ssh/server.go:249 +0x665
  golang.org/x/crypto/ssh.NewServerConn()
      /Users/jjiang/go/pkg/mod/golang.org/x/[email protected]/ssh/server.go:206 +0x239
  github.com/gliderlabs/ssh.(*Server).HandleConn()
      /Users/jjiang/go/pkg/mod/github.com/gliderlabs/[email protected]/server.go:281 +0x3b1
  github.com/gliderlabs/ssh.(*Server).Serve·dwrap·12()
      /Users/jjiang/go/pkg/mod/github.com/gliderlabs/[email protected]/server.go:258 +0x58
==================
Found 1 data race(s)
exit status 66

I believe this was introduced in #76 because there's a local variable being set in SetDeadline().

Unfortunately I'm not familiar enough with what's going on here to tell if this is actually an issue or propose a fix beyond maybe throwing a lock in there. I'd appreciate a fix here (or if this isn't actually a concern, an explanation would be nice).

Happy to provide more details, but given that I'm in very unfamiliar territory, I'm not sure how much help I can be 😅

tls handshake error

image

server code

func main() {

    cert, err := tls.LoadX509KeyPair("cert/xxxx.crt", "cert/xxx.key")
    if err != nil {
        log.Println(err)
        return
    }
    config := &tls.Config{
        Certificates: []tls.Certificate{cert}}

    ln, err := tls.Listen("tcp", ":9090", config)

    proxyListener := &proxyproto.Listener{
        Listener: ln,
    }

    conn, err := proxyListener.Accept()
    if err != nil {
        log.Fatalf("err: %v", err)
    }
    defer conn.Close()

    addr := conn.RemoteAddr().(*net.TCPAddr)
    fmt.Println("ip====", addr.IP.String())
    if addr.IP.String() != "10.1.1.1" {
        log.Fatalf("bad: %v", addr)
    }
    if addr.Port != 1000 {
        log.Fatalf("bad: %v", addr)
    }
}

client code

func main() {

    conn, err := net.Dial("tcp", "127.0.0.1:9090")
    if err != nil {
        log.Fatalf("err: %v", err)
    }
    defer conn.Close()

    // Write out the header!
    header := "PROXY TCP4 10.1.1.1 20.2.2.2 1000 2000\r\n"
    conn.Write([]byte(header))

}

trigger error

[ERR] Failed to read proxy prefix: tls: first record does not look like a TLS handshake

on golang source code
image

i think you wrap listener runing after the golang source code tls handshake

CIDR based policy for a simple non-PROXY traffic

I believe I'm not the first one who tries to solve the PROXY PROTOCOL vs no PROXY PROTOCOL traffic problem for a single listener. This is a very common behavior for k8s clusters that use PROXY_PROTOCOL based loadbalancers.

In short, the k8s pods clients local traffic doesn't use PROXY PROTOCOL, therefore accessing the service locally that expects PROXY PROTOCOL fails. There are two dirty solutions for this: route local traffic to the external loadbalancer IP (to add the required header), or fallback due to timeout.

Both options above have flaws: LB adds latency and load, timeout adds latency and acts unpredictably on heavy load.

I know that it's possible to create an extra wrapper around the original Conn, but this would look dirty. I propose to add a new SKIP policy, which will treat a connection as a regular one and it won't trigger proxyproto handlers. The SKIP policy can be defined in a PolicyFunc callback.

Objections?

LICENSE missing copyright line

Hey pires,

Looks like your LICENSE file does not have the filled in Copyright line:

Copyright {yyyy} {name of copyright owner}

Looks like it needs your name and the year filled in. I'm trying to see if I can include this library into https://github.com/fabiolb/fabio to add proxy v2 support to that project but I need to know the correct copyright line as per their contribution guidelines on attributions :)

Thanks!

Add Support for "ProxyHeaderTimeout"

This library seems to lean heavily on inspiration from https://github.com/armon/go-proxyproto

That library supports passing in a timeout when you define the listener:

		tln = &proxyproto.Listener{
			Listener:           tln,
			ProxyHeaderTimeout: l.ProxyHeaderTimeout,
		}

It would be nice if the same feature, or a similar feature was possible with this library.

Read() don't contains proxy protocol package length

Hi:

when I use proxyproto in udp, I want to get proxy protocol length from header, but I can't find it. now I replace it by:

tempBuff := bytes.NewBuffer(nil)
proxyProtocolLen, err := header.WriteTo(tempBuff)

If can get it directly from the header, it would be fine.

Tests goroutines call t.Fatalf

And go vet doesn't like it

Error: ./protocol_test.go:29:4: call to (*T).Fatalf from a non-test goroutine
Error: ./protocol_test.go:37:4: call to (*T).Fatalf from a non-test goroutine
Error: ./protocol_test.go:40:4: call to (*T).Fatalf from a non-test goroutine
Error: ./protocol_test.go:88:4: call to (*T).Fatalf from a non-test goroutine
Error: ./protocol_test.go:99:4: call to (*T).Fatalf from a non-test goroutine
Error: ./protocol_test.go:102:4: call to (*T).Fatalf from a non-test goroutine
Error: ./protocol_test.go:165:4: call to (*T).Fatalf from a non-test goroutine
Error: ./protocol_test.go:176:4: call to (*T).Fatalf from a non-test goroutine
Error: ./protocol_test.go:179:4: call to (*T).Fatalf from a non-test goroutine
Error: ./protocol_test.go:231:4: call to (*T).Fatalf from a non-test goroutine
Error: ./protocol_test.go:259:4: call to (*T).Fatalf from a non-test goroutine
Error: ./protocol_test.go:291:4: call to (*T).Fatalf from a non-test goroutine
Error: ./protocol_test.go:335:4: call to (*T).Fatalf from a non-test goroutine
Error: ./protocol_test.go:359:4: call to (*T).Fatalf from a non-test goroutine
Error: ./protocol_test.go:362:4: call to (*T).Fatalf from a non-test goroutine
Error: ./protocol_test.go:433:4: call to (*T).Fatalf from a non-test goroutine
Error: ./protocol_test.go:466:4: call to (*T).Fatalf from a non-test goroutine
Error: ./protocol_test.go:499:4: call to (*T).Fatalf from a non-test goroutine
Error: ./protocol_test.go:542:4: call to (*T).Fatalf from a non-test goroutine
Error: ./protocol_test.go:641:4: call to (*T).Fatalf from a non-test goroutine
Error: ./protocol_test.go:710:4: call to (*T).Fatalf from a non-test goroutine
Error: ./protocol_test.go:817:5: call to (*B).Fatalf from a non-test goroutine
Error: ./protocol_test.go:837:5: call to (*B).Fatalf from a non-test goroutine
Error: ./protocol_test.go:842:6: call to (*B).Fatalf from a non-test goroutine
Error: ./protocol_test.go:848:5: call to (*B).Fatalf from a non-test goroutine
Error: ./protocol_test.go:884:5: call to (*B).Fatalf from a non-test goroutine
Error: Process completed with exit code 2.

Please add the leading v to the next release tag

The leading v is required for modules:

"""
Modules must be semantically versioned according to semver, usually in the form v(major).(minor).(patch), such as v0.1.0, v1.2.3, or v1.5.0-rc.1. The leading v is required. If using Git, tag released commits with their versions
"""

thanks for this great library!

Convenient way (example) how to use tlvparse functions with proxyproto.Conn

I have question about way to use tlvparse subpackage. I need to use SSL functions from tlvparse package to fetch SSL related info from header. It takes []proxyproto.TLV as argument, so my question is what the right way to get []proxyproto.TLV from proxyproto.Conn struct? My problem is that header field unexported in proxyproto.Conn struct and I can not access it from my app. May be I do something wrong, or misunderstand something, any idea, comments, suggestion? Thanks!

Release notes for 0.3.0

This is a meta issue to plan the next breaking release.

Breaking changes

  • Rely on net.Addr, which allows to represent any network address, instead of using an IP and a port (#46)

    The Header.{Local,Remote}Addr methods are removed since users can now
    just access Header.{Source,Destination}Addr.

    New helpers (header.{TCPAddrs(),UDPAddrs(),UnixAddrs(),IPs(),Ports()}) allows to
    easily extract data from net.Addr attributes.

    The header.{Local,Remote}Addr helpers have been removed in favor or accessors header.{Source,Destination}Addr.

  • TLV.Length has been removed, in favor of using len(TLV.Value) instead (#42)

Improvements

  • Properly validate source and destination IPs when transport protocol is IP-based, e.g. ensure IPv4 addresses when TCPv4 is set (#46 and #48)
  • Support Unix sockets (#46)
  • Format a PP2SSL struct into a TLV (#41)
  • Test Go 1.14 and 1.15 support (#39)
  • Update spec URL (#40)

ReadTimeout is misleading, and more

Scenario:
You have a server that is supposed to handle both proxy and non proxy type connections. You also have payloads which might be 2-4 bytes of significant data. The way you have your timeout code:

  1. <-time.After will leak memory if it does not fire, and over time this will suck
  2. You return an error nicely after the timeout, but you still are not going to give up the Read on the connection until you see at-least 5 bytes! (or is it 12?)

My w/a:
I simply set a ReadDeadline for my duration needed and use your blocking Read which should propagate an error as expected. This can still be ugly if the timing becomes unfortunate, but seems to work for the time being. Long term, I intended to re-write the bits that concern our application, but I felt this library could be beefed up a little more.

Proposal
Could you at-least add the code to peek only one byte and if that does not match, give up and bail? This would be immediately useful for clients who connect and have very low packet sizes.

The <-time.After thing is a personal gripe of mine. It should have no place in server code unless you are 100% sure that it will either (1) fire or (2) your program will exit. It should never be part of a case statement like that. See this group thread: https://groups.google.com/forum/#!topic/golang-nuts/A597Btr_0P8

DefaultReadHeaderTimeout is too strict

200ms is too strict and is causing breakage downstream, especially when TLS is involved. Having a timeout is good, but should not penalize slow connections.

The stdlib is using a 10s timeout for the TLS handshake for instance.

Ref emersion/soju#33

An example for a Dialer and Listener is required.

This is a really good library but it's missing examples for a Dialer and a Listener.
IE client and a server service.
I might write one and I was thinking about a simple "echo" client and server.

The server would accept a PROXY TCP protocol connection and will echo back(and log to stdout) if possible the request protocol(TCP4\TCP6 only), src and destination IP:PORT.

The client would open a connection to a PROXY enabled server(Listener) with a specific src and destination IP:PORT (TCP4\TCP6) and will receive the into the socket\connection a line of readable text containing the received IP:PORT.

I have seen an implementation of this but I forgot to save the links so I cannot copy and paste it and I will need to use some CPU to write it.
Any recommendations on where to start?

I have seen this implementation but it's still not sitting well for me

Other examples:

I wrote a TPROXY Server and Library in golang at:

Which has a basic TCP proxy server function that can eventually utilize the PROXY protocol in couple environments.

RemoteAddr from upstream

Is it possible to get the upstream remote address?

func (p *Conn) RemoteAddr() net.Addr {

As far I can tell reading the docs and the code, if the PROXY PROTOCOL is enabled we can't get the upstream IP anymore.

Is that right?

Suggestion: Dynamic Proxy Protocol Listener

  • There can be requirements that server can listen and serve whenever proxy protocol is set or not for identifying real client's ip.
  • I will make PR if many people needs it.

[Feature] Support for policy on local address

Currently we can apply policy to verify the upstream address

if p.Policy != nil {
proxyHeaderPolicy, err = p.Policy(conn.RemoteAddr())
if err != nil {
// can't decide the policy, we can't accept the connection
conn.Close()
return nil, err
}
// Handle a connection as a regular one
if proxyHeaderPolicy == SKIP {
return conn, nil
}
}
. There could be scenarios where there are multiple interfaces the server is listening on and we may want to enable the proxy protocol on only 1 interface. It would help to have policy support verifying the local address.
I can create a PR if the feature is okay to be implemented.

Crashes can occur in high concurrency scenarios

Hello, I add go-proxyproto as a dependency in the server side of a VPN project. When the number of connections is high, it tends to crash. Here is the error message:

May 11 13:57:35 xxxhostname xxxappname: goroutine 1520070 [runnable]:
May 11 13:57:35 xxxhostname xxxappname: internal/poll.runtime_pollWait(0x7fd92f37bf40, 0x72)
May 11 13:57:35 xxxhostname xxxappname: runtime/netpoll.go:305 +0x89
May 11 13:57:35 xxxhostname xxxappname: internal/poll.(*pollDesc).wait(0xc009a02100?, 0xc0034db000?, 0x0)
May 11 13:57:35 xxxhostname xxxappname: internal/poll/fd_poll_runtime.go:84 +0x32
May 11 13:57:35 xxxhostname xxxappname: internal/poll.(*pollDesc).waitRead(...)
May 11 13:57:35 xxxhostname xxxappname: internal/poll/fd_poll_runtime.go:89
May 11 13:57:35 xxxhostname xxxappname: internal/poll.(*FD).Read(0xc009a02100, {0xc0034db000, 0x1000, 0x1000})
May 11 13:57:35 xxxhostname xxxappname: internal/poll/fd_unix.go:167 +0x25a
May 11 13:57:35 xxxhostname xxxappname: net.(*netFD).Read(0xc009a02100, {0xc0034db000?, 0xc00a66f3a0?, 0x40d75d?})
May 11 13:57:35 xxxhostname xxxappname: net/fd_posix.go:55 +0x29
May 11 13:57:35 xxxhostname xxxappname: net.(*conn).Read(0xc002f11e48, {0xc0034db000?, 0x7fd956a815b8?, 0x1000?})
May 11 13:57:35 xxxhostname xxxappname: net/net.go:183 +0x45
May 11 13:57:35 xxxhostname xxxappname: bufio.(*Reader).Read(0xc004ed8cc0, {0xc0034db000, 0x1000, 0x1000?})
May 11 13:57:35 xxxhostname xxxappname: bufio/bufio.go:223 +0x106
May 11 13:57:35 xxxhostname xxxappname: github.com/pires/go-proxyproto.(*Conn).Read(0xc0001229a0?, {0xc0034db000?, 0xbe14bf?, 0x7ddf8402d9f34adb?})
May 11 13:57:35 xxxhostname xxxappname: github.com/pires/[email protected]/protocol.go:132 +0xa5
...stack trace in xxxappname...

Mixing IPv6 and IPv4 in header

I noticed while doing a little testing that if you generate the SourceAddress and DestinationAddress, it's possible to get an IPv4 address and an IPv6 address.

Example:

net.LookupIP("localhost")

Can return ::1 as the net.IP, while it's up to the user to verify that they're sending IPv4/IPv6 addresses with v4/v6 protocols, I'm wondering if this writeVersion1 and writeVersion2 functions should return an error if the IPs return nil for the protocol being sent.

I'm willing to do the PR for it, but I just thought checking if the validation should be in the package or in the user code is the right way.

ReadHeaderTimeout is setting a hard timeout on connections regardless of header sending

I think this setting is supposed to timeout connections that are not sending the headers and terminate them after this amount of time. However, I set the setting to 5 seconds, and the result is that all connections are terminated after 5 seconds, even if they correctly sent the headers. The connection starts up, but is then abruptly ended after the 5 seconds are passed.

I think what is missing in #74 is a call to SetReadDeadline to reset the timeout after the proxy header was sent successfully.

Unix socket addresses missing from Header

Header only supports IP addresses. However the Unix socket header is defined as such:

        struct {        /* for AF_UNIX sockets, len = 216 */
             uint8_t src_addr[108];
             uint8_t dst_addr[108];
        } unix_addr;

Parsing v2 UNSPEC and v1 PROXY UNKNOWN not implemented

Hi! I found that this library is missing the ability to parse these two kinds of header, which are required by the protocol and mandatory to implement, while trying to resolve XTLS/Xray-core#166

v2 UNSPEC

When this header is received:

0d 0a 0d 0a 00 0d 0a 51 55 49 54 0a 20 00 00 00

which is + LOCAL command (\x20) + UNSPEC address family UNSPEC protocol (\x00)
ErrUnsupportedAddressFamilyAndProtocol is returned

go-proxyproto/v2.go

Lines 85 to 87 in adbbabe

if _, ok := supportedTransportProtocol[header.TransportProtocol]; !ok {
return nil, ErrUnsupportedAddressFamilyAndProtocol
}

This is because UNSPEC is absent in supportedTransportProtocol
const (
UNSPEC AddressFamilyAndProtocol = '\x00'
TCPv4 AddressFamilyAndProtocol = '\x11'
UDPv4 AddressFamilyAndProtocol = '\x12'
TCPv6 AddressFamilyAndProtocol = '\x21'
UDPv6 AddressFamilyAndProtocol = '\x22'
UnixStream AddressFamilyAndProtocol = '\x31'
UnixDatagram AddressFamilyAndProtocol = '\x32'
)
var supportedTransportProtocol = map[AddressFamilyAndProtocol]bool{
TCPv4: true,
UDPv4: true,
TCPv6: true,
UDPv6: true,
UnixStream: true,
UnixDatagram: true,
}

This UNSPEC header is used when HAProxy doing health checks, or the proxied client is requesting via a unix socket.
Also the protocol spec says:

Only the UNSPEC protocol byte (\x00) is mandatory to implement on the receiver.
A receiver is not required to implement other ones, provided that it
automatically falls back to the UNSPEC mode for the valid combinations above
that it does not support.

v1 PROXY UNKNOWN

When header PROXY UNKNOWN\r\n is received,

go-proxyproto/v1.go

Lines 25 to 33 in adbbabe

// Make sure we have a v1 header
line, err := reader.ReadString('\n')
if !strings.HasSuffix(line, crlf) {
return nil, ErrCantReadProtocolVersionAndCommand
}
tokens := strings.Split(line[:len(line)-2], separator)
if len(tokens) < 6 {
return nil, ErrCantReadProtocolVersionAndCommand
}

returns ErrCantReadProtocolVersionAndCommand.
This is because in this case, there's only 2 parts separated by spaces, instead of 6 parts as PROXY <PROTO> <SRC_ADDR> <DEST_ADDR> <SRC_PORT> <DEST_PORT>
The protocol spec says:

If the announced transport protocol is "UNKNOWN", then the receiver knows that
the sender speaks the correct PROXY protocol with the appropriate version, and
SHOULD accept the connection and use the real connection's parameters as if
there were no PROXY protocol header on the wire.

Also, this kind of header can be formatted by the library.

go-proxyproto/v1.go

Lines 76 to 88 in adbbabe

func (header *Header) formatVersion1() ([]byte, error) {
// As of version 1, only "TCP4" ( \x54 \x43 \x50 \x34 ) for TCP over IPv4,
// and "TCP6" ( \x54 \x43 \x50 \x36 ) for TCP over IPv6 are allowed.
var proto string
switch header.TransportProtocol {
case TCPv4:
proto = "TCP4"
case TCPv6:
proto = "TCP6"
default:
// Unknown connection (short form)
return []byte("PROXY UNKNOWN\r\n"), nil
}

Although I really don't know much about Golang, I'll try my best to support you to resolve this issue. Thanks!

ProxyProtocol usage with TLS

I want to use go-proxyproto with TLS and couldn't find any examples in the repo. Appreciate if you can provide reference code for using go-proxyproto with TLS.

I cannot implement TCP based on the domain name

I want to proxy to port 172.16.200.10 21 through the domain name ftp11.com port 21, and proxy to port 172.16.200.20 21 through the domain name ftp122com port 21. Since TCP does not carry the domain name identifier during the transmission process, I cannot implement TCP based on the domain name. Forward

Test TLS Server code has the wrong wrapping order

I was using this library to build a service that terminates TLS and does IP based blocking using the proxy protocol headers. As far I can tell, wrapping the tls listener inside of proxyproto listener is the wrong order. I'm not sure why the test code works with how it is:

https://github.com/pires/go-proxyproto/blob/main/protocol_test.go#L960

When I did it exactly the way the test does it, curl --haproxy-protocol returns TLS errors and in the code I got tls: first record does not look like a TLS handshake. When I did it this way, it works:

	l, _ := net.Listen("tcp", ":8443")
	ppl := &proxyproto.Listener{
		Listener: l,
		Policy: func(upstream net.Addr) (proxyproto.Policy, error) {
			return proxyproto.REQUIRE, nil
		},
	}
	...
	listener := tls.NewListener(l, &config)

I believe this logically makes since because the proxy protocol header needs to be handled first as the tls library doesn't know how to handle it. Is there something I'm missing?

golint code

$ golint ./...
addr_proto.go:7:2: exported const UNSPEC should have comment (or a comment on this block) or be unexported
header.go:15:2: comment on exported var SIGV1 should be of the form "SIGV1 ..."
header.go:17:2: exported var SIGV2 should have comment or be unexported
header.go:96:1: exported method Header.TCPAddrs should have comment or be unexported
header.go:105:1: exported method Header.UDPAddrs should have comment or be unexported
header.go:114:1: exported method Header.UnixAddrs should have comment or be unexported
header.go:123:1: exported method Header.IPs should have comment or be unexported
header.go:133:1: exported method Header.Ports should have comment or be unexported
header_test.go:16:2: don't use ALL_CAPS in Go names; use CamelCase
header_test.go:17:2: don't use ALL_CAPS in Go names; use CamelCase
header_test.go:18:2: don't use ALL_CAPS in Go names; use CamelCase
header_test.go:20:2: don't use ALL_CAPS in Go names; use CamelCase
tlv.go:14:2: comment on exported const PP2_TYPE_ALPN should be of the form "PP2_TYPE_ALPN ..."
tlv.go:15:2: don't use ALL_CAPS in Go names; use CamelCase
tlv.go:16:2: don't use ALL_CAPS in Go names; use CamelCase
tlv.go:16:2: exported const PP2_TYPE_AUTHORITY should have comment (or a comment on this block) or be unexported
tlv.go:17:2: don't use ALL_CAPS in Go names; use CamelCase
tlv.go:18:2: don't use ALL_CAPS in Go names; use CamelCase
tlv.go:19:2: don't use ALL_CAPS in Go names; use CamelCase
tlv.go:20:2: don't use ALL_CAPS in Go names; use CamelCase
tlv.go:21:2: don't use ALL_CAPS in Go names; use CamelCase
tlv.go:22:2: don't use ALL_CAPS in Go names; use CamelCase
tlv.go:23:2: don't use ALL_CAPS in Go names; use CamelCase
tlv.go:24:2: don't use ALL_CAPS in Go names; use CamelCase
tlv.go:25:2: don't use ALL_CAPS in Go names; use CamelCase
tlv.go:27:2: comment on exported const PP2_TYPE_MIN_CUSTOM should be of the form "PP2_TYPE_MIN_CUSTOM ..."
tlv.go:28:2: don't use ALL_CAPS in Go names; use CamelCase
tlv.go:29:2: don't use ALL_CAPS in Go names; use CamelCase
tlv.go:30:2: don't use ALL_CAPS in Go names; use CamelCase
tlv.go:31:2: don't use ALL_CAPS in Go names; use CamelCase
tlv.go:32:2: don't use ALL_CAPS in Go names; use CamelCase
tlv.go:33:2: don't use ALL_CAPS in Go names; use CamelCase
tlv.go:37:2: exported var ErrTruncatedTLV should have comment or be unexported
version_cmd.go:7:2: exported const LOCAL should have comment (or a comment on this block) or be unexported
tlvparse/aws.go:13:2: comment on exported const PP2_TYPE_AWS should be of the form "PP2_TYPE_AWS ..."
tlvparse/aws.go:14:2: don't use ALL_CAPS in Go names; use CamelCase
tlvparse/aws.go:15:2: don't use ALL_CAPS in Go names; use CamelCase
tlvparse/aws.go:15:2: exported const PP2_SUBTYPE_AWS_VPCE_ID should have comment (or a comment on this block) or be unexported
tlvparse/aws.go:20:1: exported function IsAWSVPCEndpointID should have comment or be unexported
tlvparse/aws.go:24:1: exported function AWSVPCEndpointID should have comment or be unexported
tlvparse/azure.go:13:2: comment on exported const PP2_TYPE_AZURE should be of the form "PP2_TYPE_AZURE ..."
tlvparse/azure.go:14:2: don't use ALL_CAPS in Go names; use CamelCase
tlvparse/azure.go:15:2: don't use ALL_CAPS in Go names; use CamelCase
tlvparse/azure.go:15:2: exported const PP2_SUBTYPE_AZURE_PRIVATEENDPOINT_LINKID should have comment (or a comment on this block) or be unexported
tlvparse/ssl.go:12:2: comment on exported const PP2_BITFIELD_CLIENT_SSL should be of the form "PP2_BITFIELD_CLIENT_SSL ..."
tlvparse/ssl.go:13:2: don't use ALL_CAPS in Go names; use CamelCase
tlvparse/ssl.go:14:2: don't use ALL_CAPS in Go names; use CamelCase
tlvparse/ssl.go:14:2: exported const PP2_BITFIELD_CLIENT_CERT_CONN should have comment (or a comment on this block) or be unexported
tlvparse/ssl.go:15:2: don't use ALL_CAPS in Go names; use CamelCase
tlvparse/ssl.go:20:1: comment on exported type PP2SSL should be of the form "PP2SSL ..." (with optional leading article)
tlvparse/ssl.go:97:1: comment on exported function IsSSL should be of the form "IsSSL ..."
tlvparse/ssl.go:149:1: comment on exported function FindSSL should be of the form "FindSSL ..."

😭

Don't forget to set --set_exit_status so CI fails builds if lint is violated.

Nginx proxy_protocol doesn't work.

Hi, I've used Nginx as a load balance, and use example/server or example/http as backend.
But I can't get real IP from nginx even if I have used the configuration of Nginx like proxy_protocol on

stream {

upstream my_servers{
#least_conn;
server 1.2.3.4:22;
#proxy_protocol      on;
}
server {
listen [::]:223 ipv6only=off;
listen [::]:223 ipv6only=off udp;
proxy_pass my_servers;
proxy_protocol      on;

}
}

The error was: proxyproto: invalid address or it just give me the IP of Nginx.
Because I didn't find any thing relative to the problem, so I open the issue asking you for help.
I will be appreciate if you can look deep into the problem.

support tls?

hi, when the server listen mode is tls, it isn't work. can you support tls listen?

Revisit tests to reduce duplicated code

For instance, in protocol_test.go, there are many repetitions of code that binds to a socket (listen), accepts connections, writes and reads data from each connection. I think these can be condensed to a low single digit number of parameterized functions that are then called in the tests.

Fix CI

Maybe it's time to adopt Github Actions but until then we must fix existing CI.

[Security] Workflow test.yml is using vulnerable action shogo82148/actions-goveralls

The workflow test.yml is referencing action shogo82148/actions-goveralls using references v1.2.2. However this reference is missing the commit 93213fdb0bf003e512edcc55e1bdca000f7e4d65 which may contain fix to the some vulnerability.
The vulnerability fix that is missing by actions version could be related to:
(1) CVE fix
(2) upgrade of vulnerable dependency
(3) fix to secret leak and others.
Please consider to update the reference to the action.

Export underlying connection

Hi,

It would be good to export the underlying proxyproto.Conn.conn connection.
Since proxyproto.Conn is not an net.TCPConn, and I need to cast it.

SourceChecker support?

Hi @pires ,

I know you created this after wanting v2 support in armon/go-proxyproto and it looks like this library is pretty well thought out. I'd love to have some projects at HC use this for the v2 support but one thing we rely on is the ability to do callbacks for making decisions -- see https://godoc.org/github.com/armon/go-proxyproto#SourceChecker

Not saying that is the correct interface necessarily (in particular it should probably take in and write out structs so that we can add other parameters to help make better decisions), but would you be amenable to something like that in this library?

Suggestion: Decouple Header Reading From net.Conn

Having a sync.Once be checked in each net.Conn operation (i.e. on every Read()) seems quite inefficient. I suggest enqueuing accepted connections from the inner net.Listener's Accept(), processing/reading the header asynchronously, and having the publicly exposed net.Listener return the already-processed net.Conns...

Here's some pseudocode to illustrate what I mean:

import "net"

type listener struct {
	inner net.Listener

	in  chan net.Conn
	out chan net.Conn
}

// function that reads the proxy protocol header and returns a net.Conn
// implementation which returns the right address on RemoteAddr()
func processFunc(in net.Conn) net.Conn {
    // assume implemented
}

func NewListener(inner net.Listener) net.Listener {
	wrapped := &listener{
		inner: inner,
		in:    make(chan net.Conn, 1000),
		out:   make(chan net.Conn, 1000),
	}

	// kick-off go routine to process newly accepted connections
	go func() {
		for conn := range wrapped.in {
			conn := conn // avoid address capture
			go func() {
				processed := processFunc(conn)
				if processed != nil {
					wrapped.out <- processed
				}
			}()
		}
	}()

	// kick-off go routine to accept new connections
	go func() {
		for {
			conn, err := inner.Accept()
			if err != nil {
				return // accept errors are not recoverable
			}
			wrapped.in <- conn
		}
	}()

	return wrapped
}

func (l *listener) Accept() (net.Conn, error) {
	return <-l.out, nil
}

func (l *listener) Close() error {
	return l.inner.Close()
}

func (l *listener) Addr() net.Addr {
	return l.inner.Addr()
}

If there's interest in this I would love to submit a PR... seems indeed more efficient!

Give constants a type

Right now it's not easy to tell that UNSPEC/TCPv4/etc are AddressFamilyAndProtocol. Somethign like this would help:

const (
	UNSPEC      AddressFamilyAndProtocol = '\x00'
	TCPv4       AddressFamilyAndProtocol = '\x11'
	…
)

Proxy Protocol to Fake IP example service, yes/no? alt?

I have a scenario which I need to pass the original IP address to a legacy service that doesn't support PROXY protocol at all.
Another issue with the infrastructure(cloud vendor) is that the LB service will not fake the originating source IP and enforces strict IP+mac pairs.
So for temporary solution I am trying to compose a proxy service that will receive PROXY protocol and will open a new local connection on the 'lo' interface with the fake source IP.
It's on Linux so I will try to use TPROXY.
I will be happy for suggestions.

  • The same issue exists ontop of AWS/OpenStack/Others and can be tested there. I am almost sure that F5 integration inside OpenStack some how have a resolution for this issue.

Varnish probe fails with PROXY v2

Hi,

we use go-proxyproto for some stub backends for testing purpose and it fails when we tell Varnish to use v2 while v1 works.
The connection between Varnish and HAProxy works fine so I suspect something's wrong within go-proxyproto.
Here's anything required for a testcase:

package main
import (
	"fmt"
	"net"
	"net/http"
	"github.com/pires/go-proxyproto"
)
func main() {
    handler := func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
    }
    srv := &http.Server{
        Handler: http.HandlerFunc(handler),
        Addr:    fmt.Sprintf("127.0.0.1:%d", 14240),
    }
    ln, err := net.Listen("tcp", srv.Addr)
    if err != nil {
        fmt.Println(err)
    }

    policyFunc := func(upstream net.Addr) (proxyproto.Policy, error) { return proxyproto.REQUIRE, nil }
    proxyListener := &proxyproto.Listener{Listener: ln, Policy: policyFunc}
    srv.Serve(proxyListener) //nolint:errcheck
}
go build test.go
./test

Varnish 6.4.0 (or later):
example.vcl:

vcl 4.1;
backend default {
    .host = "127.0.0.1";
    .port = "14240";
    .proxy_header = 1;

            .probe = {
                .request =
                        "GET /test HTTP/1.1"
                        "Host: www.example.com"
                        "User-Agent: varnish"
                        "Connection: close";
                .interval = 5s;
                .timeout = 3s;
                .window = 8;
                .threshold = 3;
                .expected_response = 200;
        }

}
varnishd -f /etc/varnish/example.vcl -F
varnishlog -n /var/lib/varnish/$(hostname) -g raw -i Backend_health

Now varnishlog should report:

0 Backend_health - default Still healthy 4---X-RH 4 3 8 0.000505 0.000226 "HTTP/1.1 200 OK"

Now change .proxy_header from 1 to 2 in example.vcl:

    .proxy_header = 2;

And re-run the above testcase.
varnishlog will now log:

0 Backend_health - default Still sick 4---X-R- 0 3 8 0.000322 0.000000 "HTTP/1.1 400 Bad Request"

So the probe fails. Looking into a tcpdump what it does is to send a QUIT via PROXYv2 before doing the probe request and I suspected that's the reason for the 400.

This might be related: kubernetes/kubernetes#57250 (comment)

parseVersion1() is not secure

func parseVersion1(reader *bufio.Reader) (*Header, error) {
	// Read until LF shows up, otherwise fail.
	// At this point, can't be sure CR precedes LF which will be validated next.
	line, err := reader.ReadString('\n')

The reader is a default bufio.Reader wrapping a net.Conn. It will read from the connection until it finds a newline. Since no limits are implemented in the code, a deliberately malformed V1 header could be used to exhaust memory in a server process using this code - a form of DDoS. The exploit is simple: send a stream starting with "PROXY" and keep sending data (which does not contain a newline) until the target stops acknowledging.

In most real world circumstances, the actual risk is small since only trusted sources should be allowed to send proxy protocol headers. However, this is still a security issue and should be resolved.

Easiest fix: reader.Peek(107) and scan for a newline. If none is found, then it is not a valid version 1 header anyway, so you can fail fast (the maximum v1 header size is 107 bytes). Otherwise, proceed with the reader.ReadString('\n') in full confidence.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.