ce7cb702218693d08ea6d83464d2457c9250027c
This commit is contained in:
commit
34c5f8963c
13 changed files with 145336 additions and 0 deletions
53
services/oci-l4-relay-wan-controller/go.mod
Normal file
53
services/oci-l4-relay-wan-controller/go.mod
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
module github.com/quorate/ms2k-internal/services/oci-l4-relay-wan-controller
|
||||
|
||||
go 1.26.4
|
||||
|
||||
require (
|
||||
github.com/oracle/oci-go-sdk/v65 v65.121.0
|
||||
k8s.io/apimachinery v0.36.2
|
||||
k8s.io/client-go v0.36.2
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.13.0 // 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/gofrs/flock v0.10.0 // indirect
|
||||
github.com/google/gnostic-models 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/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.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/sony/gobreaker/v2 v2.4.0 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/crypto v0.52.0 // indirect
|
||||
golang.org/x/net v0.54.0 // indirect
|
||||
golang.org/x/oauth2 v0.34.0 // indirect
|
||||
golang.org/x/sys v0.45.0 // indirect
|
||||
golang.org/x/term v0.43.0 // indirect
|
||||
golang.org/x/text v0.37.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // 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/api v0.36.2 // indirect
|
||||
k8s.io/klog/v2 v2.140.0 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect
|
||||
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // 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.2 // indirect
|
||||
sigs.k8s.io/yaml v1.6.0 // indirect
|
||||
)
|
||||
127
services/oci-l4-relay-wan-controller/go.sum
Normal file
127
services/oci-l4-relay-wan-controller/go.sum
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes=
|
||||
github.com/emicklei/go-restful/v3 v3.13.0/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/gofrs/flock v0.10.0 h1:SHMXenfaB03KbroETaCMtbBg3Yn29v4w1r+tgy4ff4k=
|
||||
github.com/gofrs/flock v0.10.0/go.mod h1:FirDy1Ing0mI2+kB6wk+vyyAH+e6xiE+EYA0jnzV9jc=
|
||||
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/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/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/oracle/oci-go-sdk/v65 v65.121.0 h1:1J+5ARgrodrx8kzFy/hxznaoUzz43jr0EestCzEaOHw=
|
||||
github.com/oracle/oci-go-sdk/v65 v65.121.0/go.mod h1:Pzy+BpgkDesvGZXEHgslwhIYobHCPHg6wRta1mWnlqQ=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/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/sony/gobreaker/v2 v2.4.0 h1:g2KJRW1Ubty3+ZOcSEUN7K+REQJdN6yo6XvaML+jptg=
|
||||
github.com/sony/gobreaker/v2 v2.4.0/go.mod h1:pTyFJgcZ3h2tdQVLZZruK2C0eoFL1fb/G83wK1ZQl+s=
|
||||
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/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
|
||||
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.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
|
||||
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
|
||||
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
|
||||
golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
|
||||
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
|
||||
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
|
||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI=
|
||||
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
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.36.2 h1:TF6YDLIzKfccK7cq9YpTcGX8TJmEkHVRv78DM51fRYY=
|
||||
k8s.io/api v0.36.2/go.mod h1:F4LbMO4brjZYh7yFkXWhynSvtB7YauxV4c+HHkNRGNg=
|
||||
k8s.io/apimachinery v0.36.2 h1:0PE/W/WNy1UX61NLbXY5TMbJ6UwLL6E6lAPkYrKFxbQ=
|
||||
k8s.io/apimachinery v0.36.2/go.mod h1:fvf/HOLXq9RId0rnDIbN1OEBvHXdQbLMM8nu0LcBUf4=
|
||||
k8s.io/client-go v0.36.2 h1:bfgxmFKc9CgqsgX4xKLAAdmTQlWee7Ob/HlDOrJ5TBI=
|
||||
k8s.io/client-go v0.36.2/go.mod h1:1vgO4OAlfPnoLcb+Rze2GF5rAr14w8qjrYMoyXJzQj0=
|
||||
k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc=
|
||||
k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0=
|
||||
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a h1:xCeOEAOoGYl2jnJoHkC3hkbPJgdATINPMAxaynU2Ovg=
|
||||
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0=
|
||||
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU=
|
||||
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
|
||||
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.2 h1:kwVWMx5yS1CrnFWA/2QHyRVJ8jM6dBA80uLmm0wJkk8=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.2/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=
|
||||
109
services/oci-l4-relay-wan-controller/k8s/deployment.yaml
Normal file
109
services/oci-l4-relay-wan-controller/k8s/deployment.yaml
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: oci-l4-relay-wan-controller
|
||||
namespace: kube-system
|
||||
spec:
|
||||
replicas: 2
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: oci-l4-relay-wan-controller
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: oci-l4-relay-wan-controller
|
||||
spec:
|
||||
serviceAccountName: oci-l4-relay-wan-controller
|
||||
automountServiceAccountToken: true
|
||||
securityContext:
|
||||
fsGroup: 65532
|
||||
fsGroupChangePolicy: OnRootMismatch
|
||||
affinity:
|
||||
podAntiAffinity:
|
||||
requiredDuringSchedulingIgnoredDuringExecution:
|
||||
- labelSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: oci-l4-relay-wan-controller
|
||||
topologyKey: kubernetes.io/hostname
|
||||
containers:
|
||||
- name: controller
|
||||
image: registry.quorate.internal/quorate/oci-l4-relay-wan-controller@sha256:395e68f424e6fddcfb72cee8f83887ed219b47e16cd87562c4073325b3598803
|
||||
imagePullPolicy: IfNotPresent
|
||||
env:
|
||||
- name: POD_NAMESPACE
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.namespace
|
||||
- name: OPNSENSE_URL
|
||||
value: https://10.212.111.120:8443
|
||||
- name: OPNSENSE_INTERFACE
|
||||
value: WAN
|
||||
- name: POLL_INTERVAL
|
||||
value: 10s
|
||||
- name: MINIMUM_OBSERVATIONS
|
||||
value: "2"
|
||||
- name: WIREGUARD_PORT
|
||||
value: "51820"
|
||||
- name: OPNSENSE_KEY_PATH
|
||||
value: /run/secrets/opnsense/key
|
||||
- name: OPNSENSE_SECRET_PATH
|
||||
value: /run/secrets/opnsense/secret
|
||||
- name: OPNSENSE_CA_PATH
|
||||
value: /run/secrets/opnsense-ca/ca.crt
|
||||
- name: OCI_TENANCY_OCID_PATH
|
||||
value: /run/secrets/oci/tenancy_ocid
|
||||
- name: OCI_USER_OCID_PATH
|
||||
value: /run/secrets/oci/user_ocid
|
||||
- name: OCI_FINGERPRINT_PATH
|
||||
value: /run/secrets/oci/fingerprint
|
||||
- name: OCI_PRIVATE_KEY_PATH
|
||||
value: /run/secrets/oci/private_key
|
||||
- name: OCI_REGION_PATH
|
||||
value: /run/secrets/oci/region
|
||||
- name: OCI_NSG_OCID_PATH
|
||||
value: /run/secrets/oci/network_security_group_id
|
||||
- name: OCI_SECURITY_RULE_ID_PATH
|
||||
value: /run/secrets/oci/security_rule_id
|
||||
- name: WG_STATUS_URL
|
||||
value: http://oci-l4-relay-metrics.kube-system.svc.cluster.local:19100/metrics
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
readOnlyRootFilesystem: true
|
||||
runAsNonRoot: true
|
||||
runAsUser: 65532
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
resources:
|
||||
requests:
|
||||
cpu: 4m
|
||||
memory: 20Mi
|
||||
limits:
|
||||
memory: 128Mi
|
||||
volumeMounts:
|
||||
- name: opnsense
|
||||
mountPath: /run/secrets/opnsense
|
||||
readOnly: true
|
||||
- name: opnsense-ca
|
||||
mountPath: /run/secrets/opnsense-ca
|
||||
readOnly: true
|
||||
- name: oci
|
||||
mountPath: /run/secrets/oci
|
||||
readOnly: true
|
||||
volumes:
|
||||
- name: opnsense
|
||||
secret:
|
||||
secretName: opnsense-interface-watcher
|
||||
defaultMode: 0440
|
||||
- name: opnsense-ca
|
||||
secret:
|
||||
secretName: ca-opnsense
|
||||
defaultMode: 0440
|
||||
- name: oci
|
||||
secret:
|
||||
secretName: oci-l4-relay-wan-controller-api
|
||||
defaultMode: 0440
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
namespace: kube-system
|
||||
resources:
|
||||
- service-account.yaml
|
||||
- vault-secret.yaml
|
||||
- deployment.yaml
|
||||
- network-policy.yaml
|
||||
59
services/oci-l4-relay-wan-controller/k8s/network-policy.yaml
Normal file
59
services/oci-l4-relay-wan-controller/k8s/network-policy.yaml
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
apiVersion: cilium.io/v2
|
||||
kind: CiliumNetworkPolicy
|
||||
metadata:
|
||||
name: oci-l4-relay-wan-controller
|
||||
namespace: kube-system
|
||||
spec:
|
||||
endpointSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: oci-l4-relay-wan-controller
|
||||
ingress: []
|
||||
egress:
|
||||
- toEntities:
|
||||
- kube-apiserver
|
||||
toPorts:
|
||||
- ports:
|
||||
- port: "6443"
|
||||
protocol: TCP
|
||||
- toEndpoints:
|
||||
- matchLabels:
|
||||
k8s:io.kubernetes.pod.namespace: kube-system
|
||||
k8s-app: kube-dns
|
||||
toPorts:
|
||||
- ports:
|
||||
- port: "53"
|
||||
protocol: UDP
|
||||
- port: "53"
|
||||
protocol: TCP
|
||||
rules:
|
||||
dns:
|
||||
- matchPattern: "*"
|
||||
- toServices:
|
||||
- k8sService:
|
||||
namespace: kube-system
|
||||
serviceName: kube-dns
|
||||
toPorts:
|
||||
- ports:
|
||||
- port: "53"
|
||||
protocol: UDP
|
||||
- port: "53"
|
||||
protocol: TCP
|
||||
- toCIDR:
|
||||
- 10.212.111.120/32
|
||||
toPorts:
|
||||
- ports:
|
||||
- port: "8443"
|
||||
protocol: TCP
|
||||
- toEntities:
|
||||
- host
|
||||
- remote-node
|
||||
toPorts:
|
||||
- ports:
|
||||
- port: "19100"
|
||||
protocol: TCP
|
||||
- toFQDNs:
|
||||
- matchPattern: iaas.*.oraclecloud.com
|
||||
toPorts:
|
||||
- ports:
|
||||
- port: "443"
|
||||
protocol: TCP
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: oci-l4-relay-wan-controller
|
||||
namespace: kube-system
|
||||
automountServiceAccountToken: true
|
||||
imagePullSecrets:
|
||||
- name: quay-cluster-pull
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: oci-l4-relay-wan-controller
|
||||
namespace: kube-system
|
||||
rules:
|
||||
- apiGroups:
|
||||
- coordination.k8s.io
|
||||
resources:
|
||||
- leases
|
||||
verbs:
|
||||
- create
|
||||
- apiGroups:
|
||||
- coordination.k8s.io
|
||||
resourceNames:
|
||||
- wan-controller
|
||||
resources:
|
||||
- leases
|
||||
verbs:
|
||||
- get
|
||||
- update
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: oci-l4-relay-wan-controller
|
||||
namespace: kube-system
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
name: oci-l4-relay-wan-controller
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: oci-l4-relay-wan-controller
|
||||
namespace: kube-system
|
||||
109
services/oci-l4-relay-wan-controller/k8s/vault-secret.yaml
Normal file
109
services/oci-l4-relay-wan-controller/k8s/vault-secret.yaml
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
apiVersion: secrets.hashicorp.com/v1beta1
|
||||
kind: VaultAuth
|
||||
metadata:
|
||||
name: oci-l4-relay-wan-controller
|
||||
namespace: kube-system
|
||||
spec:
|
||||
method: kubernetes
|
||||
mount: kubernetes
|
||||
kubernetes:
|
||||
role: oci-l4-relay-wan-controller
|
||||
serviceAccount: oci-l4-relay-wan-controller
|
||||
vaultConnectionRef: vault/vault
|
||||
---
|
||||
apiVersion: secrets.hashicorp.com/v1beta1
|
||||
kind: VaultStaticSecret
|
||||
metadata:
|
||||
name: opnsense-interface-watcher
|
||||
namespace: kube-system
|
||||
spec:
|
||||
type: kv-v2
|
||||
mount: secret
|
||||
path: opnsense/interface-watcher
|
||||
destination:
|
||||
name: opnsense-interface-watcher
|
||||
create: true
|
||||
overwrite: true
|
||||
transformation:
|
||||
excludeRaw: true
|
||||
excludes:
|
||||
- .*
|
||||
templates:
|
||||
key:
|
||||
text: '{{ get .Secrets "key" }}'
|
||||
secret:
|
||||
text: '{{ get .Secrets "secret" }}'
|
||||
rolloutRestartTargets:
|
||||
- kind: Deployment
|
||||
name: oci-l4-relay-wan-controller
|
||||
vaultAuthRef: oci-l4-relay-wan-controller
|
||||
---
|
||||
apiVersion: secrets.hashicorp.com/v1beta1
|
||||
kind: VaultStaticSecret
|
||||
metadata:
|
||||
name: opnsense-root-ca
|
||||
namespace: kube-system
|
||||
spec:
|
||||
type: kv-v2
|
||||
mount: secret
|
||||
path: ca/opnsense
|
||||
hmacSecretData: true
|
||||
refreshAfter: 1h
|
||||
destination:
|
||||
name: ca-opnsense
|
||||
create: true
|
||||
overwrite: true
|
||||
transformation:
|
||||
excludeRaw: true
|
||||
excludes:
|
||||
- .*
|
||||
templates:
|
||||
ca.crt:
|
||||
text: >-
|
||||
{{- $ca := get .Secrets "certificate" -}}
|
||||
{{- if not (hasPrefix "-----BEGIN CERTIFICATE-----" $ca) -}}
|
||||
{{- fail "CA publication is pending" -}}
|
||||
{{- end -}}
|
||||
{{- $ca -}}
|
||||
rolloutRestartTargets:
|
||||
- kind: Deployment
|
||||
name: oci-l4-relay-wan-controller
|
||||
vaultAuthRef: oci-l4-relay-wan-controller
|
||||
---
|
||||
apiVersion: secrets.hashicorp.com/v1beta1
|
||||
kind: VaultStaticSecret
|
||||
metadata:
|
||||
name: oci-l4-relay-wan-controller-api
|
||||
namespace: kube-system
|
||||
spec:
|
||||
type: kv-v2
|
||||
mount: secret
|
||||
path: oci/relay/wan-controller-api
|
||||
destination:
|
||||
name: oci-l4-relay-wan-controller-api
|
||||
create: true
|
||||
overwrite: true
|
||||
transformation:
|
||||
excludeRaw: true
|
||||
excludes:
|
||||
- .*
|
||||
templates:
|
||||
tenancy_ocid:
|
||||
text: '{{ get .Secrets "tenancy_ocid" }}'
|
||||
user_ocid:
|
||||
text: '{{ get .Secrets "user_ocid" }}'
|
||||
fingerprint:
|
||||
text: '{{ get .Secrets "fingerprint" }}'
|
||||
private_key:
|
||||
text: |
|
||||
{{ get .Secrets "private_key" }}
|
||||
region:
|
||||
text: '{{ get .Secrets "region" }}'
|
||||
network_security_group_id:
|
||||
text: '{{ get .Secrets "network_security_group_id" }}'
|
||||
security_rule_id:
|
||||
text: '{{ get .Secrets "security_rule_id" }}'
|
||||
rolloutRestartTargets:
|
||||
- kind: Deployment
|
||||
name: oci-l4-relay-wan-controller
|
||||
vaultAuthRef: oci-l4-relay-wan-controller
|
||||
419
services/oci-l4-relay-wan-controller/main.go
Normal file
419
services/oci-l4-relay-wan-controller/main.go
Normal file
|
|
@ -0,0 +1,419 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
coordinationv1 "k8s.io/client-go/kubernetes/typed/coordination/v1"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/leaderelection"
|
||||
"k8s.io/client-go/tools/leaderelection/resourcelock"
|
||||
)
|
||||
|
||||
const leaderLockName = "wan-controller"
|
||||
|
||||
type opnsenseClient struct {
|
||||
baseURL string
|
||||
key string
|
||||
secret string
|
||||
http *http.Client
|
||||
}
|
||||
|
||||
type interfaceOverview struct {
|
||||
Rows []interfaceRow `json:"rows"`
|
||||
}
|
||||
|
||||
type interfaceRow struct {
|
||||
Identifier string `json:"identifier"`
|
||||
Description string `json:"description"`
|
||||
IPv4 []struct {
|
||||
Address string `json:"ipaddr"`
|
||||
} `json:"ipv4"`
|
||||
}
|
||||
|
||||
func (c *opnsenseClient) wanIPv4(ctx context.Context, selector string) (net.IP, error) {
|
||||
endpoint := strings.TrimRight(c.baseURL, "/") + "/api/interfaces/overview/interfaces_info"
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.SetBasicAuth(c.key, c.secret)
|
||||
resp, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("OPNsense returned HTTP %d", resp.StatusCode)
|
||||
}
|
||||
var overview interfaceOverview
|
||||
if err := json.NewDecoder(io.LimitReader(resp.Body, 1<<20)).Decode(&overview); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var selected *interfaceRow
|
||||
for index := range overview.Rows {
|
||||
row := &overview.Rows[index]
|
||||
if row.Identifier != selector && row.Description != selector {
|
||||
continue
|
||||
}
|
||||
if selected != nil {
|
||||
return nil, fmt.Errorf("OPNsense interface selector %q is ambiguous", selector)
|
||||
}
|
||||
selected = row
|
||||
}
|
||||
if selected == nil {
|
||||
return nil, fmt.Errorf("OPNsense interface selector %q did not match", selector)
|
||||
}
|
||||
for _, address := range selected.IPv4 {
|
||||
if ip := parseInterfaceIPv4(address.Address); validPublicIPv4(ip) {
|
||||
return ip.To4(), nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("OPNsense interface %q contains no public IPv4", selector)
|
||||
}
|
||||
|
||||
func parseInterfaceIPv4(address string) net.IP {
|
||||
if ip := net.ParseIP(address); ip != nil {
|
||||
return ip
|
||||
}
|
||||
ip, _, err := net.ParseCIDR(address)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return ip
|
||||
}
|
||||
|
||||
func validPublicIPv4(ip net.IP) bool {
|
||||
return ip != nil && ip.To4() != nil && !ip.IsPrivate() && !ip.IsLoopback() &&
|
||||
!ip.IsUnspecified() && !ip.IsMulticast() && !ip.IsLinkLocalUnicast()
|
||||
}
|
||||
|
||||
type sourceReconciler interface {
|
||||
Reconcile(ctx context.Context, source net.IP) error
|
||||
}
|
||||
|
||||
func poll(ctx context.Context, client *opnsenseClient, selector string, interval time.Duration, minimumObservations int, reconcile sourceReconciler) error {
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
var applied, candidate string
|
||||
observations := 0
|
||||
for {
|
||||
ip, err := client.wanIPv4(ctx, selector)
|
||||
if err != nil {
|
||||
slog.Error("poll OPNsense WAN", "error", err)
|
||||
candidate = ""
|
||||
observations = 0
|
||||
} else {
|
||||
current := ip.String()
|
||||
if current == applied {
|
||||
candidate = ""
|
||||
observations = 0
|
||||
} else {
|
||||
if current != candidate {
|
||||
candidate = current
|
||||
observations = 1
|
||||
slog.Info("observed new OPNsense WAN candidate", "source", current, "observations", observations)
|
||||
} else {
|
||||
observations++
|
||||
}
|
||||
if observations >= minimumObservations {
|
||||
if err := reconcile.Reconcile(ctx, ip); err != nil {
|
||||
slog.Error("reconcile OCI WireGuard source", "source", current, "error", err)
|
||||
} else {
|
||||
applied = current
|
||||
candidate = ""
|
||||
observations = 0
|
||||
slog.Info("reconciled OCI WireGuard source", "source", current)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-ticker.C:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||
defer stop()
|
||||
if err := run(ctx); err != nil && !errors.Is(err, context.Canceled) {
|
||||
slog.Error("controller stopped", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func run(ctx context.Context) error {
|
||||
cfg, err := loadConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
restConfig, err := rest.InClusterConfig()
|
||||
if err != nil {
|
||||
return fmt.Errorf("load Kubernetes client config: %w", err)
|
||||
}
|
||||
kubeClient, err := coordinationv1.NewForConfig(restConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create Kubernetes client: %w", err)
|
||||
}
|
||||
identity, err := os.Hostname()
|
||||
if err != nil {
|
||||
return fmt.Errorf("read pod hostname: %w", err)
|
||||
}
|
||||
namespace := os.Getenv("POD_NAMESPACE")
|
||||
if namespace == "" {
|
||||
return errors.New("POD_NAMESPACE is required")
|
||||
}
|
||||
|
||||
electionCtx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
controllerErr := make(chan error, 1)
|
||||
elector, err := leaderelection.NewLeaderElector(leaderelection.LeaderElectionConfig{
|
||||
Lock: &resourcelock.LeaseLock{
|
||||
LeaseMeta: metav1.ObjectMeta{Name: leaderLockName, Namespace: namespace},
|
||||
Client: kubeClient,
|
||||
LockConfig: resourcelock.ResourceLockConfig{
|
||||
Identity: identity,
|
||||
},
|
||||
},
|
||||
LeaseDuration: 30 * time.Second,
|
||||
RenewDeadline: 20 * time.Second,
|
||||
RetryPeriod: 5 * time.Second,
|
||||
ReleaseOnCancel: true,
|
||||
Name: leaderLockName,
|
||||
Callbacks: leaderelection.LeaderCallbacks{
|
||||
OnStartedLeading: func(leaderCtx context.Context) {
|
||||
controllerErr <- runLeader(leaderCtx, cfg)
|
||||
cancel()
|
||||
},
|
||||
OnStoppedLeading: func() {
|
||||
if ctx.Err() == nil {
|
||||
select {
|
||||
case controllerErr <- errors.New("leadership lost"):
|
||||
default:
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("configure leader election: %w", err)
|
||||
}
|
||||
elector.Run(electionCtx)
|
||||
select {
|
||||
case err := <-controllerErr:
|
||||
return err
|
||||
default:
|
||||
return electionCtx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
func runLeader(ctx context.Context, cfg config) error {
|
||||
reconciler, err := newOCIReconciler(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
httpClient, err := newOPNsenseHTTPClient(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client := &opnsenseClient{
|
||||
baseURL: cfg.opnsenseURL,
|
||||
key: cfg.opnsenseKey,
|
||||
secret: cfg.opnsenseSecret,
|
||||
http: httpClient,
|
||||
}
|
||||
return poll(ctx, client, cfg.opnsenseInterface, cfg.pollInterval, cfg.minimumObservations, reconciler)
|
||||
}
|
||||
|
||||
type config struct {
|
||||
opnsenseURL string
|
||||
opnsenseInterface string
|
||||
opnsenseKey string
|
||||
opnsenseSecret string
|
||||
ociTenancy string
|
||||
ociUser string
|
||||
ociFingerprint string
|
||||
ociPrivateKey string
|
||||
ociRegion string
|
||||
networkSecurityGroup string
|
||||
securityRule string
|
||||
wireguardPort int
|
||||
pollInterval time.Duration
|
||||
minimumObservations int
|
||||
requestTimeout time.Duration
|
||||
opnsenseCACert string
|
||||
wireguardURL string
|
||||
recoveryTimeout time.Duration
|
||||
}
|
||||
|
||||
func loadConfig() (config, error) {
|
||||
read := func(name string) (string, error) {
|
||||
path := os.Getenv(name)
|
||||
if path == "" {
|
||||
return "", fmt.Errorf("%s is required", name)
|
||||
}
|
||||
value, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read %s: %w", name, err)
|
||||
}
|
||||
return strings.TrimSpace(string(value)), nil
|
||||
}
|
||||
opnsenseKey, err := read("OPNSENSE_KEY_PATH")
|
||||
if err != nil {
|
||||
return config{}, err
|
||||
}
|
||||
opnsenseSecret, err := read("OPNSENSE_SECRET_PATH")
|
||||
if err != nil {
|
||||
return config{}, err
|
||||
}
|
||||
opnsenseCACert, err := read("OPNSENSE_CA_PATH")
|
||||
if err != nil {
|
||||
return config{}, err
|
||||
}
|
||||
ociTenancy, err := read("OCI_TENANCY_OCID_PATH")
|
||||
if err != nil {
|
||||
return config{}, err
|
||||
}
|
||||
ociUser, err := read("OCI_USER_OCID_PATH")
|
||||
if err != nil {
|
||||
return config{}, err
|
||||
}
|
||||
ociFingerprint, err := read("OCI_FINGERPRINT_PATH")
|
||||
if err != nil {
|
||||
return config{}, err
|
||||
}
|
||||
ociPrivateKey, err := read("OCI_PRIVATE_KEY_PATH")
|
||||
if err != nil {
|
||||
return config{}, err
|
||||
}
|
||||
ociRegion, err := read("OCI_REGION_PATH")
|
||||
if err != nil {
|
||||
return config{}, err
|
||||
}
|
||||
nsg, err := read("OCI_NSG_OCID_PATH")
|
||||
if err != nil {
|
||||
return config{}, err
|
||||
}
|
||||
rule, err := read("OCI_SECURITY_RULE_ID_PATH")
|
||||
if err != nil {
|
||||
return config{}, err
|
||||
}
|
||||
port, err := strconv.Atoi(envOrDefault("WIREGUARD_PORT", "51820"))
|
||||
if err != nil || port < 1 || port > 65535 {
|
||||
return config{}, errors.New("WIREGUARD_PORT must be a valid port")
|
||||
}
|
||||
pollInterval, err := time.ParseDuration(envOrDefault("POLL_INTERVAL", "30s"))
|
||||
if err != nil || pollInterval < 5*time.Second {
|
||||
return config{}, errors.New("POLL_INTERVAL must be at least 5s")
|
||||
}
|
||||
minimumObservations, err := strconv.Atoi(envOrDefault("MINIMUM_OBSERVATIONS", "2"))
|
||||
if err != nil || minimumObservations < 1 || minimumObservations > 10 {
|
||||
return config{}, errors.New("MINIMUM_OBSERVATIONS must be between 1 and 10")
|
||||
}
|
||||
opnsenseURL, err := requiredHTTPSOrigin("OPNSENSE_URL")
|
||||
if err != nil {
|
||||
return config{}, err
|
||||
}
|
||||
wireguardURL, err := requiredMetricsURL("WG_STATUS_URL")
|
||||
if err != nil {
|
||||
return config{}, err
|
||||
}
|
||||
return config{
|
||||
opnsenseURL: opnsenseURL,
|
||||
opnsenseInterface: envOrDefault("OPNSENSE_INTERFACE", "WAN"),
|
||||
opnsenseKey: opnsenseKey,
|
||||
opnsenseSecret: opnsenseSecret,
|
||||
opnsenseCACert: opnsenseCACert,
|
||||
ociTenancy: ociTenancy,
|
||||
ociUser: ociUser,
|
||||
ociFingerprint: ociFingerprint,
|
||||
ociPrivateKey: ociPrivateKey,
|
||||
ociRegion: ociRegion,
|
||||
networkSecurityGroup: nsg,
|
||||
securityRule: rule,
|
||||
wireguardPort: port,
|
||||
pollInterval: pollInterval,
|
||||
minimumObservations: minimumObservations,
|
||||
requestTimeout: 15 * time.Second,
|
||||
wireguardURL: wireguardURL,
|
||||
recoveryTimeout: 90 * time.Second,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func newOPNsenseHTTPClient(cfg config) (*http.Client, error) {
|
||||
block, _ := pem.Decode([]byte(cfg.opnsenseCACert))
|
||||
if block == nil || block.Type != "CERTIFICATE" {
|
||||
return nil, errors.New("OPNSENSE_CA_PATH contains no PEM certificate")
|
||||
}
|
||||
certificate, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse OPNsense CA certificate: %w", err)
|
||||
}
|
||||
remaining := time.Until(certificate.NotAfter)
|
||||
if remaining <= 0 {
|
||||
return nil, errors.New("OPNsense CA certificate is expired")
|
||||
}
|
||||
if remaining < 90*24*time.Hour {
|
||||
slog.Warn("OPNsense CA certificate expires soon", "not_after", certificate.NotAfter.UTC())
|
||||
}
|
||||
if !certificate.IsCA || certificate.KeyUsage&x509.KeyUsageCertSign == 0 {
|
||||
return nil, errors.New("OPNSENSE_CA_PATH certificate is not a certificate authority")
|
||||
}
|
||||
roots := x509.NewCertPool()
|
||||
if !roots.AppendCertsFromPEM([]byte(cfg.opnsenseCACert)) {
|
||||
return nil, errors.New("OPNSENSE_CA_PATH contains no PEM certificates")
|
||||
}
|
||||
transport := &http.Transport{TLSClientConfig: &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
RootCAs: roots,
|
||||
}}
|
||||
return &http.Client{Timeout: cfg.requestTimeout, Transport: transport}, nil
|
||||
}
|
||||
|
||||
func requiredHTTPSOrigin(name string) (string, error) {
|
||||
raw := os.Getenv(name)
|
||||
if raw == "" {
|
||||
return "", fmt.Errorf("%s is required", name)
|
||||
}
|
||||
parsed, err := url.Parse(raw)
|
||||
if err != nil || parsed.Scheme != "https" || parsed.Host == "" || parsed.User != nil || parsed.RawQuery != "" || parsed.Fragment != "" || (parsed.Path != "" && parsed.Path != "/") {
|
||||
return "", fmt.Errorf("%s must be an HTTPS origin", name)
|
||||
}
|
||||
return strings.TrimRight(raw, "/"), nil
|
||||
}
|
||||
|
||||
func envOrDefault(name, fallback string) string {
|
||||
if value := os.Getenv(name); value != "" {
|
||||
return value
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func requiredMetricsURL(name string) (string, error) {
|
||||
value := os.Getenv(name)
|
||||
parsed, err := url.Parse(value)
|
||||
if err != nil || parsed.Scheme != "http" || parsed.Host == "" || parsed.User != nil || parsed.RawQuery != "" || parsed.Fragment != "" || parsed.Path != "/metrics" {
|
||||
return "", fmt.Errorf("%s must be an HTTP metrics endpoint", name)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
72
services/oci-l4-relay-wan-controller/main_test.go
Normal file
72
services/oci-l4-relay-wan-controller/main_test.go
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestWANIPv4RejectsPrivateAddress(t *testing.T) {
|
||||
server := interfaceServer(t, `{"rows":[{"identifier":"wan","description":"WAN","ipv4":[{"ipaddr":"10.180.186.43"}]}]}`)
|
||||
defer server.Close()
|
||||
client := &opnsenseClient{baseURL: server.URL, key: "key", secret: "secret", http: server.Client()}
|
||||
if _, err := client.wanIPv4(context.Background(), "WAN"); err == nil {
|
||||
t.Fatal("expected private address rejection")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWANIPv4ReturnsPublicAddressByDescription(t *testing.T) {
|
||||
server := interfaceServer(t, `{"rows":[{"identifier":"wan","description":"WAN","ipv4":[{"ipaddr":"203.0.113.7/24"}]}]}`)
|
||||
defer server.Close()
|
||||
client := &opnsenseClient{baseURL: server.URL, http: server.Client()}
|
||||
ip, err := client.wanIPv4(context.Background(), "WAN")
|
||||
if err != nil || !ip.Equal(net.ParseIP("203.0.113.7")) {
|
||||
t.Fatalf("WAN address = %v, error = %v", ip, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWANIPv4RejectsAmbiguousSelector(t *testing.T) {
|
||||
server := interfaceServer(t, `{"rows":[{"identifier":"wan","description":"WAN","ipv4":[{"ipaddr":"203.0.113.7"}]},{"identifier":"WAN","description":"other","ipv4":[{"ipaddr":"203.0.113.8"}]}]}`)
|
||||
defer server.Close()
|
||||
client := &opnsenseClient{baseURL: server.URL, http: server.Client()}
|
||||
if _, err := client.wanIPv4(context.Background(), "WAN"); err == nil {
|
||||
t.Fatal("expected ambiguous selector rejection")
|
||||
}
|
||||
}
|
||||
|
||||
func interfaceServer(t *testing.T, body string) *httptest.Server {
|
||||
t.Helper()
|
||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/api/interfaces/overview/interfaces_info" {
|
||||
t.Errorf("path = %q", r.URL.Path)
|
||||
}
|
||||
_, _ = w.Write([]byte(body))
|
||||
}))
|
||||
}
|
||||
|
||||
type recordingReconciler struct {
|
||||
cancel context.CancelFunc
|
||||
calls int
|
||||
}
|
||||
|
||||
func (r *recordingReconciler) Reconcile(_ context.Context, _ net.IP) error {
|
||||
r.calls++
|
||||
r.cancel()
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestPollDebouncesWANChange(t *testing.T) {
|
||||
server := interfaceServer(t, `{"rows":[{"identifier":"wan","description":"WAN","ipv4":[{"ipaddr":"203.0.113.7"}]}]}`)
|
||||
defer server.Close()
|
||||
client := &opnsenseClient{baseURL: server.URL, http: server.Client()}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
reconciler := &recordingReconciler{cancel: cancel}
|
||||
_ = poll(ctx, client, "WAN", time.Millisecond, 2, reconciler)
|
||||
if reconciler.calls != 1 {
|
||||
t.Fatalf("reconcile calls = %d", reconciler.calls)
|
||||
}
|
||||
}
|
||||
118
services/oci-l4-relay-wan-controller/oci.go
Normal file
118
services/oci-l4-relay-wan-controller/oci.go
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/oracle/oci-go-sdk/v65/common"
|
||||
"github.com/oracle/oci-go-sdk/v65/core"
|
||||
)
|
||||
|
||||
type networkSecurityGroupClient interface {
|
||||
UpdateNetworkSecurityGroupSecurityRules(context.Context, core.UpdateNetworkSecurityGroupSecurityRulesRequest) (core.UpdateNetworkSecurityGroupSecurityRulesResponse, error)
|
||||
}
|
||||
|
||||
type ociReconciler struct {
|
||||
client networkSecurityGroupClient
|
||||
networkSecurityGroup string
|
||||
securityRule string
|
||||
wireguardDestinationPort int
|
||||
wireguardURL string
|
||||
recoveryTimeout time.Duration
|
||||
probeWireGuard func(context.Context, string, time.Duration) error
|
||||
}
|
||||
|
||||
func newOCIReconciler(cfg config) (*ociReconciler, error) {
|
||||
provider := common.NewRawConfigurationProvider(
|
||||
cfg.ociTenancy,
|
||||
cfg.ociUser,
|
||||
cfg.ociRegion,
|
||||
cfg.ociFingerprint,
|
||||
cfg.ociPrivateKey,
|
||||
nil,
|
||||
)
|
||||
client, err := core.NewVirtualNetworkClientWithConfigurationProvider(provider)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create OCI virtual network client: %w", err)
|
||||
}
|
||||
return &ociReconciler{
|
||||
client: &client,
|
||||
networkSecurityGroup: cfg.networkSecurityGroup,
|
||||
securityRule: cfg.securityRule,
|
||||
wireguardDestinationPort: cfg.wireguardPort,
|
||||
wireguardURL: cfg.wireguardURL,
|
||||
recoveryTimeout: cfg.recoveryTimeout,
|
||||
probeWireGuard: waitForWireGuard,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *ociReconciler) Reconcile(ctx context.Context, source net.IP) error {
|
||||
if !validPublicIPv4(source) {
|
||||
return fmt.Errorf("refuse invalid WAN source %q", source)
|
||||
}
|
||||
port := r.wireguardDestinationPort
|
||||
protocol := "17"
|
||||
description := "Home WireGuard peer"
|
||||
sourceCIDR := source.String() + "/32"
|
||||
response, err := r.client.UpdateNetworkSecurityGroupSecurityRules(ctx, core.UpdateNetworkSecurityGroupSecurityRulesRequest{
|
||||
NetworkSecurityGroupId: common.String(r.networkSecurityGroup),
|
||||
UpdateNetworkSecurityGroupSecurityRulesDetails: core.UpdateNetworkSecurityGroupSecurityRulesDetails{
|
||||
SecurityRules: []core.UpdateSecurityRuleDetails{{
|
||||
Direction: core.UpdateSecurityRuleDetailsDirectionIngress,
|
||||
Id: common.String(r.securityRule),
|
||||
Protocol: common.String(protocol),
|
||||
Description: common.String(description),
|
||||
Source: common.String(sourceCIDR),
|
||||
SourceType: core.UpdateSecurityRuleDetailsSourceTypeCidrBlock,
|
||||
IsStateless: common.Bool(false),
|
||||
UdpOptions: &core.UdpOptions{DestinationPortRange: &core.PortRange{Min: common.Int(port), Max: common.Int(port)}},
|
||||
}},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("update WireGuard NSG source: %w", err)
|
||||
}
|
||||
if err := verifyUpdatedRule(response.SecurityRules, r.securityRule, sourceCIDR, port); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.probeWireGuard(ctx, r.wireguardURL, r.recoveryTimeout); err != nil {
|
||||
return fmt.Errorf("verify WireGuard connectivity: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func verifyUpdatedRule(rules []core.SecurityRule, id, source string, port int) error {
|
||||
if len(rules) != 1 {
|
||||
return fmt.Errorf("OCI returned %d updated rules, want 1", len(rules))
|
||||
}
|
||||
rule := rules[0]
|
||||
if stringValue(rule.Id) != id || stringValue(rule.Source) != source ||
|
||||
stringValue(rule.Protocol) != "17" || rule.Direction != core.SecurityRuleDirectionIngress ||
|
||||
rule.SourceType != core.SecurityRuleSourceTypeCidrBlock || boolValue(rule.IsStateless) ||
|
||||
rule.UdpOptions == nil || rule.UdpOptions.DestinationPortRange == nil ||
|
||||
intValue(rule.UdpOptions.DestinationPortRange.Min) != port ||
|
||||
intValue(rule.UdpOptions.DestinationPortRange.Max) != port {
|
||||
return fmt.Errorf("OCI returned an unexpected WireGuard rule")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func stringValue(value *string) string {
|
||||
if value == nil {
|
||||
return ""
|
||||
}
|
||||
return *value
|
||||
}
|
||||
|
||||
func boolValue(value *bool) bool {
|
||||
return value != nil && *value
|
||||
}
|
||||
|
||||
func intValue(value *int) int {
|
||||
if value == nil {
|
||||
return 0
|
||||
}
|
||||
return *value
|
||||
}
|
||||
85
services/oci-l4-relay-wan-controller/oci_test.go
Normal file
85
services/oci-l4-relay-wan-controller/oci_test.go
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/oracle/oci-go-sdk/v65/common"
|
||||
"github.com/oracle/oci-go-sdk/v65/core"
|
||||
)
|
||||
|
||||
type recordingNSGClient struct {
|
||||
request core.UpdateNetworkSecurityGroupSecurityRulesRequest
|
||||
calls int
|
||||
rules []core.SecurityRule
|
||||
}
|
||||
|
||||
func (c *recordingNSGClient) UpdateNetworkSecurityGroupSecurityRules(_ context.Context, request core.UpdateNetworkSecurityGroupSecurityRulesRequest) (core.UpdateNetworkSecurityGroupSecurityRulesResponse, error) {
|
||||
c.request = request
|
||||
c.calls++
|
||||
return core.UpdateNetworkSecurityGroupSecurityRulesResponse{
|
||||
UpdatedNetworkSecurityGroupSecurityRules: core.UpdatedNetworkSecurityGroupSecurityRules{SecurityRules: c.rules},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func returnedRule(id, source string, port int) core.SecurityRule {
|
||||
return core.SecurityRule{
|
||||
Direction: core.SecurityRuleDirectionIngress,
|
||||
Id: common.String(id),
|
||||
Protocol: common.String("17"),
|
||||
Source: common.String(source),
|
||||
SourceType: core.SecurityRuleSourceTypeCidrBlock,
|
||||
IsStateless: common.Bool(false),
|
||||
UdpOptions: &core.UdpOptions{DestinationPortRange: &core.PortRange{Min: common.Int(port), Max: common.Int(port)}},
|
||||
}
|
||||
}
|
||||
|
||||
func TestOCIReconcilerUpdatesOnlyConfiguredWireGuardRule(t *testing.T) {
|
||||
client := &recordingNSGClient{rules: []core.SecurityRule{returnedRule("rule", "203.0.113.7/32", 51820)}}
|
||||
probed := false
|
||||
reconciler := &ociReconciler{
|
||||
client: client, networkSecurityGroup: "nsg", securityRule: "rule", wireguardDestinationPort: 51820,
|
||||
wireguardURL: "http://relay.test:19100/metrics", recoveryTimeout: time.Minute,
|
||||
probeWireGuard: func(_ context.Context, address string, timeout time.Duration) error {
|
||||
probed = address == "http://relay.test:19100/metrics" && timeout == time.Minute
|
||||
return nil
|
||||
},
|
||||
}
|
||||
if err := reconciler.Reconcile(context.Background(), net.ParseIP("203.0.113.7")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rules := client.request.SecurityRules
|
||||
if len(rules) != 1 {
|
||||
t.Fatalf("rule count = %d", len(rules))
|
||||
}
|
||||
rule := rules[0]
|
||||
if *rule.Id != "rule" || *rule.Source != "203.0.113.7/32" || *rule.Protocol != "17" {
|
||||
t.Fatalf("unexpected rule: %#v", rule)
|
||||
}
|
||||
if rule.UdpOptions == nil || *rule.UdpOptions.DestinationPortRange.Min != 51820 || *rule.UdpOptions.DestinationPortRange.Max != 51820 {
|
||||
t.Fatalf("unexpected UDP options: %#v", rule.UdpOptions)
|
||||
}
|
||||
if !probed {
|
||||
t.Fatal("WireGuard connectivity was not probed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOCIReconcilerRejectsUnexpectedUpdatedRule(t *testing.T) {
|
||||
client := &recordingNSGClient{rules: []core.SecurityRule{returnedRule("other", "203.0.113.7/32", 51820)}}
|
||||
reconciler := &ociReconciler{
|
||||
client: client, networkSecurityGroup: "nsg", securityRule: "rule", wireguardDestinationPort: 51820,
|
||||
probeWireGuard: func(context.Context, string, time.Duration) error { return nil },
|
||||
}
|
||||
if err := reconciler.Reconcile(context.Background(), net.ParseIP("203.0.113.7")); err == nil {
|
||||
t.Fatal("expected returned rule rejection")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOCIReconcilerRejectsPrivateSource(t *testing.T) {
|
||||
reconciler := &ociReconciler{client: &recordingNSGClient{}, networkSecurityGroup: "nsg", securityRule: "rule", wireguardDestinationPort: 51820}
|
||||
if err := reconciler.Reconcile(context.Background(), net.ParseIP("10.180.186.43")); err == nil {
|
||||
t.Fatal("expected private source rejection")
|
||||
}
|
||||
}
|
||||
52
services/oci-l4-relay-wan-controller/recovery.go
Normal file
52
services/oci-l4-relay-wan-controller/recovery.go
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func waitForWireGuard(ctx context.Context, endpoint string, timeout time.Duration) error {
|
||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
transport := &http.Transport{}
|
||||
defer transport.CloseIdleConnections()
|
||||
client := &http.Client{
|
||||
Transport: transport,
|
||||
Timeout: 5 * time.Second,
|
||||
CheckRedirect: func(_ *http.Request, _ []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
ticker := time.NewTicker(2 * time.Second)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
if err := probeWireGuard(ctx, client, endpoint); err == nil {
|
||||
return nil
|
||||
} else {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return fmt.Errorf("%w: %v", ctx.Err(), err)
|
||||
case <-ticker.C:
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func probeWireGuard(ctx context.Context, client *http.Client, endpoint string) error {
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = response.Body.Close() }()
|
||||
// The relay proxy fetches this response live from the OCI WireGuard address.
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("WireGuard endpoint returned HTTP %d", response.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
115
services/oci-l4-relay-wan-controller/recovery_test.go
Normal file
115
services/oci-l4-relay-wan-controller/recovery_test.go
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/oracle/oci-go-sdk/v65/core"
|
||||
)
|
||||
|
||||
func TestWireGuardIgnoresWebHealth(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet || r.URL.Path != "/metrics" {
|
||||
t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
_, _ = w.Write([]byte("oci_l4_relay_component_healthy{component=\"web_allow\"} 0\n"))
|
||||
}))
|
||||
defer server.Close()
|
||||
if err := waitForWireGuard(context.Background(), server.URL+"/metrics", time.Second); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWireGuardRejectsUnavailableTunnel(t *testing.T) {
|
||||
for _, status := range []int{http.StatusBadGateway, http.StatusServiceUnavailable} {
|
||||
t.Run(http.StatusText(status), func(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(status)
|
||||
}))
|
||||
defer server.Close()
|
||||
err := waitForWireGuard(context.Background(), server.URL+"/metrics", 20*time.Millisecond)
|
||||
if !errors.Is(err, context.DeadlineExceeded) {
|
||||
t.Fatalf("expected recovery deadline, got %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWireGuardDoesNotFollowRedirects(t *testing.T) {
|
||||
var calls atomic.Int32
|
||||
destination := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
calls.Add(1)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer destination.Close()
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, destination.URL, http.StatusFound)
|
||||
}))
|
||||
defer server.Close()
|
||||
err := waitForWireGuard(context.Background(), server.URL+"/metrics", 20*time.Millisecond)
|
||||
if !errors.Is(err, context.DeadlineExceeded) || calls.Load() != 0 {
|
||||
t.Fatalf("error = %v, redirected calls = %d", err, calls.Load())
|
||||
}
|
||||
}
|
||||
|
||||
func TestWireGuardCancelsBlockedRequest(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
|
||||
<-r.Context().Done()
|
||||
}))
|
||||
defer server.Close()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)
|
||||
defer cancel()
|
||||
if err := waitForWireGuard(ctx, server.URL+"/metrics", time.Minute); !errors.Is(err, context.DeadlineExceeded) {
|
||||
t.Fatalf("expected cancellation, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPollStopsUpdatingAfterWireGuardRecovery(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
observations := 0
|
||||
opnsense := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
observations++
|
||||
if observations == 5 {
|
||||
cancel()
|
||||
}
|
||||
_, _ = w.Write([]byte(`{"rows":[{"identifier":"wan","description":"WAN","ipv4":[{"ipaddr":"203.0.113.7"}]}]}`))
|
||||
}))
|
||||
defer opnsense.Close()
|
||||
var probes atomic.Int32
|
||||
tunnel := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
probes.Add(1)
|
||||
_, _ = w.Write([]byte("oci_l4_relay_component_healthy{component=\"web_allow\"} 0\n"))
|
||||
}))
|
||||
defer tunnel.Close()
|
||||
nsg := &recordingNSGClient{rules: []core.SecurityRule{returnedRule("rule", "203.0.113.7/32", 51820)}}
|
||||
reconciler := &ociReconciler{
|
||||
client: nsg, networkSecurityGroup: "nsg", securityRule: "rule", wireguardDestinationPort: 51820,
|
||||
wireguardURL: tunnel.URL + "/metrics", recoveryTimeout: time.Second, probeWireGuard: waitForWireGuard,
|
||||
}
|
||||
client := &opnsenseClient{baseURL: opnsense.URL, http: opnsense.Client()}
|
||||
_ = poll(ctx, client, "WAN", time.Millisecond, 2, reconciler)
|
||||
if observations != 5 || nsg.calls != 1 || probes.Load() != 1 {
|
||||
t.Fatalf("observations = %d, NSG updates = %d, tunnel probes = %d", observations, nsg.calls, probes.Load())
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetricsURL(t *testing.T) {
|
||||
for _, value := range []string{"", "https://relay.test/metrics", "http://relay.test/", "http://relay.test/metrics?query=1", "http://key:[email protected]/metrics"} {
|
||||
t.Run(value, func(t *testing.T) {
|
||||
t.Setenv("WG_STATUS_URL", value)
|
||||
if _, err := requiredMetricsURL("WG_STATUS_URL"); err == nil {
|
||||
t.Fatal("expected invalid endpoint rejection")
|
||||
}
|
||||
})
|
||||
}
|
||||
t.Setenv("WG_STATUS_URL", "http://relay.test:19100/metrics")
|
||||
if _, err := requiredMetricsURL("WG_STATUS_URL"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue