ce7cb702218693d08ea6d83464d2457c9250027c
This commit is contained in:
commit
34c5f8963c
14 changed files with 145336 additions and 0 deletions
59
services/oci-l4-relay/go.mod
Normal file
59
services/oci-l4-relay/go.mod
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
module quorate.dev/services/oci-l4-relay
|
||||
|
||||
go 1.26.4
|
||||
|
||||
require (
|
||||
github.com/google/nftables v0.3.0
|
||||
github.com/vishvananda/netlink v1.3.1
|
||||
golang.org/x/sys v0.38.0
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
|
||||
k8s.io/api v0.35.3
|
||||
k8s.io/apimachinery v0.35.3
|
||||
k8s.io/client-go v0.35.3
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/google/gnostic-models v0.7.0 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mdlayher/genetlink v1.3.2 // indirect
|
||||
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 // indirect
|
||||
github.com/mdlayher/socket v0.5.1 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/vishvananda/netns v0.0.5 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/crypto v0.44.0 // indirect
|
||||
golang.org/x/net v0.47.0 // indirect
|
||||
golang.org/x/oauth2 v0.30.0 // indirect
|
||||
golang.org/x/sync v0.18.0 // indirect
|
||||
golang.org/x/term v0.37.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
golang.org/x/time v0.9.0 // indirect
|
||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect
|
||||
google.golang.org/protobuf v1.36.8 // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect
|
||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
|
||||
sigs.k8s.io/yaml v1.6.0 // indirect
|
||||
)
|
||||
153
services/oci-l4-relay/go.sum
Normal file
153
services/oci-l4-relay/go.sum
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
||||
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
|
||||
github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
|
||||
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
|
||||
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
||||
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
|
||||
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
|
||||
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
|
||||
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
||||
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
||||
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
|
||||
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/nftables v0.3.0 h1:bkyZ0cbpVeMHXOrtlFc8ISmfVqq5gPJukoYieyVmITg=
|
||||
github.com/google/nftables v0.3.0/go.mod h1:BCp9FsrbF1Fn/Yu6CLUc9GGZFw/+hsxfluNXXmxBfRM=
|
||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
|
||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
|
||||
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
|
||||
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 h1:A1Cq6Ysb0GM0tpKMbdCXCIfBclan4oHk1Jb+Hrejirg=
|
||||
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42/go.mod h1:BB4YCPDOzfy7FniQ/lxuYQ3dgmM2cZumHbK8RpTjN2o=
|
||||
github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
|
||||
github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
|
||||
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
|
||||
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns=
|
||||
github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
|
||||
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
|
||||
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
|
||||
github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=
|
||||
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
|
||||
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
|
||||
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
|
||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 h1:3GDAcqdIg1ozBNLgPy4SLT84nfcBjr6rhGtXYtrkWLU=
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10/go.mod h1:T97yPqesLiNrOYxkwmhMI0ZIlJDm+p0PMR8eRVeR5tQ=
|
||||
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
|
||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
k8s.io/api v0.35.3 h1:pA2fiBc6+N9PDf7SAiluKGEBuScsTzd2uYBkA5RzNWQ=
|
||||
k8s.io/api v0.35.3/go.mod h1:9Y9tkBcFwKNq2sxwZTQh1Njh9qHl81D0As56tu42GA4=
|
||||
k8s.io/apimachinery v0.35.3 h1:MeaUwQCV3tjKP4bcwWGgZ/cp/vpsRnQzqO6J6tJyoF8=
|
||||
k8s.io/apimachinery v0.35.3/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
|
||||
k8s.io/client-go v0.35.3 h1:s1lZbpN4uI6IxeTM2cpdtrwHcSOBML1ODNTCCfsP1pg=
|
||||
k8s.io/client-go v0.35.3/go.mod h1:RzoXkc0mzpWIDvBrRnD+VlfXP+lRzqQjCmKtiwZ8Q9c=
|
||||
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=
|
||||
k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
|
||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
|
||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
|
||||
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
|
||||
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
|
||||
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
|
||||
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
|
||||
147
services/oci-l4-relay/k8s/deployment.yaml
Normal file
147
services/oci-l4-relay/k8s/deployment.yaml
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: oci-l4-relay
|
||||
spec:
|
||||
replicas: 2
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxUnavailable: 1
|
||||
maxSurge: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: oci-l4-relay
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: oci-l4-relay
|
||||
spec:
|
||||
serviceAccountName: oci-l4-relay
|
||||
automountServiceAccountToken: true
|
||||
hostNetwork: true
|
||||
dnsPolicy: ClusterFirstWithHostNet
|
||||
affinity:
|
||||
podAntiAffinity:
|
||||
requiredDuringSchedulingIgnoredDuringExecution:
|
||||
- labelSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: oci-l4-relay
|
||||
topologyKey: kubernetes.io/hostname
|
||||
priorityClassName: system-node-critical
|
||||
terminationGracePeriodSeconds: 35
|
||||
containers:
|
||||
- name: relay-agent
|
||||
image: registry.quorate.internal/quorate/oci-l4-relay-agent@sha256:c5d82adbde80a5a3c3e3ecbbc5aebf53d449474cbcfc2e7a7988576edf068dfc
|
||||
imagePullPolicy: IfNotPresent
|
||||
env:
|
||||
- name: POD_NAMESPACE
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.namespace
|
||||
- name: POD_NAME
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.name
|
||||
- name: POD_UID
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.uid
|
||||
- name: POD_IP
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: status.podIP
|
||||
- name: POD_NODE_NAME
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: spec.nodeName
|
||||
- name: RELAY_TUNNEL_ADDRESS
|
||||
value: 10.153.238.225/30
|
||||
- name: RELAY_TARGETS
|
||||
value: '[{"address":"10.104.40.127","ports":[80,443]}]'
|
||||
- name: RELAY_MAIL_UPSTREAM
|
||||
value: stalwart-relay.stalwart.svc.cluster.local
|
||||
- name: RELAY_LOCAL_RETURN_CIDRS
|
||||
value: 10.63.81.107/24,10.162.8.60/24
|
||||
- name: RELAY_METRICS_LISTEN_ADDRESS
|
||||
value: :19100
|
||||
- name: OCI_RELAY_METRICS_URL
|
||||
value: http://10.40.68.115:19100/metrics
|
||||
- name: OCI_RELAY_ENDPOINT_IP
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: oci-l4-relay-oci-wireguard
|
||||
key: endpoint_ip
|
||||
- name: OCI_RELAY_LISTEN_PORT
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: oci-l4-relay-oci-wireguard
|
||||
key: listen_port
|
||||
- name: OCI_RELAY_PUBLIC_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: oci-l4-relay-oci-wireguard
|
||||
key: public_key
|
||||
ports:
|
||||
- name: metrics
|
||||
containerPort: 19100
|
||||
protocol: TCP
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
add:
|
||||
- NET_ADMIN
|
||||
- NET_BIND_SERVICE
|
||||
readOnlyRootFilesystem: true
|
||||
runAsNonRoot: false
|
||||
runAsUser: 0
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
readinessProbe:
|
||||
exec:
|
||||
command:
|
||||
- /oci-l4-relay-agent
|
||||
- health
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
failureThreshold: 3
|
||||
livenessProbe:
|
||||
exec:
|
||||
command:
|
||||
- /oci-l4-relay-agent
|
||||
- health
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 10
|
||||
failureThreshold: 3
|
||||
resources:
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 32Mi
|
||||
limits:
|
||||
memory: 128Mi
|
||||
volumeMounts:
|
||||
- name: home-wireguard
|
||||
mountPath: /run/secrets/wireguard
|
||||
readOnly: true
|
||||
- name: runtime
|
||||
mountPath: /run/relay-agent
|
||||
volumes:
|
||||
- name: home-wireguard
|
||||
secret:
|
||||
secretName: oci-l4-relay-home-wireguard
|
||||
defaultMode: 0400
|
||||
- name: runtime
|
||||
emptyDir:
|
||||
medium: Memory
|
||||
---
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: oci-l4-relay
|
||||
spec:
|
||||
minAvailable: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: oci-l4-relay
|
||||
10
services/oci-l4-relay/k8s/kustomization.yaml
Normal file
10
services/oci-l4-relay/k8s/kustomization.yaml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
namespace: kube-system
|
||||
resources:
|
||||
- service-account.yaml
|
||||
- lease-rbac.yaml
|
||||
- vault-secret.yaml
|
||||
- deployment.yaml
|
||||
- metrics-endpoint-slice.yaml
|
||||
- service.yaml
|
||||
42
services/oci-l4-relay/k8s/lease-rbac.yaml
Normal file
42
services/oci-l4-relay/k8s/lease-rbac.yaml
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
apiVersion: coordination.k8s.io/v1
|
||||
kind: Lease
|
||||
metadata:
|
||||
name: oci-l4-relay
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: oci-l4-relay-lease
|
||||
rules:
|
||||
- apiGroups:
|
||||
- coordination.k8s.io
|
||||
resources:
|
||||
- leases
|
||||
resourceNames:
|
||||
- oci-l4-relay
|
||||
verbs:
|
||||
- get
|
||||
- update
|
||||
- patch
|
||||
- apiGroups:
|
||||
- discovery.k8s.io
|
||||
resources:
|
||||
- endpointslices
|
||||
resourceNames:
|
||||
- oci-l4-relay-metrics
|
||||
verbs:
|
||||
- get
|
||||
- update
|
||||
- patch
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: oci-l4-relay-lease
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
name: oci-l4-relay-lease
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: oci-l4-relay
|
||||
12
services/oci-l4-relay/k8s/metrics-endpoint-slice.yaml
Normal file
12
services/oci-l4-relay/k8s/metrics-endpoint-slice.yaml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
apiVersion: discovery.k8s.io/v1
|
||||
kind: EndpointSlice
|
||||
metadata:
|
||||
name: oci-l4-relay-metrics
|
||||
labels:
|
||||
kubernetes.io/service-name: oci-l4-relay-metrics
|
||||
addressType: IPv4
|
||||
ports:
|
||||
- name: metrics
|
||||
protocol: TCP
|
||||
port: 19100
|
||||
endpoints: []
|
||||
7
services/oci-l4-relay/k8s/service-account.yaml
Normal file
7
services/oci-l4-relay/k8s/service-account.yaml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: oci-l4-relay
|
||||
automountServiceAccountToken: true
|
||||
imagePullSecrets:
|
||||
- name: quay-cluster-pull
|
||||
13
services/oci-l4-relay/k8s/service.yaml
Normal file
13
services/oci-l4-relay/k8s/service.yaml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: oci-l4-relay-metrics
|
||||
labels:
|
||||
app.kubernetes.io/name: oci-l4-relay
|
||||
app.kubernetes.io/component: metrics
|
||||
spec:
|
||||
ports:
|
||||
- name: metrics
|
||||
port: 19100
|
||||
targetPort: metrics
|
||||
protocol: TCP
|
||||
92
services/oci-l4-relay/k8s/vault-secret.yaml
Normal file
92
services/oci-l4-relay/k8s/vault-secret.yaml
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
apiVersion: secrets.hashicorp.com/v1beta1
|
||||
kind: VaultAuth
|
||||
metadata:
|
||||
name: oci-l4-relay
|
||||
spec:
|
||||
method: kubernetes
|
||||
mount: kubernetes
|
||||
kubernetes:
|
||||
role: oci-l4-relay
|
||||
serviceAccount: oci-l4-relay
|
||||
vaultConnectionRef: vault/vault
|
||||
---
|
||||
apiVersion: secrets.hashicorp.com/v1beta1
|
||||
kind: VaultStaticSecret
|
||||
metadata:
|
||||
name: oci-l4-relay-home-wireguard
|
||||
spec:
|
||||
type: kv-v2
|
||||
mount: secret
|
||||
path: oci/relay/home-wireguard
|
||||
destination:
|
||||
name: oci-l4-relay-home-wireguard
|
||||
create: true
|
||||
overwrite: true
|
||||
transformation:
|
||||
excludeRaw: true
|
||||
excludes:
|
||||
- .*
|
||||
templates:
|
||||
private_key:
|
||||
text: '{{ get .Secrets "private_key" }}'
|
||||
rolloutRestartTargets:
|
||||
- kind: Deployment
|
||||
name: oci-l4-relay
|
||||
vaultAuthRef: oci-l4-relay
|
||||
---
|
||||
apiVersion: secrets.hashicorp.com/v1beta1
|
||||
kind: VaultStaticSecret
|
||||
metadata:
|
||||
name: quay-cluster-pull
|
||||
spec:
|
||||
type: kv-v2
|
||||
mount: secret
|
||||
path: quay/robots/cluster_pull
|
||||
destination:
|
||||
name: quay-cluster-pull
|
||||
create: true
|
||||
overwrite: true
|
||||
type: kubernetes.io/dockerconfigjson
|
||||
transformation:
|
||||
excludeRaw: true
|
||||
excludes:
|
||||
- .*
|
||||
templates:
|
||||
.dockerconfigjson:
|
||||
text: >-
|
||||
{"auths":{"registry.quorate.internal":{"auth":"{{ printf "%s:%s" (get .Secrets "username") (get .Secrets "token") | b64enc }}"}}}
|
||||
refreshAfter: 1h
|
||||
rolloutRestartTargets:
|
||||
- kind: Deployment
|
||||
name: oci-l4-relay
|
||||
- kind: Deployment
|
||||
name: oci-l4-relay-wan-controller
|
||||
vaultAuthRef: oci-l4-relay
|
||||
---
|
||||
apiVersion: secrets.hashicorp.com/v1beta1
|
||||
kind: VaultStaticSecret
|
||||
metadata:
|
||||
name: oci-l4-relay-oci-wireguard
|
||||
spec:
|
||||
type: kv-v2
|
||||
mount: secret
|
||||
path: oci/relay/peer
|
||||
destination:
|
||||
name: oci-l4-relay-oci-wireguard
|
||||
create: true
|
||||
overwrite: true
|
||||
transformation:
|
||||
excludeRaw: true
|
||||
excludes:
|
||||
- .*
|
||||
templates:
|
||||
endpoint_ip:
|
||||
text: '{{ get .Secrets "endpoint_ip" }}'
|
||||
listen_port:
|
||||
text: '{{ get .Secrets "listen_port" }}'
|
||||
public_key:
|
||||
text: '{{ get .Secrets "public_key" }}'
|
||||
rolloutRestartTargets:
|
||||
- kind: Deployment
|
||||
name: oci-l4-relay
|
||||
vaultAuthRef: oci-l4-relay
|
||||
1210
services/oci-l4-relay/main.go
Normal file
1210
services/oci-l4-relay/main.go
Normal file
File diff suppressed because it is too large
Load diff
329
services/oci-l4-relay/main_test.go
Normal file
329
services/oci-l4-relay/main_test.go
Normal file
|
|
@ -0,0 +1,329 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/nftables"
|
||||
"github.com/google/nftables/expr"
|
||||
discoveryv1 "k8s.io/api/discovery/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
)
|
||||
|
||||
type roundTripperFunc func(*http.Request) (*http.Response, error)
|
||||
|
||||
func (function roundTripperFunc) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||
return function(request)
|
||||
}
|
||||
|
||||
func TestRepeatElectionAfterTermFailure(t *testing.T) {
|
||||
ctx, stop := context.WithCancel(context.Background())
|
||||
var attempts atomic.Int32
|
||||
repeatElection(ctx, time.Millisecond, 10*time.Millisecond, func(_ context.Context, cancelElection context.CancelFunc) bool {
|
||||
if attempts.Add(1) == 1 {
|
||||
cancelElection()
|
||||
return false
|
||||
}
|
||||
stop()
|
||||
return false
|
||||
})
|
||||
if attempts.Load() != 2 {
|
||||
t.Fatalf("expected two election attempts, got %d", attempts.Load())
|
||||
}
|
||||
}
|
||||
|
||||
func TestElectionRetryDelay(t *testing.T) {
|
||||
base := 5 * time.Second
|
||||
maximum := 60 * time.Second
|
||||
for unhealthyTerms, expected := range map[int]time.Duration{
|
||||
0: base,
|
||||
1: base,
|
||||
2: 10 * time.Second,
|
||||
3: 20 * time.Second,
|
||||
4: 40 * time.Second,
|
||||
5: maximum,
|
||||
20: maximum,
|
||||
} {
|
||||
if actual := electionRetryDelay(base, maximum, unhealthyTerms); actual != expected {
|
||||
t.Fatalf("terms=%d: got %s, want %s", unhealthyTerms, actual, expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseCIDRPreservesHostAddress(t *testing.T) {
|
||||
address, err := parseCIDR("10.153.238.225/30")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := address.String(); got != "10.153.238.225/30" {
|
||||
t.Fatalf("got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseMetricsTarget(t *testing.T) {
|
||||
tunnel, err := parseCIDR("10.153.238.225/30")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
target, err := parseMetricsTarget("http://10.40.68.115:19100/metrics", tunnel)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if target.String() != "http://10.40.68.115:19100/metrics" {
|
||||
t.Fatalf("unexpected target %q", target)
|
||||
}
|
||||
for _, value := range []string{
|
||||
"https://10.40.68.115:19100/metrics",
|
||||
"http://10.153.238.225:19100/metrics",
|
||||
"http://10.40.68.115:19100/other",
|
||||
"http://10.40.68.115:19100/metrics?query=true",
|
||||
"http://192.0.2.1:19100/metrics",
|
||||
} {
|
||||
if _, err := parseMetricsTarget(value, tunnel); err == nil {
|
||||
t.Fatalf("expected %q to fail", value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetricsHandlerRequiresActiveOwner(t *testing.T) {
|
||||
target, err := url.Parse("http://10.40.68.115:19100/metrics")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
client := &http.Client{Transport: roundTripperFunc(func(request *http.Request) (*http.Response, error) {
|
||||
if request.URL.String() != target.String() {
|
||||
t.Fatalf("unexpected upstream request %q", request.URL)
|
||||
}
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Header: http.Header{"Content-Type": []string{"text/plain; version=0.0.4"}},
|
||||
Body: io.NopCloser(strings.NewReader("relay_metric 1\n")),
|
||||
}, nil
|
||||
})}
|
||||
instance := &relay{config: config{metricsTarget: target}, metricsClient: client}
|
||||
inactive := httptest.NewRecorder()
|
||||
instance.metricsHandler().ServeHTTP(inactive, httptest.NewRequest(http.MethodGet, "/metrics", nil))
|
||||
if inactive.Code != http.StatusServiceUnavailable {
|
||||
t.Fatalf("inactive status = %d", inactive.Code)
|
||||
}
|
||||
instance.active.Store(true)
|
||||
active := httptest.NewRecorder()
|
||||
instance.metricsHandler().ServeHTTP(active, httptest.NewRequest(http.MethodGet, "/metrics", nil))
|
||||
if active.Code != http.StatusOK || active.Body.String() != "relay_metric 1\n" || active.Header().Get("Content-Type") != "text/plain; version=0.0.4" {
|
||||
t.Fatalf("unexpected active response: status=%d content-type=%q body=%q", active.Code, active.Header().Get("Content-Type"), active.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetricsEndpointFollowsOwner(t *testing.T) {
|
||||
client := fake.NewSimpleClientset(&discoveryv1.EndpointSlice{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "oci-l4-relay-metrics", Namespace: "kube-system"},
|
||||
AddressType: discoveryv1.AddressTypeIPv4,
|
||||
})
|
||||
owner := &relay{
|
||||
client: client,
|
||||
config: config{
|
||||
namespace: "kube-system",
|
||||
podName: "relay-owner",
|
||||
podUID: "owner-uid",
|
||||
podIP: "10.167.40.23",
|
||||
podNodeName: "node-a",
|
||||
metricsSliceName: "oci-l4-relay-metrics",
|
||||
},
|
||||
}
|
||||
if err := owner.publishMetricsEndpoint(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
slice, err := client.DiscoveryV1().EndpointSlices("kube-system").Get(context.Background(), "oci-l4-relay-metrics", metav1.GetOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !owner.metricsEndpointMatches(slice) {
|
||||
t.Fatalf("endpoint does not match owner: %#v", slice.Endpoints)
|
||||
}
|
||||
standby := &relay{client: client, config: config{namespace: "kube-system", podUID: "standby-uid", metricsSliceName: "oci-l4-relay-metrics"}}
|
||||
if err := standby.clearMetricsEndpoint(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
slice, err = client.DiscoveryV1().EndpointSlices("kube-system").Get(context.Background(), "oci-l4-relay-metrics", metav1.GetOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(slice.Endpoints) != 1 {
|
||||
t.Fatal("standby cleared the active owner's endpoint")
|
||||
}
|
||||
if err := owner.clearMetricsEndpoint(context.Background()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
slice, err = client.DiscoveryV1().EndpointSlices("kube-system").Get(context.Background(), "oci-l4-relay-metrics", metav1.GetOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(slice.Endpoints) != 0 {
|
||||
t.Fatal("owner endpoint was not cleared")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllowedRuleContainsGatewayAndPort(t *testing.T) {
|
||||
rule := allowedRule(net.ParseIP("10.104.40.127"), 443)
|
||||
if len(rule) != 12 {
|
||||
t.Fatalf("got %d expressions", len(rule))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetricsReturnRuleIsBoundedToEstablishedPeerTraffic(t *testing.T) {
|
||||
rule := metricsReturnRule(net.ParseIP("10.153.238.225"), net.ParseIP("10.40.68.115"), 19100)
|
||||
if len(rule) != 16 {
|
||||
t.Fatalf("got %d expressions", len(rule))
|
||||
}
|
||||
comparison, ok := rule[14].(*expr.Cmp)
|
||||
if !ok || comparison.Op != expr.CmpOpNeq {
|
||||
t.Fatal("metrics return rule does not require a matching conntrack state")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRelayTargets(t *testing.T) {
|
||||
targets, err := parseRelayTargets(`[{"address":"10.66.153.202","ports":[993,25,465]},{"address":"10.104.40.127","ports":[443,80]}]`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(targets) != 2 ||
|
||||
targets[0].address.String() != "10.104.40.127" ||
|
||||
targets[1].address.String() != "10.66.153.202" ||
|
||||
targets[1].ports[0] != 25 ||
|
||||
targets[1].ports[2] != 993 {
|
||||
t.Fatalf("unexpected normalized targets: %#v", targets)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRelayTargetsRejectsAmbiguousMappings(t *testing.T) {
|
||||
for name, value := range map[string]string{
|
||||
"unknown field": `[{"address":"10.104.40.127","ports":[80],"name":"web"}]`,
|
||||
"duplicate port": `[{"address":"10.104.40.127","ports":[80]},{"address":"10.66.153.202","ports":[80]}]`,
|
||||
"unknown port": `[{"address":"10.104.40.127","ports":[22]}]`,
|
||||
"submission": `[{"address":"10.66.153.202","ports":[587]}]`,
|
||||
"duplicate IP": `[{"address":"10.104.40.127","ports":[80]},{"address":"10.104.40.127","ports":[443]}]`,
|
||||
"IPv6": `[{"address":"2001:db8::1","ports":[443]}]`,
|
||||
"trailing": `[{"address":"10.104.40.127","ports":[80]}] true`,
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if _, err := parseRelayTargets(value); err == nil {
|
||||
t.Fatal("expected target validation failure")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseNetworks(t *testing.T) {
|
||||
networks, err := parseNetworks("10.63.81.107/24, 10.162.8.60/24")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(networks) != 2 || networks[0].String() != "10.63.81.107/24" || networks[1].String() != "10.162.8.60/24" {
|
||||
t.Fatalf("unexpected networks: %v", networks)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFirewallCountersFromRules(t *testing.T) {
|
||||
rule := func(packets uint64) *nftables.Rule {
|
||||
return &nftables.Rule{Exprs: []expr.Any{&expr.Counter{Packets: packets}}}
|
||||
}
|
||||
targets := []relayTarget{{address: net.ParseIP("10.104.40.127").To4(), ports: []uint16{80, 443}}}
|
||||
counters, err := firewallCountersFromRules(map[string][]*nftables.Rule{
|
||||
"prerouting": {{}, rule(1), rule(2), rule(3)},
|
||||
"output": {rule(4), rule(5)},
|
||||
}, targets)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if counters.tunnelAllowed["10.104.40.127:80"] != 1 || counters.tunnelAllowed["10.104.40.127:443"] != 2 || counters.tunnelDenied != 3 || counters.outerAllowed != 4 || counters.outerDenied != 5 {
|
||||
t.Fatalf("unexpected counters: %+v", counters)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFirewallCountersRequireEveryRuleCounter(t *testing.T) {
|
||||
withCounter := &nftables.Rule{Exprs: []expr.Any{&expr.Counter{}}}
|
||||
withoutCounter := &nftables.Rule{}
|
||||
_, err := firewallCountersFromRules(map[string][]*nftables.Rule{
|
||||
"prerouting": {{}, withCounter, withoutCounter, withCounter},
|
||||
"output": {withCounter, withCounter},
|
||||
}, []relayTarget{{address: net.ParseIP("10.104.40.127").To4(), ports: []uint16{80, 443}}})
|
||||
if err == nil {
|
||||
t.Fatal("expected missing counter error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckHealth(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "health")
|
||||
if err := writeHealth(path, true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := checkHealth(path, time.Minute); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := writeHealth(path, false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := checkHealth(path, time.Minute); err == nil || err.Error() != "relay reports unhealthy" {
|
||||
t.Fatalf("expected unhealthy state failure, got %v", err)
|
||||
}
|
||||
if err := writeHealth(path, true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
old := time.Now().Add(-time.Hour)
|
||||
if err := os.Chtimes(path, old, old); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := checkHealth(path, time.Minute); err == nil {
|
||||
t.Fatal("expected stale health failure")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyReversePathFilterAcceptsEffectiveLooseMode(t *testing.T) {
|
||||
for name, values := range map[string]map[string]string{
|
||||
"interface": {"all": "0\n", interfaceName: "2\n"},
|
||||
"all": {"all": "2\n", interfaceName: "1\n"},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
for directoryName, value := range values {
|
||||
directory := filepath.Join(root, directoryName)
|
||||
if err := os.MkdirAll(directory, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(directory, "rp_filter"), []byte(value), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := verifyReversePathFilter(root, interfaceName); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyReversePathFilterRejectsStrictMode(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
for _, name := range []string{"all", interfaceName} {
|
||||
directory := filepath.Join(root, name)
|
||||
if err := os.MkdirAll(directory, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(directory, "rp_filter"), []byte("1\n"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := verifyReversePathFilter(root, interfaceName); err == nil {
|
||||
t.Fatal("expected strict reverse-path filtering to fail")
|
||||
}
|
||||
}
|
||||
170
services/oci-l4-relay/network_linux_test.go
Normal file
170
services/oci-l4-relay/network_linux_test.go
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/nftables"
|
||||
"github.com/vishvananda/netlink"
|
||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||
)
|
||||
|
||||
func TestNetworkLifecycle(t *testing.T) {
|
||||
if os.Getenv("RELAY_NETWORK_TEST") != "1" {
|
||||
t.Skip("RELAY_NETWORK_TEST is not enabled")
|
||||
}
|
||||
privateKey, err := wgtypes.GeneratePrivateKey()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
peerKey, err := wgtypes.GeneratePrivateKey()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
privateKeyPath := filepath.Join(t.TempDir(), "private_key")
|
||||
if err := os.WriteFile(privateKeyPath, []byte(privateKey.String()), 0400); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tunnelAddress, err := parseCIDR("198.18.0.2/30")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
localReturnCIDR, err := parseCIDR("203.0.113.0/24")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
targets := []relayTarget{
|
||||
{address: net.ParseIP("198.18.0.10").To4(), ports: []uint16{80, 443}},
|
||||
{address: net.ParseIP("198.18.0.11").To4(), ports: []uint16{25, 465, 993}},
|
||||
}
|
||||
loopback, err := netlink.LinkByName("lo")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := netlink.LinkSetUp(loopback); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, target := range targets {
|
||||
if err := netlink.AddrAdd(loopback, &netlink.Addr{
|
||||
IPNet: &net.IPNet{IP: target.address, Mask: net.CIDRMask(32, 32)},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := netlink.RouteAdd(&netlink.Route{
|
||||
LinkIndex: loopback.Attrs().Index,
|
||||
Dst: localReturnCIDR,
|
||||
Scope: netlink.SCOPE_LINK,
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
instance := &relay{config: config{
|
||||
privateKeyPath: privateKeyPath,
|
||||
peerPublicKey: peerKey.PublicKey().String(),
|
||||
peerEndpoint: "192.0.2.1:51820",
|
||||
listenPort: 51820,
|
||||
tunnelAddress: tunnelAddress,
|
||||
targets: targets,
|
||||
endpointAddress: net.ParseIP("192.0.2.1").To4(),
|
||||
localReturnCIDRs: []*net.IPNet{localReturnCIDR},
|
||||
}}
|
||||
if err := instance.configure(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err := instance.cleanup(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
})
|
||||
if err := instance.refreshFence(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := verifyFirewall(instance.config.endpointAddress, instance.config.targets); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := netlink.LinkByName(interfaceName); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rules, err := netlink.RuleList(netlink.FAMILY_V4)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, target := range instance.config.targets {
|
||||
foundRule := false
|
||||
for _, rule := range rules {
|
||||
if isRelayPolicyRule(rule, target.address) {
|
||||
foundRule = true
|
||||
}
|
||||
}
|
||||
if !foundRule {
|
||||
t.Fatalf("relay policy rule for %s is absent", target.address)
|
||||
}
|
||||
externalRoutes, err := netlink.RouteGetWithOptions(
|
||||
net.ParseIP("192.0.2.2").To4(),
|
||||
&netlink.RouteGetOptions{SrcAddr: target.address},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(externalRoutes) != 1 || externalRoutes[0].LinkIndex != linkIndex(t, interfaceName) {
|
||||
t.Fatalf("external traffic from %s does not select %s: %+v", target.address, interfaceName, externalRoutes)
|
||||
}
|
||||
localRoutes, err := netlink.RouteGetWithOptions(
|
||||
net.ParseIP("203.0.113.1").To4(),
|
||||
&netlink.RouteGetOptions{SrcAddr: target.address},
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(localRoutes) != 1 || localRoutes[0].LinkIndex != loopback.Attrs().Index {
|
||||
t.Fatalf("local traffic from %s does not select the main table: %+v", target.address, localRoutes)
|
||||
}
|
||||
}
|
||||
foundTable := false
|
||||
tables, err := (&nftables.Conn{}).ListTables()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, table := range tables {
|
||||
if table.Family == nftables.TableFamilyINet && table.Name == firewallTable {
|
||||
foundTable = true
|
||||
}
|
||||
}
|
||||
if !foundTable {
|
||||
t.Fatal("relay firewall table is absent")
|
||||
}
|
||||
time.Sleep(16 * time.Second)
|
||||
if err := verifyFirewall(instance.config.endpointAddress, instance.config.targets); err == nil {
|
||||
t.Fatal("expected the unrefreshed ownership fence to expire")
|
||||
}
|
||||
if err := instance.cleanup(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := netlink.LinkByName(interfaceName); err == nil {
|
||||
t.Fatalf("%s remains after cleanup", interfaceName)
|
||||
}
|
||||
rules, err = netlink.RuleList(netlink.FAMILY_V4)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, target := range instance.config.targets {
|
||||
for _, rule := range rules {
|
||||
if isRelayPolicyRule(rule, target.address) ||
|
||||
isLocalReturnRule(rule, target.address, localReturnCIDR, localPriority) {
|
||||
t.Fatalf("policy rule for %s remains after cleanup: %+v", target.address, rule)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func linkIndex(t *testing.T, name string) int {
|
||||
t.Helper()
|
||||
link, err := netlink.LinkByName(name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return link.Attrs().Index
|
||||
}
|
||||
208
services/oci-l4-relay/proxy.go
Normal file
208
services/oci-l4-relay/proxy.go
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var proxySignature = []byte("\r\n\r\n\x00\r\nQUIT\n")
|
||||
|
||||
type mailProxy struct {
|
||||
listener net.Listener
|
||||
upstream string
|
||||
source net.IP
|
||||
failures chan<- error
|
||||
slots chan struct{}
|
||||
mu sync.Mutex
|
||||
closed bool
|
||||
conns map[net.Conn]struct{}
|
||||
workers sync.WaitGroup
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
func newMailProxy(listener net.Listener, upstream string, source net.IP, failures chan<- error, slots chan struct{}) *mailProxy {
|
||||
return &mailProxy{listener: listener, upstream: upstream, source: source, failures: failures, slots: slots, conns: make(map[net.Conn]struct{}), done: make(chan struct{})}
|
||||
}
|
||||
|
||||
func (r *relay) startProxies() error {
|
||||
if r.config.mailTarget == nil {
|
||||
return nil
|
||||
}
|
||||
slots := make(chan struct{}, mailConnectionLimit)
|
||||
for _, port := range r.config.mailTarget.ports {
|
||||
listener, err := net.ListenTCP("tcp4", &net.TCPAddr{IP: r.config.mailTarget.address, Port: int(port)})
|
||||
if err != nil {
|
||||
r.stopProxies()
|
||||
return fmt.Errorf("listen on %s:%d: %w", r.config.mailTarget.address, port, err)
|
||||
}
|
||||
proxy := newMailProxy(listener, net.JoinHostPort(r.config.mailUpstream, strconv.Itoa(int(port))), net.ParseIP(r.config.podIP).To4(), r.proxyFailures, slots)
|
||||
r.proxies = append(r.proxies, proxy)
|
||||
go proxy.serve()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *relay) stopProxies() {
|
||||
for _, proxy := range r.proxies {
|
||||
proxy.close()
|
||||
}
|
||||
r.proxies = nil
|
||||
}
|
||||
|
||||
func (p *mailProxy) serve() {
|
||||
defer close(p.done)
|
||||
for {
|
||||
client, err := p.listener.Accept()
|
||||
if err != nil {
|
||||
if !p.isClosed() {
|
||||
p.report(fmt.Errorf("accept connection: %w", err))
|
||||
}
|
||||
return
|
||||
}
|
||||
select {
|
||||
case p.slots <- struct{}{}:
|
||||
default:
|
||||
_ = client.Close()
|
||||
continue
|
||||
}
|
||||
if !p.track(client) {
|
||||
<-p.slots
|
||||
_ = client.Close()
|
||||
return
|
||||
}
|
||||
p.workers.Add(1)
|
||||
go p.forward(client)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *mailProxy) forward(client net.Conn) {
|
||||
defer p.workers.Done()
|
||||
defer func() { <-p.slots }()
|
||||
defer p.untrack(client)
|
||||
defer client.Close()
|
||||
dialer := net.Dialer{Timeout: 10 * time.Second, KeepAlive: 30 * time.Second}
|
||||
if p.source != nil {
|
||||
dialer.LocalAddr = &net.TCPAddr{IP: p.source}
|
||||
}
|
||||
server, err := dialer.DialContext(context.Background(), "tcp4", p.upstream)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer server.Close()
|
||||
if !p.track(server) {
|
||||
return
|
||||
}
|
||||
defer p.untrack(server)
|
||||
if err := writeProxyHeader(server, client.RemoteAddr(), client.LocalAddr()); err != nil {
|
||||
return
|
||||
}
|
||||
clientTCP, clientOK := client.(*net.TCPConn)
|
||||
serverTCP, serverOK := server.(*net.TCPConn)
|
||||
if !clientOK || !serverOK {
|
||||
return
|
||||
}
|
||||
results := make(chan error, 2)
|
||||
go copyStream(results, serverTCP, clientTCP)
|
||||
go copyStream(results, clientTCP, serverTCP)
|
||||
<-results
|
||||
<-results
|
||||
}
|
||||
|
||||
func (p *mailProxy) close() {
|
||||
p.mu.Lock()
|
||||
if p.closed {
|
||||
p.mu.Unlock()
|
||||
return
|
||||
}
|
||||
p.closed = true
|
||||
connections := make([]net.Conn, 0, len(p.conns))
|
||||
for connection := range p.conns {
|
||||
connections = append(connections, connection)
|
||||
}
|
||||
p.mu.Unlock()
|
||||
_ = p.listener.Close()
|
||||
<-p.done
|
||||
for _, connection := range connections {
|
||||
_ = connection.Close()
|
||||
}
|
||||
p.workers.Wait()
|
||||
}
|
||||
|
||||
func (p *mailProxy) track(connection net.Conn) bool {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
if p.closed {
|
||||
return false
|
||||
}
|
||||
p.conns[connection] = struct{}{}
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *mailProxy) untrack(connection net.Conn) {
|
||||
p.mu.Lock()
|
||||
delete(p.conns, connection)
|
||||
p.mu.Unlock()
|
||||
}
|
||||
|
||||
func (p *mailProxy) isClosed() bool {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
return p.closed
|
||||
}
|
||||
|
||||
func (p *mailProxy) report(err error) {
|
||||
select {
|
||||
case p.failures <- err:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func copyStream(results chan<- error, destination, source *net.TCPConn) {
|
||||
_, err := io.Copy(destination, source)
|
||||
if closeErr := destination.CloseWrite(); err == nil {
|
||||
err = closeErr
|
||||
}
|
||||
results <- err
|
||||
}
|
||||
|
||||
func writeProxyHeader(writer io.Writer, sourceAddress, destinationAddress net.Addr) error {
|
||||
source, sourceOK := sourceAddress.(*net.TCPAddr)
|
||||
destination, destinationOK := destinationAddress.(*net.TCPAddr)
|
||||
if !sourceOK || !destinationOK {
|
||||
return errors.New("PROXY addresses are not TCP")
|
||||
}
|
||||
sourceIP := source.IP.To4()
|
||||
destinationIP := destination.IP.To4()
|
||||
if sourceIP == nil || destinationIP == nil || source.Port < 1 || source.Port > 65535 || destination.Port < 1 || destination.Port > 65535 {
|
||||
return errors.New("PROXY addresses are not IPv4")
|
||||
}
|
||||
header := make([]byte, 28)
|
||||
copy(header, proxySignature)
|
||||
header[12] = 0x21
|
||||
header[13] = 0x11
|
||||
binary.BigEndian.PutUint16(header[14:16], 12)
|
||||
copy(header[16:20], sourceIP)
|
||||
copy(header[20:24], destinationIP)
|
||||
binary.BigEndian.PutUint16(header[24:26], uint16(source.Port))
|
||||
binary.BigEndian.PutUint16(header[26:28], uint16(destination.Port))
|
||||
_, err := io.Copy(writer, bytes.NewReader(header))
|
||||
return err
|
||||
}
|
||||
|
||||
func drainFailures(failures <-chan error) {
|
||||
for {
|
||||
select {
|
||||
case <-failures:
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
245
services/oci-l4-relay/proxy_test.go
Normal file
245
services/oci-l4-relay/proxy_test.go
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type proxyRecord struct {
|
||||
header []byte
|
||||
payload []byte
|
||||
peer net.IP
|
||||
}
|
||||
|
||||
func TestMailTarget(t *testing.T) {
|
||||
tunnel, err := parseCIDR("10.153.238.225/30")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
target, upstream, err := mailProxyTarget("stalwart-relay.stalwart.svc.cluster.local", tunnel)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !target.address.Equal(tunnel.IP) || upstream != "stalwart-relay.stalwart.svc.cluster.local" {
|
||||
t.Fatalf("unexpected proxy target: %#v %q", target, upstream)
|
||||
}
|
||||
if len(target.ports) != 3 || target.ports[0] != 25 || target.ports[1] != 465 || target.ports[2] != 993 {
|
||||
t.Fatalf("unexpected proxy ports: %v", target.ports)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMailProxyTCP(t *testing.T) {
|
||||
upstream := listenTCP(t)
|
||||
listener := listenTCP(t)
|
||||
failures := make(chan error, 1)
|
||||
proxy := newMailProxy(listener, upstream.Addr().String(), net.ParseIP("127.0.0.2"), failures, make(chan struct{}, mailConnectionLimit))
|
||||
go proxy.serve()
|
||||
defer proxy.close()
|
||||
records := make(chan proxyRecord, 1)
|
||||
go func() {
|
||||
connection, err := upstream.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer connection.Close()
|
||||
header := make([]byte, 28)
|
||||
if _, err := io.ReadFull(connection, header); err != nil {
|
||||
return
|
||||
}
|
||||
payload := make([]byte, 4)
|
||||
if _, err := io.ReadFull(connection, payload); err != nil {
|
||||
return
|
||||
}
|
||||
records <- proxyRecord{header: header, payload: payload, peer: connection.RemoteAddr().(*net.TCPAddr).IP}
|
||||
_, _ = connection.Write([]byte("pong"))
|
||||
}()
|
||||
client, err := net.DialTCP("tcp4", nil, listener.Addr().(*net.TCPAddr))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer client.Close()
|
||||
if _, err := client.Write([]byte("ping")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
reply := make([]byte, 4)
|
||||
if _, err := io.ReadFull(client, reply); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(reply) != "pong" {
|
||||
t.Fatalf("reply = %q", reply)
|
||||
}
|
||||
select {
|
||||
case record := <-records:
|
||||
assertProxyRecord(t, record, client.LocalAddr().(*net.TCPAddr), listener.Addr().(*net.TCPAddr))
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("upstream did not receive the proxied stream")
|
||||
}
|
||||
select {
|
||||
case err := <-failures:
|
||||
t.Fatalf("unexpected proxy failure: %v", err)
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func TestMailDialFailure(t *testing.T) {
|
||||
unavailable := listenTCP(t)
|
||||
upstream := unavailable.Addr().String()
|
||||
_ = unavailable.Close()
|
||||
listener := listenTCP(t)
|
||||
failures := make(chan error, 1)
|
||||
proxy := newMailProxy(listener, upstream, nil, failures, make(chan struct{}, mailConnectionLimit))
|
||||
go proxy.serve()
|
||||
defer proxy.close()
|
||||
client, err := net.Dial("tcp4", listener.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer client.Close()
|
||||
if err := client.SetReadDeadline(time.Now().Add(time.Second)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := client.Read(make([]byte, 1)); err == nil {
|
||||
t.Fatal("client remained open after the upstream refusal")
|
||||
}
|
||||
select {
|
||||
case err := <-failures:
|
||||
t.Fatalf("connection-level dial failure was reported as relay-fatal: %v", err)
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func TestMailProxyClose(t *testing.T) {
|
||||
upstream := listenTCP(t)
|
||||
listener := listenTCP(t)
|
||||
proxy := newMailProxy(listener, upstream.Addr().String(), nil, make(chan error, 1), make(chan struct{}, mailConnectionLimit))
|
||||
go proxy.serve()
|
||||
accepted := make(chan net.Conn, 1)
|
||||
go func() {
|
||||
connection, err := upstream.Accept()
|
||||
if err == nil {
|
||||
accepted <- connection
|
||||
}
|
||||
}()
|
||||
client, err := net.Dial("tcp4", listener.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer client.Close()
|
||||
var server net.Conn
|
||||
select {
|
||||
case server = <-accepted:
|
||||
defer server.Close()
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("proxy did not open the upstream stream")
|
||||
}
|
||||
closed := make(chan struct{})
|
||||
go func() {
|
||||
proxy.close()
|
||||
close(closed)
|
||||
}()
|
||||
select {
|
||||
case <-closed:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("proxy shutdown did not close active streams")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMailProxyConnectionLimit(t *testing.T) {
|
||||
upstream := listenTCP(t)
|
||||
listener := listenTCP(t)
|
||||
slots := make(chan struct{}, 1)
|
||||
proxy := newMailProxy(listener, upstream.Addr().String(), nil, make(chan error, 1), slots)
|
||||
go proxy.serve()
|
||||
defer proxy.close()
|
||||
accepted := make(chan net.Conn, 2)
|
||||
go func() {
|
||||
for {
|
||||
connection, err := upstream.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
accepted <- connection
|
||||
}
|
||||
}()
|
||||
first, err := net.Dial("tcp4", listener.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer first.Close()
|
||||
firstUpstream := waitForAccepted(t, accepted)
|
||||
defer firstUpstream.Close()
|
||||
second, err := net.Dial("tcp4", listener.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer second.Close()
|
||||
if err := second.SetReadDeadline(time.Now().Add(time.Second)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := second.Read(make([]byte, 1)); err == nil {
|
||||
t.Fatal("over-limit connection remained open")
|
||||
}
|
||||
select {
|
||||
case connection := <-accepted:
|
||||
_ = connection.Close()
|
||||
t.Fatal("over-limit connection reached the upstream")
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
}
|
||||
_ = first.Close()
|
||||
_ = firstUpstream.Close()
|
||||
deadline := time.Now().Add(time.Second)
|
||||
for len(slots) != 0 && time.Now().Before(deadline) {
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
if len(slots) != 0 {
|
||||
t.Fatal("connection slot was not released")
|
||||
}
|
||||
third, err := net.Dial("tcp4", listener.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer third.Close()
|
||||
thirdUpstream := waitForAccepted(t, accepted)
|
||||
_ = thirdUpstream.Close()
|
||||
}
|
||||
|
||||
func listenTCP(t *testing.T) *net.TCPListener {
|
||||
t.Helper()
|
||||
listener, err := net.ListenTCP("tcp4", &net.TCPAddr{IP: net.ParseIP("127.0.0.1")})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = listener.Close() })
|
||||
return listener
|
||||
}
|
||||
|
||||
func waitForAccepted(t *testing.T, connections <-chan net.Conn) net.Conn {
|
||||
t.Helper()
|
||||
select {
|
||||
case connection := <-connections:
|
||||
return connection
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("proxy did not open the upstream stream")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func assertProxyRecord(t *testing.T, record proxyRecord, source, destination *net.TCPAddr) {
|
||||
t.Helper()
|
||||
if len(record.header) != 28 || !bytes.Equal(record.header[:12], proxySignature) || record.header[12] != 0x21 || record.header[13] != 0x11 || binary.BigEndian.Uint16(record.header[14:16]) != 12 {
|
||||
t.Fatalf("invalid PROXY header: %x", record.header)
|
||||
}
|
||||
if !bytes.Equal(record.header[16:20], source.IP.To4()) || !bytes.Equal(record.header[20:24], destination.IP.To4()) || binary.BigEndian.Uint16(record.header[24:26]) != uint16(source.Port) || binary.BigEndian.Uint16(record.header[26:28]) != uint16(destination.Port) {
|
||||
t.Fatalf("unexpected PROXY addresses: %x", record.header)
|
||||
}
|
||||
if string(record.payload) != "ping" {
|
||||
t.Fatalf("payload = %q", record.payload)
|
||||
}
|
||||
if !record.peer.Equal(net.ParseIP("127.0.0.2")) {
|
||||
t.Fatalf("upstream peer = %s", record.peer)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue