cloudproxy: initial version

This commit is contained in:
Yousong Zhou
2021-02-23 15:34:23 +08:00
parent 98fcdb2be5
commit 5eefa38f84
35 changed files with 3079 additions and 0 deletions
+15
View File
@@ -0,0 +1,15 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package models // import "yunion.io/x/onecloud/pkg/proxyagent/models"
+43
View File
@@ -0,0 +1,43 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package models
import (
proxy_models "yunion.io/x/onecloud/pkg/cloudproxy/models"
)
type ProxyEndpoint struct {
proxy_models.SProxyEndpoint
Forwards Forwards `json:"-"`
}
func (el *ProxyEndpoint) Copy() *ProxyEndpoint {
return &ProxyEndpoint{
SProxyEndpoint: el.SProxyEndpoint,
}
}
type Forward struct {
proxy_models.SForward
ProxyEndpoint *ProxyEndpoint
}
func (el *Forward) Copy() *Forward {
return &Forward{
SForward: el.SForward,
}
}
+90
View File
@@ -0,0 +1,90 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package models
import (
"yunion.io/x/log"
"yunion.io/x/onecloud/pkg/apihelper"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
mcclient_modulebase "yunion.io/x/onecloud/pkg/mcclient/modulebase"
mcclient_modules "yunion.io/x/onecloud/pkg/mcclient/modules/cloudproxy"
)
type (
ProxyEndpoints map[string]*ProxyEndpoint
Forwards map[string]*Forward
)
func (set ProxyEndpoints) ModelManager() mcclient_modulebase.IBaseManager {
return &mcclient_modules.ProxyEndpoints
}
func (set ProxyEndpoints) NewModel() db.IModel {
return &ProxyEndpoint{}
}
func (set ProxyEndpoints) AddModel(i db.IModel) {
m := i.(*ProxyEndpoint)
set[m.Id] = m
}
func (set ProxyEndpoints) Copy() apihelper.IModelSet {
setCopy := ProxyEndpoints{}
for id, el := range set {
setCopy[id] = el.Copy()
}
return setCopy
}
func (ms ProxyEndpoints) joinForwards(subEntries Forwards) bool {
correct := true
for _, subEntry := range subEntries {
epId := subEntry.ProxyEndpointId
m, ok := ms[epId]
if !ok {
log.Warningf("proxy_endpoint_id %s of forward %s(%s) is not present", epId, subEntry.Name, subEntry.Id)
correct = false
continue
}
subEntry.ProxyEndpoint = m
if m.Forwards == nil {
m.Forwards = Forwards{}
}
m.Forwards[subEntry.Id] = subEntry
}
return correct
}
func (set Forwards) ModelManager() mcclient_modulebase.IBaseManager {
return &mcclient_modules.Forwards
}
func (set Forwards) NewModel() db.IModel {
return &Forward{}
}
func (set Forwards) AddModel(i db.IModel) {
m := i.(*Forward)
set[m.Id] = m
}
func (set Forwards) Copy() apihelper.IModelSet {
setCopy := Forwards{}
for id, el := range set {
setCopy[id] = el.Copy()
}
return setCopy
}
+106
View File
@@ -0,0 +1,106 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package models
import (
"time"
"yunion.io/x/onecloud/pkg/apihelper"
)
type ModelSetsMaxUpdatedAt struct {
ProxyEndpoints time.Time
Forwards time.Time
}
func NewModelSetsMaxUpdatedAt() *ModelSetsMaxUpdatedAt {
return &ModelSetsMaxUpdatedAt{
ProxyEndpoints: apihelper.PseudoZeroTime,
Forwards: apihelper.PseudoZeroTime,
}
}
type ModelSets struct {
ProxyEndpoints ProxyEndpoints
Forwards Forwards
}
func NewModelSets() *ModelSets {
return &ModelSets{
ProxyEndpoints: ProxyEndpoints{},
Forwards: Forwards{},
}
}
func (mss *ModelSets) ModelSetList() []apihelper.IModelSet {
// it's ordered this way to favour creation, not deletion
return []apihelper.IModelSet{
mss.ProxyEndpoints,
mss.Forwards,
}
}
func (mss *ModelSets) NewEmpty() apihelper.IModelSets {
return NewModelSets()
}
func (mss *ModelSets) copy_() *ModelSets {
mssCopy := &ModelSets{
ProxyEndpoints: mss.ProxyEndpoints.Copy().(ProxyEndpoints),
Forwards: mss.Forwards.Copy().(Forwards),
}
return mssCopy
}
func (mss *ModelSets) Copy() apihelper.IModelSets {
return mss.copy_()
}
func (mss *ModelSets) CopyJoined() apihelper.IModelSets {
mssCopy := mss.copy_()
mssCopy.join()
return mssCopy
}
func (mss *ModelSets) ApplyUpdates(mssNews apihelper.IModelSets) apihelper.ModelSetsUpdateResult {
r := apihelper.ModelSetsUpdateResult{
Changed: false,
Correct: true,
}
mssList := mss.ModelSetList()
mssNewsList := mssNews.ModelSetList()
for i, mss := range mssList {
mssNews := mssNewsList[i]
msR := apihelper.ModelSetApplyUpdates(mss, mssNews)
if !r.Changed && msR.Changed {
r.Changed = true
}
}
if r.Changed {
r.Correct = mss.join()
}
return r
}
func (mss *ModelSets) join() bool {
var p []bool
p = append(p, mss.ProxyEndpoints.joinForwards(mss.Forwards))
for _, b := range p {
if !b {
return false
}
}
return true
}
+15
View File
@@ -0,0 +1,15 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package options // import "yunion.io/x/onecloud/pkg/proxyagent/options"
+54
View File
@@ -0,0 +1,54 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package options
import (
"fmt"
"time"
)
type Options struct {
ProxyAgentId string
ProxyAgentInitWait string `help:"duration to try and wait for init" default:"15s"`
proxyAgentInitWaitDuration time.Duration
APISyncInterval int `default:"10"`
APIListBatchSize int `default:"1024"`
}
func (opts *Options) GetProxyAgentInitWaitDuration() time.Duration {
return opts.proxyAgentInitWaitDuration
}
func (opts *Options) ValidateThenInit() error {
if opts.ProxyAgentId == "" {
return fmt.Errorf("empty proxy_agent_id")
}
if d, err := time.ParseDuration(opts.ProxyAgentInitWait); err != nil {
return fmt.Errorf("parse proxy_agent_init_wait: %v", err)
} else {
opts.proxyAgentInitWaitDuration = d
}
if opts.APIListBatchSize <= 20 {
opts.APIListBatchSize = 20
}
if opts.APISyncInterval <= 10 {
opts.APISyncInterval = 10
}
return nil
}
+345
View File
@@ -0,0 +1,345 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ssh
import (
"context"
"fmt"
"net"
"sync"
"time"
"golang.org/x/crypto/ssh"
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
"yunion.io/x/pkg/util/sets"
)
type addrMap map[string]interface{}
type portMap map[int]addrMap
func (pm portMap) contains(port int, addr string) bool {
am, ok := pm[port]
if !ok {
return false
}
return am.contains(addr)
}
func (pm portMap) get(port int, addr string) interface{} {
am, ok := pm[port]
if ok {
return am.get(addr)
}
return nil
}
func (pm portMap) set(port int, addr string, v interface{}) {
am, ok := pm[port]
if !ok {
am = addrMap{}
pm[port] = am
}
am.set(addr, v)
}
func (pm portMap) delete(port int, addr string) {
if am, ok := pm[port]; ok {
am.delete(addr)
}
}
func (am addrMap) contains(addr string) bool {
const (
ip4wild = "0.0.0.0"
ip6wild = "::"
)
_, ok := am[addr]
if ok {
return true
}
if _, ok := am[ip4wild]; ok {
return true
}
if _, ok := am[ip6wild]; ok {
return true
}
return false
}
func (am addrMap) get(addr string) interface{} {
return am[addr]
}
func (am addrMap) set(addr string, v interface{}) {
am[addr] = v
}
func (am addrMap) delete(addr string) {
delete(am, addr)
}
type Client struct {
cc *ClientConfig
c *ssh.Client
stopc chan sets.Empty
stopcEx *sync.Mutex
stopcc bool
wakec chan sets.Empty
lfc chan LocalForwardReq
rfc chan RemoteForwardReq
lfclosec chan LocalForwardReq
rfclosec chan RemoteForwardReq
localForwards portMap
remoteForwards portMap
}
func NewClient(cc *ClientConfig) *Client {
c := &Client{
cc: cc,
stopc: make(chan sets.Empty),
stopcEx: &sync.Mutex{},
wakec: make(chan sets.Empty),
lfc: make(chan LocalForwardReq),
rfc: make(chan RemoteForwardReq),
lfclosec: make(chan LocalForwardReq),
rfclosec: make(chan RemoteForwardReq),
localForwards: portMap{},
remoteForwards: portMap{},
}
return c
}
func (c *Client) Stop(ctx context.Context) {
c.stopcEx.Lock()
defer c.stopcEx.Unlock()
if !c.stopcc {
close(c.stopc)
c.stopcc = true
}
}
func (c *Client) Start(ctx context.Context) {
pingT := time.NewTimer(17 * time.Second)
pingFailCount := 0
const pingMaxFail = 3
const (
stateInit = iota
stateOK
)
state := stateInit
stateC := make(chan int)
stateInitRetryInterval := 7 * time.Second
stateInitRetryT := time.NewTicker(stateInitRetryInterval)
for {
switch state {
case stateOK:
// check forwards and start
case stateInit:
if sshc, err := c.connect(ctx); err != nil {
log.Errorf("ssh connect: %v", err)
} else {
c.c = sshc
state = stateOK
go func() {
defer c.c.Conn.Close()
err := c.c.Conn.Wait()
if err != nil {
log.Errorf("ssh client conn: %v", err)
}
select {
case stateC <- stateInit:
case <-ctx.Done():
}
}()
}
}
select {
case req := <-c.lfc:
if c.c != nil {
c.localForward(ctx, req)
}
case req := <-c.rfc:
if c.c != nil {
c.remoteForward(ctx, req)
}
case req := <-c.lfclosec:
c.localForwardClose(ctx, req)
case req := <-c.rfclosec:
c.remoteForwardClose(ctx, req)
case <-c.wakec:
break
case <-pingT.C:
//TODO ping check
//ping fail
if pingFailCount > pingMaxFail {
state = stateInit
}
case newState := <-stateC:
state = newState
case <-stateInitRetryT.C:
case <-c.stopc:
if c.c != nil {
c.c.Conn.Close()
}
return
case <-ctx.Done():
return
}
}
}
func (c *Client) connect(ctx context.Context) (*ssh.Client, error) {
sshc, err := c.cc.NewClient(ctx)
return sshc, err
}
func (c *Client) LocalForward(ctx context.Context, req LocalForwardReq) {
select {
case c.lfc <- req:
case <-ctx.Done():
}
}
func (c *Client) localForward(ctx context.Context, req LocalForwardReq) {
if err := c.localForward_(ctx, req); err != nil {
log.Errorf("local forward: %v", err)
}
}
func (c *Client) localForward_(ctx context.Context, req LocalForwardReq) error {
// check LocalAddr/LocalPort existence
if c.localForwards.contains(req.LocalPort, req.LocalAddr) {
return errors.Errorf("local addr occupied: %s:%d", req.LocalAddr, req.LocalPort)
}
addr := net.JoinHostPort(req.LocalAddr, fmt.Sprintf("%d", req.LocalPort))
listener, err := net.Listen("tcp", addr)
if err != nil {
return errors.Wrapf(err, "tcp listen %s", addr)
}
fwd := &forwarder{
listener: listener,
dial: c.c.Dial,
dialAddr: req.RemoteAddr,
dialPort: req.RemotePort,
done: c.localForwardDone,
doneAddr: req.LocalAddr,
donePort: req.LocalPort,
tick: req.Tick,
tickCb: req.TickCb,
}
c.localForwards.set(req.LocalPort, req.LocalAddr, fwd)
go fwd.Start(ctx)
return nil
}
func (c *Client) localForwardDone(laddr string, lport int) {
c.localForwards.delete(lport, laddr)
}
func (c *Client) RemoteForward(ctx context.Context, req RemoteForwardReq) {
select {
case c.rfc <- req:
case <-ctx.Done():
}
}
func (c *Client) remoteForward(ctx context.Context, req RemoteForwardReq) {
if err := c.remoteForward_(ctx, req); err != nil {
log.Errorf("remote forward: %v", err)
}
}
func (c *Client) remoteForward_(ctx context.Context, req RemoteForwardReq) error {
// check RemoteAddr/RemotePort existence
if c.remoteForwards.contains(req.RemotePort, req.RemoteAddr) {
return errors.Errorf("remote addr occupied: %s:%d", req.RemoteAddr, req.RemotePort)
}
addr := net.JoinHostPort(req.RemoteAddr, fmt.Sprintf("%d", req.RemotePort))
listener, err := c.c.Listen("tcp", addr)
if err != nil {
return errors.Wrapf(err, "ssh listen %s", addr)
}
fwd := &forwarder{
listener: listener,
dial: net.Dial,
dialAddr: req.LocalAddr,
dialPort: req.LocalPort,
done: c.remoteForwardDone,
doneAddr: req.RemoteAddr,
donePort: req.RemotePort,
tick: req.Tick,
tickCb: req.TickCb,
}
c.remoteForwards.set(req.RemotePort, req.RemoteAddr, fwd)
go fwd.Start(ctx)
return nil
}
func (c *Client) remoteForwardDone(raddr string, rport int) {
c.remoteForwards.delete(rport, raddr)
}
func (c *Client) LocalForwardClose(ctx context.Context, req LocalForwardReq) {
select {
case c.lfclosec <- req:
case <-ctx.Done():
}
}
func (c *Client) localForwardClose(ctx context.Context, req LocalForwardReq) {
v := c.localForwards.get(req.LocalPort, req.LocalAddr)
if v != nil {
fwd := v.(*forwarder)
fwd.Stop(ctx)
}
}
func (c *Client) RemoteForwardClose(ctx context.Context, req RemoteForwardReq) {
select {
case c.rfclosec <- req:
case <-ctx.Done():
}
}
func (c *Client) remoteForwardClose(ctx context.Context, req RemoteForwardReq) {
v := c.remoteForwards.get(req.RemotePort, req.RemoteAddr)
if v != nil {
fwd := v.(*forwarder)
fwd.Stop(ctx)
}
}
+62
View File
@@ -0,0 +1,62 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ssh
import (
"context"
"fmt"
"net"
"golang.org/x/crypto/ssh"
"yunion.io/x/pkg/errors"
)
type ClientConfig struct {
User string
Host string
Port int
Key string
}
func (cc *ClientConfig) NewClient(ctx context.Context) (*ssh.Client, error) {
signer, err := ssh.ParsePrivateKey([]byte(cc.Key))
if err != nil {
return nil, errors.Wrap(err, "parse ssh key")
}
sshcc := &ssh.ClientConfig{
User: cc.User,
Auth: []ssh.AuthMethod{
ssh.PublicKeys(signer),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
addr := net.JoinHostPort(cc.Host, fmt.Sprintf("%d", cc.Port))
d := &net.Dialer{}
netconn, err := d.DialContext(ctx, "tcp", addr)
if err != nil {
return nil, errors.Wrap(err, "net dial")
}
sshconn, chans, reqs, err := ssh.NewClientConn(netconn, addr, sshcc)
if err != nil {
netconn.Close()
return nil, errors.Wrap(err, "ssh new client conn")
}
sshc := ssh.NewClient(sshconn, chans, reqs)
return sshc, nil
}
+184
View File
@@ -0,0 +1,184 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ssh
import (
"context"
)
type epClientSet struct {
cc ClientConfig
clients []*Client
mark bool
}
func (epcs *epClientSet) clearMark() {
epcs.mark = false
}
func (epcs *epClientSet) setMark() {
epcs.mark = true
}
func (epcs *epClientSet) getMark() bool {
return epcs.mark
}
func (epcs *epClientSet) stop(ctx context.Context) {
for _, client := range epcs.clients {
client.Stop(ctx)
}
}
type epClients map[string]*epClientSet // key: epKey
type ClientSet struct {
epClients epClients
}
func NewClientSet() *ClientSet {
cs := &ClientSet{
epClients: epClients{},
}
return cs
}
func (cs *ClientSet) ClearAllMark() {
for _, epcs := range cs.epClients {
epcs.clearMark()
}
}
func (cs *ClientSet) ResetIfChanged(ctx context.Context, epKey string, cc ClientConfig) bool {
epcs, ok := cs.epClients[epKey]
if ok {
if epcs.cc != cc {
epcs.stop(ctx)
delete(cs.epClients, epKey)
return true
}
epcs.setMark()
}
return false
}
func (cs *ClientSet) AddIfNotExist(ctx context.Context, epKey string, cc ClientConfig) bool {
epcs, ok := cs.epClients[epKey]
if !ok {
epcs := &epClientSet{
cc: cc,
}
epcs.setMark()
cs.epClients[epKey] = epcs
return true
}
epcs.setMark()
return false
}
func (cs *ClientSet) ResetUnmarked(ctx context.Context) {
for epKey, epcs := range cs.epClients {
if !epcs.getMark() {
epcs.stop(ctx)
delete(cs.epClients, epKey)
}
}
}
func (cs *ClientSet) ForwardKeySet() ForwardKeySet {
fks := ForwardKeySet{}
for epKey, epcs := range cs.epClients {
for _, client := range epcs.clients {
fks.addByPortMap(epKey, ForwardKeyTypeL, client.localForwards)
fks.addByPortMap(epKey, ForwardKeyTypeR, client.remoteForwards)
}
}
return fks
}
func (cs *ClientSet) LocalForward(ctx context.Context, epKey string, req LocalForwardReq) {
client, created := cs.getOrCreateClient(epKey, ForwardKeyTypeL)
if created {
go client.Start(ctx)
}
client.LocalForward(ctx, req)
}
func (cs *ClientSet) RemoteForward(ctx context.Context, epKey string, req RemoteForwardReq) {
client, created := cs.getOrCreateClient(epKey, ForwardKeyTypeR)
if created {
go client.Start(ctx)
}
client.RemoteForward(ctx, req)
}
func (cs *ClientSet) CloseForward(ctx context.Context, fk ForwardKey) {
client := cs.getClient(fk.EpKey, fk.Type)
if client == nil {
return
}
switch fk.Type {
case ForwardKeyTypeL:
client.LocalForwardClose(ctx, LocalForwardReq{
LocalAddr: fk.KeyAddr,
LocalPort: fk.KeyPort,
})
case ForwardKeyTypeR:
client.RemoteForwardClose(ctx, RemoteForwardReq{
RemoteAddr: fk.KeyAddr,
RemotePort: fk.KeyPort,
})
}
}
/*
func (cs *ClientSet) LocalForwardClose(ctx context.Context, epKey string, req LocalForwardReq) {
client := cs.getClient(epKey, ForwardKeyTypeL)
client.LocalForwardClose(ctx, req)
}
func (cs *ClientSet) RemoteForwardClose(ctx context.Context, epKey string, req RemoteForwardReq) {
client := cs.getClient(epKey, ForwardKeyTypeR)
client.RemoteForwardClose(ctx, req)
}
*/
func (cs *ClientSet) getOrCreateClient(epKey string, typ string) (*Client, bool) {
return cs.getClient_(epKey, typ, true)
}
func (cs *ClientSet) getClient(epKey string, typ string) *Client {
client, _ := cs.getClient_(epKey, typ, false)
return client
}
func (cs *ClientSet) getClient_(epKey string, typ string, create bool) (*Client, bool) {
var client *Client
clients, ok := cs.epClients[epKey]
if !ok || len(clients.clients) == 0 {
if !create {
return nil, false
}
client = NewClient(&clients.cc)
clients.clients = append(clients.clients, client)
cs.epClients[epKey] = clients
return client, true
} else {
client = clients.clients[0]
return client, false
}
}
+15
View File
@@ -0,0 +1,15 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ssh // import "yunion.io/x/onecloud/pkg/proxyagent/agent/ssh"
+69
View File
@@ -0,0 +1,69 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ssh
import (
"fmt"
)
const (
ForwardKeyTypeL = "L"
ForwardKeyTypeR = "R"
)
type ForwardKey struct {
EpKey string
Type string
KeyAddr string
KeyPort int
Value interface{}
}
func (fk *ForwardKey) Key() string {
return fmt.Sprintf("%s/%s/%s/%d", fk.EpKey, fk.Type, fk.KeyAddr, fk.KeyPort)
}
type ForwardKeySet map[string]ForwardKey
func (fks ForwardKeySet) addByPortMap(epKey, typ string, pm portMap) {
for port, addrMap := range pm {
for addr := range addrMap {
fk := ForwardKey{
EpKey: epKey,
Type: typ,
KeyAddr: addr,
KeyPort: port,
}
fks.Add(fk)
}
}
}
func (fks ForwardKeySet) Contains(fk ForwardKey) bool {
key := fk.Key()
if _, ok := fks[key]; ok {
return true
}
return false
}
func (fks ForwardKeySet) Remove(fk ForwardKey) {
delete(fks, fk.Key())
}
func (fks ForwardKeySet) Add(fk ForwardKey) {
fks[fk.Key()] = fk
}
+147
View File
@@ -0,0 +1,147 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ssh
import (
"context"
"fmt"
"io"
"net"
"time"
"yunion.io/x/log"
)
type TickFunc func(context.Context)
type LocalForwardReq struct {
LocalAddr string
LocalPort int
RemoteAddr string
RemotePort int
Tick time.Duration
TickCb TickFunc
}
type RemoteForwardReq struct {
// LocalAddr is the address the forward will forward to
LocalAddr string
// LocalPort is the port the forward will forward to
LocalPort int
// RemoteAddr is the address on the remote to listen on
RemoteAddr string
// RemotePort is the address on the remote to listen on
RemotePort int
Tick time.Duration
TickCb TickFunc
}
type dialFunc func(n, addr string) (net.Conn, error)
type doneFunc func(laddr string, lport int)
type forwarder struct {
listener net.Listener
dial dialFunc
dialAddr string
dialPort int
done doneFunc
doneAddr string
donePort int
tick time.Duration
tickCb TickFunc
}
func (fwd *forwarder) Stop(ctx context.Context) {
fwd.listener.Close()
}
func (fwd *forwarder) Start(
ctx context.Context,
) {
var (
listener = fwd.listener
dial = fwd.dial
dialAddr = fwd.dialAddr
dialPort = fwd.dialPort
done = fwd.done
doneAddr = fwd.doneAddr
donePort = fwd.donePort
tick = fwd.tick
tickCb = fwd.tickCb
)
ctx, cancelFunc := context.WithCancel(ctx)
if done != nil {
defer done(doneAddr, donePort)
}
defer listener.Close()
go func() { // accept local/remote connection
for {
conn, err := listener.Accept()
if err != nil {
log.Warningf("local forward: accept: %v", err)
cancelFunc()
break
}
go func(local net.Conn) {
defer local.Close()
// dial remote/local
addr := net.JoinHostPort(dialAddr, fmt.Sprintf("%d", dialPort))
remote, err := dial("tcp", addr)
if err != nil {
log.Warningf("local forward: dial remote: %v", err)
return
}
defer remote.Close()
// forward
go io.Copy(local, remote)
go io.Copy(remote, local)
<-ctx.Done()
}(conn)
}
}()
if tick > 0 && tickCb != nil {
go func() {
ticker := time.NewTicker(tick)
defer ticker.Stop()
for {
select {
case <-ticker.C:
tickCb(ctx)
case <-ctx.Done():
return
}
}
}()
}
for {
select {
case <-ctx.Done():
return
}
}
}
+15
View File
@@ -0,0 +1,15 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package worker // import "yunion.io/x/onecloud/pkg/proxyagent/agent/worker"
+44
View File
@@ -0,0 +1,44 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package worker
import (
"context"
"math/rand"
"time"
"yunion.io/x/log"
agentssh "yunion.io/x/onecloud/pkg/cloudproxy/agent/ssh"
"yunion.io/x/onecloud/pkg/mcclient/auth"
cloudproxy_modules "yunion.io/x/onecloud/pkg/mcclient/modules/cloudproxy"
)
func tickDuration(timeout int) time.Duration {
if timeout > 30 {
return time.Duration(timeout-5-rand.Intn(10)) * time.Second
}
return (time.Duration(timeout) * time.Second / 3) * 2
}
func heartbeatFunc(fwdId string, sessionCache *auth.SessionCache) agentssh.TickFunc {
return func(ctx context.Context) {
s := sessionCache.Get(ctx)
_, err := cloudproxy_modules.Forwards.PerformAction(s, fwdId, "heartbeat", nil)
if err != nil {
log.Errorf("forwarder heartbeat: %s: %v", fwdId, err)
}
}
}
+275
View File
@@ -0,0 +1,275 @@
// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package worker
import (
"context"
"runtime"
"runtime/debug"
"sync"
"time"
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
"yunion.io/x/pkg/utils"
"yunion.io/x/onecloud/pkg/apihelper"
api "yunion.io/x/onecloud/pkg/apis/cloudproxy"
common_options "yunion.io/x/onecloud/pkg/cloudcommon/options"
agentmodels "yunion.io/x/onecloud/pkg/cloudproxy/agent/models"
agentoptions "yunion.io/x/onecloud/pkg/cloudproxy/agent/options"
agentssh "yunion.io/x/onecloud/pkg/cloudproxy/agent/ssh"
"yunion.io/x/onecloud/pkg/mcclient/auth"
cloudproxy_modules "yunion.io/x/onecloud/pkg/mcclient/modules/cloudproxy"
"yunion.io/x/onecloud/pkg/util/netutils2"
)
type Worker struct {
commonOpts *common_options.CommonOptions
opts *agentoptions.Options
proxyAgentId string
bindAddr string
apih *apihelper.APIHelper
clientSet *agentssh.ClientSet
sessionCache *auth.SessionCache
}
func NewWorker(commonOpts *common_options.CommonOptions, opts *agentoptions.Options) *Worker {
modelSets := agentmodels.NewModelSets()
apiOpts := &apihelper.Options{
CommonOptions: *commonOpts,
SyncInterval: opts.APISyncInterval,
ListBatchSize: opts.APIListBatchSize,
}
apih, err := apihelper.NewAPIHelper(apiOpts, modelSets)
if err != nil {
return nil
}
w := &Worker{
commonOpts: commonOpts,
opts: opts,
proxyAgentId: opts.ProxyAgentId,
apih: apih,
clientSet: agentssh.NewClientSet(),
sessionCache: &auth.SessionCache{
Region: commonOpts.Region,
APIVersion: "v2",
UseAdminToken: true,
EarlyRefresh: time.Hour,
},
}
return w
}
func (w *Worker) initProxyAgent_(ctx context.Context) error {
s := w.sessionCache.Get(ctx)
var agentDetail api.ProxyAgentDetails
{
j, err := cloudproxy_modules.ProxyAgents.Get(s, w.proxyAgentId, nil)
if err != nil {
return errors.Wrapf(err, "fetch proxy agent %s", w.proxyAgentId)
}
if err := j.Unmarshal(&agentDetail); err != nil {
return errors.Wrapf(err, "unmarshal proxy agent detail: %s", j.String())
}
if agentDetail.Id == "" {
return errors.Error("proxy agent id is empty")
}
w.proxyAgentId = agentDetail.Id
}
var bindAddr = agentDetail.BindAddr
if bindAddr == "" {
var err error
bindAddr, err = netutils2.MyIP()
if err != nil {
return errors.Wrap(err, "find bind Addr")
}
}
w.bindAddr = bindAddr
if agentDetail.BindAddr == "" || agentDetail.AdvertiseAddr == "" {
var advertiseAddr = agentDetail.AdvertiseAddr
if advertiseAddr == "" {
if true { //TODO, fetch clusterIP from k8s environ
advertiseAddr = bindAddr
}
}
req := api.ProxyAgentUpdateInput{
BindAddr: bindAddr,
AdvertiseAddr: advertiseAddr,
}
reqJ := req.JSON(req)
if _, err := cloudproxy_modules.ProxyAgents.Put(s, w.proxyAgentId, reqJ); err != nil {
return errors.Wrapf(err, "update proxy agent addr: %s", reqJ.String())
}
}
return nil
}
func (w *Worker) initProxyAgent(ctx context.Context) error {
done, err := utils.NewFibonacciRetrierMaxElapse(
w.opts.GetProxyAgentInitWaitDuration(),
func(retrier utils.FibonacciRetrier) (bool, error) {
err := w.initProxyAgent_(ctx)
if err != nil {
return false, err
}
return true, nil
}).Start(ctx)
if done {
return nil
}
return err
}
func (w *Worker) Start(ctx context.Context) {
wg := ctx.Value("wg").(*sync.WaitGroup)
wg.Add(1)
defer func() {
log.Infoln("agent: worker bye")
wg.Done()
}()
if err := w.initProxyAgent(ctx); err != nil {
log.Errorf("init proxy agent: %v", err)
return
}
go w.apih.Start(ctx)
var mss *agentmodels.ModelSets
for {
select {
case imss := <-w.apih.ModelSets():
log.Infof("agent: got new data from api helper")
mss = imss.(*agentmodels.ModelSets)
if err := w.run(ctx, mss); err != nil {
log.Errorf("agent: %v", err)
}
case <-ctx.Done():
return
}
}
}
func (w *Worker) run(ctx context.Context, mss *agentmodels.ModelSets) (err error) {
defer func() {
if panicVal := recover(); panicVal != nil {
if panicErr, ok := panicVal.(runtime.Error); ok {
err = errors.Wrap(panicErr, string(debug.Stack()))
} else if panicErr, ok := panicVal.(error); ok {
err = panicErr
} else {
panic(panicVal)
}
}
}()
w.clientSet.ClearAllMark()
for _, pep := range mss.ProxyEndpoints {
cc := agentssh.ClientConfig{
User: pep.User,
Host: pep.Host,
Port: pep.Port,
Key: pep.PrivateKey,
}
if reset := w.clientSet.ResetIfChanged(ctx, pep.Id, cc); reset {
log.Warningf("proxy endpoint %s changed, connections reset", pep.Id)
} else if added := w.clientSet.AddIfNotExist(ctx, pep.Id, cc); added {
log.Infof("proxy endpoint %s added", pep.Id)
}
}
w.clientSet.ResetUnmarked(ctx)
removes := w.clientSet.ForwardKeySet()
adds := agentssh.ForwardKeySet{}
for _, pep := range mss.ProxyEndpoints {
for _, forward := range pep.Forwards {
if forward.ProxyAgentId != w.proxyAgentId {
continue
}
if forward.ProxyEndpointId == "" {
continue
}
var (
typ string
addr string
port int
)
switch forward.Type {
case api.FORWARD_TYPE_LOCAL:
addr = w.bindAddr
port = forward.BindPort
typ = agentssh.ForwardKeyTypeL
case api.FORWARD_TYPE_REMOTE:
addr = forward.ProxyEndpoint.IntranetIpAddr
port = forward.BindPort
typ = agentssh.ForwardKeyTypeR
default:
log.Warningf("unknown forward type %s", forward.Type)
continue
}
fk := agentssh.ForwardKey{
EpKey: forward.ProxyEndpointId,
Type: typ,
KeyAddr: addr,
KeyPort: port,
Value: forward,
}
if removes.Contains(fk) {
removes.Remove(fk)
} else {
adds.Add(fk)
}
}
}
for _, fk := range removes {
log.Infof("close forward %s", fk.Key())
w.clientSet.CloseForward(ctx, fk)
}
for _, fk := range adds {
log.Infof("open forward %s", fk.Key())
forward := fk.Value.(*agentmodels.Forward)
tick := tickDuration(forward.LastSeenTimeout)
tickCb := heartbeatFunc(forward.Id, w.sessionCache)
switch fk.Type {
case agentssh.ForwardKeyTypeL:
w.clientSet.LocalForward(ctx, fk.EpKey, agentssh.LocalForwardReq{
LocalAddr: fk.KeyAddr,
LocalPort: fk.KeyPort,
RemoteAddr: forward.RemoteAddr,
RemotePort: forward.RemotePort,
Tick: tick,
TickCb: tickCb,
})
case agentssh.ForwardKeyTypeR:
w.clientSet.RemoteForward(ctx, fk.EpKey, agentssh.RemoteForwardReq{
RemoteAddr: fk.KeyAddr,
RemotePort: fk.KeyPort,
LocalAddr: forward.RemoteAddr,
LocalPort: forward.RemotePort,
Tick: tick,
TickCb: tickCb,
})
}
}
return nil
}