Skip to content

Commit

Permalink
WIP implement IP proxying
Browse files Browse the repository at this point in the history
  • Loading branch information
marten-seemann committed Oct 19, 2024
1 parent d8c074a commit 5645881
Show file tree
Hide file tree
Showing 9 changed files with 886 additions and 21 deletions.
38 changes: 20 additions & 18 deletions capsule.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,25 +164,27 @@ func parseAddress(r io.Reader) (requestID uint64, prefix netip.Prefix, _ error)

// routeAdvertisementCapsule represents a ROUTE_ADVERTISEMENT capsule
type routeAdvertisementCapsule struct {
IPAddressRanges []IPAddressRange
IPAddressRanges []IPRoute
}

// IPAddressRange represents an IP Address Range within a ROUTE_ADVERTISEMENT capsule
type IPAddressRange struct {
StartIP netip.Addr
EndIP netip.Addr
// IPRoute represents an IP Address Range
type IPRoute struct {
StartIP netip.Addr
EndIP netip.Addr
// IPProtocol is the Internet Protocol Number for traffic that can be sent to this range.
// If the value is 0, all protocols are allowed.
IPProtocol uint8
}

func (r IPAddressRange) len() int { return 1 + r.StartIP.BitLen()/8 + r.EndIP.BitLen()/8 + 1 }
func (r IPRoute) len() int { return 1 + r.StartIP.BitLen()/8 + r.EndIP.BitLen()/8 + 1 }

// Prefixes returns the prefixes that this IP address range covers.
// Note that depending on the start and end addresses,
// this conversion can result in a large number of prefixes.
func (r IPAddressRange) Prefixes() []netip.Prefix { return rangeToPrefixes(r.StartIP, r.EndIP) }
func (r IPRoute) Prefixes() []netip.Prefix { return rangeToPrefixes(r.StartIP, r.EndIP) }

func parseRouteAdvertisementCapsule(r io.Reader) (*routeAdvertisementCapsule, error) {
var ranges []IPAddressRange
var ranges []IPRoute
for {
ipRange, err := parseIPAddressRange(r)
if err != nil {
Expand Down Expand Up @@ -218,47 +220,47 @@ func (c *routeAdvertisementCapsule) append(b []byte) []byte {
return b
}

func parseIPAddressRange(r io.Reader) (IPAddressRange, error) {
func parseIPAddressRange(r io.Reader) (IPRoute, error) {
var ipVersion uint8
if err := binary.Read(r, binary.LittleEndian, &ipVersion); err != nil {
return IPAddressRange{}, err
return IPRoute{}, err
}

var startIP, endIP netip.Addr
switch ipVersion {
case 4:
var start, end [4]byte
if _, err := io.ReadFull(r, start[:]); err != nil {
return IPAddressRange{}, err
return IPRoute{}, err
}
if _, err := io.ReadFull(r, end[:]); err != nil {
return IPAddressRange{}, err
return IPRoute{}, err
}
startIP = netip.AddrFrom4(start)
endIP = netip.AddrFrom4(end)
case 6:
var start, end [16]byte
if _, err := io.ReadFull(r, start[:]); err != nil {
return IPAddressRange{}, err
return IPRoute{}, err
}
if _, err := io.ReadFull(r, end[:]); err != nil {
return IPAddressRange{}, err
return IPRoute{}, err
}
startIP = netip.AddrFrom16(start)
endIP = netip.AddrFrom16(end)
default:
return IPAddressRange{}, fmt.Errorf("invalid IP version: %d", ipVersion)
return IPRoute{}, fmt.Errorf("invalid IP version: %d", ipVersion)
}

if startIP.Compare(endIP) > 0 {
return IPAddressRange{}, errors.New("start IP is greater than end IP")
return IPRoute{}, errors.New("start IP is greater than end IP")
}

var ipProtocol uint8
if err := binary.Read(r, binary.LittleEndian, &ipProtocol); err != nil {
return IPAddressRange{}, err
return IPRoute{}, err
}
return IPAddressRange{
return IPRoute{
StartIP: startIP,
EndIP: endIP,
IPProtocol: ipProtocol,
Expand Down
4 changes: 2 additions & 2 deletions capsule_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ func TestParseRouteAdvertisementCapsule(t *testing.T) {
capsule, err := parseRouteAdvertisementCapsule(cr)
require.NoError(t, err)
require.Equal(t,
[]IPAddressRange{
[]IPRoute{
{StartIP: netip.MustParseAddr("1.1.1.1"), EndIP: netip.MustParseAddr("1.2.3.4"), IPProtocol: 13},
{StartIP: netip.MustParseAddr("2001:db8::1"), EndIP: netip.MustParseAddr("2001:db8::100"), IPProtocol: 37},
},
Expand All @@ -240,7 +240,7 @@ func TestParseRouteAdvertisementCapsule(t *testing.T) {

func TestWriteRouteAdvertisementCapsule(t *testing.T) {
c := &routeAdvertisementCapsule{
IPAddressRanges: []IPAddressRange{
IPAddressRanges: []IPRoute{
{StartIP: netip.MustParseAddr("1.1.1.1"), EndIP: netip.MustParseAddr("1.2.3.4"), IPProtocol: 13},
{StartIP: netip.MustParseAddr("2001:db8::1"), EndIP: netip.MustParseAddr("2001:db8::100"), IPProtocol: 37},
},
Expand Down
89 changes: 89 additions & 0 deletions cert_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package connectip

import (
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"log"
"math/big"
"time"

"github.com/quic-go/quic-go/http3"
)

var (
tlsConf *tls.Config
certPool *x509.CertPool
)

func generateCA() (*x509.Certificate, *rsa.PrivateKey, error) {
certTempl := &x509.Certificate{
SerialNumber: big.NewInt(2019),
Subject: pkix.Name{},
NotBefore: time.Now(),
NotAfter: time.Now().Add(24 * time.Hour),
IsCA: true,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
BasicConstraintsValid: true,
}
caPrivateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, nil, err
}
caBytes, err := x509.CreateCertificate(rand.Reader, certTempl, certTempl, &caPrivateKey.PublicKey, caPrivateKey)
if err != nil {
return nil, nil, err
}
ca, err := x509.ParseCertificate(caBytes)
if err != nil {
return nil, nil, err
}
return ca, caPrivateKey, nil
}

func generateLeafCert(ca *x509.Certificate, caPrivateKey *rsa.PrivateKey) (*x509.Certificate, *rsa.PrivateKey, error) {
certTempl := &x509.Certificate{
SerialNumber: big.NewInt(1),
DNSNames: []string{"localhost", "127.0.0.1"},
NotBefore: time.Now(),
NotAfter: time.Now().Add(24 * time.Hour),
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
KeyUsage: x509.KeyUsageDigitalSignature,
}
privKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, nil, err
}
certBytes, err := x509.CreateCertificate(rand.Reader, certTempl, ca, &privKey.PublicKey, caPrivateKey)
if err != nil {
return nil, nil, err
}
cert, err := x509.ParseCertificate(certBytes)
if err != nil {
return nil, nil, err
}
return cert, privKey, nil
}

func init() {
ca, caPrivateKey, err := generateCA()
if err != nil {
log.Fatal("failed to generate CA certificate:", err)
}
leafCert, leafPrivateKey, err := generateLeafCert(ca, caPrivateKey)
if err != nil {
log.Fatal("failed to generate leaf certificate:", err)
}
certPool = x509.NewCertPool()
certPool.AddCert(ca)
tlsConf = &tls.Config{
Certificates: []tls.Certificate{{
Certificate: [][]byte{leafCert.Raw},
PrivateKey: leafPrivateKey,
}},
NextProtos: []string{http3.NextProtoH3},
}
}
62 changes: 62 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package connectip

import (
"context"
"errors"
"fmt"
"net/http"
"net/url"

"github.com/quic-go/quic-go/http3"
"github.com/yosida95/uritemplate/v3"
)

// Dial dials a proxied connection to a target server.
func Dial(ctx context.Context, conn *http3.ClientConn, template *uritemplate.Template) (*Conn, *http.Response, error) {
if len(template.Varnames()) > 0 {
return nil, nil, errors.New("connect-ip-go currently does not support IP flow forwarding")
}

Check warning on line 18 in client.go

View check run for this annotation

Codecov / codecov/patch

client.go#L17-L18

Added lines #L17 - L18 were not covered by tests

u, err := url.Parse(template.Raw())
if err != nil {
return nil, nil, fmt.Errorf("connect-ip: failed to parse URI: %w", err)
}

Check warning on line 23 in client.go

View check run for this annotation

Codecov / codecov/patch

client.go#L22-L23

Added lines #L22 - L23 were not covered by tests

select {
case <-ctx.Done():
return nil, nil, context.Cause(ctx)
case <-conn.Context().Done():
return nil, nil, context.Cause(conn.Context())

Check warning on line 29 in client.go

View check run for this annotation

Codecov / codecov/patch

client.go#L26-L29

Added lines #L26 - L29 were not covered by tests
case <-conn.ReceivedSettings():
}
settings := conn.Settings()
if !settings.EnableExtendedConnect {
return nil, nil, errors.New("connect-ip: server didn't enable Extended CONNECT")
}

Check warning on line 35 in client.go

View check run for this annotation

Codecov / codecov/patch

client.go#L34-L35

Added lines #L34 - L35 were not covered by tests
if !settings.EnableDatagrams {
return nil, nil, errors.New("connect-ip: server didn't enable Datagrams")
}

Check warning on line 38 in client.go

View check run for this annotation

Codecov / codecov/patch

client.go#L37-L38

Added lines #L37 - L38 were not covered by tests

rstr, err := conn.OpenRequestStream(ctx)
if err != nil {
return nil, nil, fmt.Errorf("connect-ip: failed to open request stream: %w", err)
}

Check warning on line 43 in client.go

View check run for this annotation

Codecov / codecov/patch

client.go#L42-L43

Added lines #L42 - L43 were not covered by tests
if err := rstr.SendRequestHeader(&http.Request{
Method: http.MethodConnect,
Proto: requestProtocol,
Host: u.Host,
Header: http.Header{capsuleHeader: []string{capsuleProtocolHeaderValue}},
URL: u,
}); err != nil {
return nil, nil, fmt.Errorf("connect-ip: failed to send request: %w", err)
}

Check warning on line 52 in client.go

View check run for this annotation

Codecov / codecov/patch

client.go#L51-L52

Added lines #L51 - L52 were not covered by tests
// TODO: optimistically return the connection
rsp, err := rstr.ReadResponse()
if err != nil {
return nil, nil, fmt.Errorf("connect-ip: failed to read response: %w", err)
}

Check warning on line 57 in client.go

View check run for this annotation

Codecov / codecov/patch

client.go#L56-L57

Added lines #L56 - L57 were not covered by tests
if rsp.StatusCode < 200 || rsp.StatusCode > 299 {
return nil, rsp, fmt.Errorf("connect-ip: server responded with %d", rsp.StatusCode)
}

Check warning on line 60 in client.go

View check run for this annotation

Codecov / codecov/patch

client.go#L59-L60

Added lines #L59 - L60 were not covered by tests
return newProxiedConn(rstr), rsp, nil
}
Loading

0 comments on commit 5645881

Please sign in to comment.