原生 ssh 增加 socks 代理功能

This commit is contained in:
dushixiang
2021-11-15 20:01:19 +08:00
parent 569a1ad763
commit 8759365f08
7 changed files with 145 additions and 31 deletions

View File

@ -134,8 +134,18 @@ func SSHEndpoint(c echo.Context) (err error) {
recording = path.Join(config.GlobalCfg.Guacd.Recording, sessionId, "recording.cast") recording = path.Join(config.GlobalCfg.Guacd.Recording, sessionId, "recording.cast")
} }
attributes, err := assetRepository.FindAssetAttrMapByAssetId(s.AssetId)
if err != nil {
return WriteMessage(ws, NewMessage(Closed, "获取资产属性失败:"+err.Error()))
}
var xterm = "xterm-256color" var xterm = "xterm-256color"
nextTerminal, err := term.NewNextTerminal(ip, port, username, password, privateKey, passphrase, rows, cols, recording, xterm, true) var nextTerminal *term.NextTerminal
if "true" == attributes[constant.SocksProxyEnable] {
nextTerminal, err = term.NewNextTerminalUseSocks(ip, port, username, password, privateKey, passphrase, rows, cols, recording, xterm, true, attributes[constant.SocksProxyHost], attributes[constant.SocksProxyPort], attributes[constant.SocksProxyUsername], attributes[constant.SocksProxyPassword])
} else {
nextTerminal, err = term.NewNextTerminal(ip, port, username, password, privateKey, passphrase, rows, cols, recording, xterm, true)
}
if err != nil { if err != nil {
return WriteMessage(ws, NewMessage(Closed, "创建SSH客户端失败"+err.Error())) return WriteMessage(ws, NewMessage(Closed, "创建SSH客户端失败"+err.Error()))

View File

@ -59,9 +59,15 @@ const (
StatusEnabled = "enabled" StatusEnabled = "enabled"
StatusDisabled = "disabled" StatusDisabled = "disabled"
SocksProxyEnable = "socks-proxy-enable"
SocksProxyHost = "socks-proxy-host"
SocksProxyPort = "socks-proxy-port"
SocksProxyUsername = "socks-proxy-username"
SocksProxyPassword = "socks-proxy-password"
) )
var SSHParameterNames = []string{guacd.FontName, guacd.FontSize, guacd.ColorScheme, guacd.Backspace, guacd.TerminalType, SshMode} var SSHParameterNames = []string{guacd.FontName, guacd.FontSize, guacd.ColorScheme, guacd.Backspace, guacd.TerminalType, SshMode, SocksProxyEnable, SocksProxyHost, SocksProxyPort, SocksProxyUsername, SocksProxyPassword}
var RDPParameterNames = []string{guacd.Domain, guacd.RemoteApp, guacd.RemoteAppDir, guacd.RemoteAppArgs, guacd.EnableDrive, guacd.DrivePath} var RDPParameterNames = []string{guacd.Domain, guacd.RemoteApp, guacd.RemoteAppDir, guacd.RemoteAppArgs, guacd.EnableDrive, guacd.DrivePath}
var VNCParameterNames = []string{guacd.ColorDepth, guacd.Cursor, guacd.SwapRedBlue, guacd.DestHost, guacd.DestPort} var VNCParameterNames = []string{guacd.ColorDepth, guacd.Cursor, guacd.SwapRedBlue, guacd.DestHost, guacd.DestPort}
var TelnetParameterNames = []string{guacd.FontName, guacd.FontSize, guacd.ColorScheme, guacd.Backspace, guacd.TerminalType, guacd.UsernameRegex, guacd.PasswordRegex, guacd.LoginSuccessRegex, guacd.LoginFailureRegex} var TelnetParameterNames = []string{guacd.FontName, guacd.FontSize, guacd.ColorScheme, guacd.Backspace, guacd.TerminalType, guacd.UsernameRegex, guacd.PasswordRegex, guacd.LoginSuccessRegex, guacd.LoginFailureRegex}

View File

@ -229,7 +229,7 @@ func (opt *Tunnel) WriteInstructionAndFlush(instruction Instruction) error {
} }
func (opt *Tunnel) WriteAndFlush(p []byte) (int, error) { func (opt *Tunnel) WriteAndFlush(p []byte) (int, error) {
//fmt.Printf("-> %v\n", string(p)) fmt.Printf("-> %v\n", string(p))
nn, err := opt.writer.Write(p) nn, err := opt.writer.Write(p)
if err != nil { if err != nil {
return nn, err return nn, err
@ -257,7 +257,7 @@ func (opt *Tunnel) Read() (p []byte, err error) {
} }
buffer = buffer[0:read] buffer = buffer[0:read]
s := string(buffer) s := string(buffer)
//fmt.Printf("<- %v \n", s) fmt.Printf("<- %v \n", s)
if s == "rate=44100,channels=2;" { if s == "rate=44100,channels=2;" {
return make([]byte, 0), nil return make([]byte, 0), nil
} }

View File

@ -22,7 +22,18 @@ func NewNextTerminal(ip string, port int, username, password, privateKey, passph
if err != nil { if err != nil {
return nil, err return nil, err
} }
return newNT(err, sshClient, pipe, recording, term, rows, cols)
}
func NewNextTerminalUseSocks(ip string, port int, username, password, privateKey, passphrase string, rows, cols int, recording, term string, pipe bool, socksProxyHost, socksProxyPort, socksProxyUsername, socksProxyPassword string) (*NextTerminal, error) {
sshClient, err := NewSshClientUseSocks(ip, port, username, password, privateKey, passphrase, socksProxyHost, socksProxyPort, socksProxyUsername, socksProxyPassword)
if err != nil {
return nil, err
}
return newNT(err, sshClient, pipe, recording, term, rows, cols)
}
func newNT(err error, sshClient *ssh.Client, pipe bool, recording string, term string, rows int, cols int) (*NextTerminal, error) {
sshSession, err := sshClient.NewSession() sshSession, err := sshClient.NewSession()
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -2,6 +2,8 @@ package term
import ( import (
"fmt" "fmt"
"golang.org/x/net/proxy"
"net"
"time" "time"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
@ -49,29 +51,73 @@ func NewSshClient(ip string, port int, username, password, privateKey, passphras
} }
addr := fmt.Sprintf("%s:%d", ip, port) addr := fmt.Sprintf("%s:%d", ip, port)
//
//socks5, err := proxy.SOCKS5("tcp", "",
// &proxy.Auth{User: "username", Password: "password"},
// &net.Dialer{
// Timeout: 30 * time.Second,
// KeepAlive: 30 * time.Second,
// },
//)
//if err != nil {
// return nil, err
//}
//
//conn, err := socks5.Dial("tcp", addr)
//if err != nil {
// return nil, err
//}
//
//clientConn, channels, requests, err := ssh.NewClientConn(conn, addr, config)
//if err != nil {
// return nil, err
//}
//
//ssh.NewClient(clientConn, channels, requests)
return ssh.Dial("tcp", addr, config) return ssh.Dial("tcp", addr, config)
} }
func NewSshClientUseSocks(ip string, port int, username, password, privateKey, passphrase string, socksProxyHost, socksProxyPort, socksProxyUsername, socksProxyPassword string) (*ssh.Client, error) {
var authMethod ssh.AuthMethod
if username == "-" || username == "" {
username = "root"
}
if password == "-" {
password = ""
}
if privateKey == "-" {
privateKey = ""
}
if passphrase == "-" {
passphrase = ""
}
var err error
if privateKey != "" {
var key ssh.Signer
if len(passphrase) > 0 {
key, err = ssh.ParsePrivateKeyWithPassphrase([]byte(privateKey), []byte(passphrase))
if err != nil {
return nil, err
}
} else {
key, err = ssh.ParsePrivateKey([]byte(privateKey))
if err != nil {
return nil, err
}
}
authMethod = ssh.PublicKeys(key)
} else {
authMethod = ssh.Password(password)
}
config := &ssh.ClientConfig{
Timeout: 3 * time.Second,
User: username,
Auth: []ssh.AuthMethod{authMethod},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
socksProxyAddr := fmt.Sprintf("%s:%s", socksProxyHost, socksProxyPort)
socks5, err := proxy.SOCKS5("tcp", socksProxyAddr,
&proxy.Auth{User: socksProxyUsername, Password: socksProxyPassword},
&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
},
)
if err != nil {
return nil, err
}
addr := fmt.Sprintf("%s:%d", ip, port)
conn, err := socks5.Dial("tcp", addr)
if err != nil {
return nil, err
}
clientConn, channels, requests, err := ssh.NewClientConn(conn, addr, config)
if err != nil {
return nil, err
}
return ssh.NewClient(clientConn, channels, requests), nil
}

View File

@ -283,6 +283,7 @@ class Asset extends Component {
asset['use-ssl'] = asset['use-ssl'] === 'true'; asset['use-ssl'] = asset['use-ssl'] === 'true';
asset['ignore-cert'] = asset['ignore-cert'] === 'true'; asset['ignore-cert'] = asset['ignore-cert'] === 'true';
asset['enable-drive'] = asset['enable-drive'] === 'true'; asset['enable-drive'] = asset['enable-drive'] === 'true';
asset['socks-proxy-enable'] = asset['socks-proxy-enable'] === 'true';
this.setState({ this.setState({
modalTitle: title, modalTitle: title,

View File

@ -126,6 +126,7 @@ const AssetModal = function ({title, visible, handleOk, handleCancel, confirmLoa
} }
let [enableDrive, setEnableDrive] = useState(model['enable-drive']); let [enableDrive, setEnableDrive] = useState(model['enable-drive']);
let [socksProxyEnable, setSocksProxyEnable] = useState(model['socks-proxy-enable']);
let [storages, setStorages] = useState([]); let [storages, setStorages] = useState([]);
useEffect(() => { useEffect(() => {
@ -138,7 +139,7 @@ const AssetModal = function ({title, visible, handleOk, handleCancel, confirmLoa
getStorages(); getStorages();
}, []); }, []);
let [accessGateways,setAccessGateways] = useState([]); let [accessGateways, setAccessGateways] = useState([]);
useEffect(() => { useEffect(() => {
const getAccessGateways = async () => { const getAccessGateways = async () => {
const result = await request.get('/access-gateways'); const result = await request.get('/access-gateways');
@ -320,7 +321,7 @@ const AssetModal = function ({title, visible, handleOk, handleCancel, confirmLoa
</Form.Item> </Form.Item>
</Col> </Col>
<Col span={11}> <Col span={11}>
<Collapse defaultActiveKey={['remote-app', '认证', 'VNC中继', 'storage', '模式设置', '显示设置', '控制终端行为']} <Collapse defaultActiveKey={['remote-app', '认证', 'VNC中继', 'storage', '模式设置', '显示设置', '控制终端行为', 'socks']}
ghost> ghost>
{ {
protocol === 'rdp' ? protocol === 'rdp' ?
@ -476,8 +477,47 @@ Windows需要对远程应用程序的名称使用特殊的符号。
</Select> </Select>
</Form.Item> </Form.Item>
</Panel> </Panel>
</> : <>
<Panel header={<Text strong>Socks 代理</Text>} key="socks">
<Form.Item
name="socks-proxy-enable"
label="使用Socks代理"
valuePropName="checked"
>
<Switch checkedChildren="是" unCheckedChildren="否"
onChange={(checked, event) => {
setSocksProxyEnable(checked);
}}/>
</Form.Item>
{
socksProxyEnable ? <>
<Form.Item label="代理地址" name='socks-proxy-host'
rules={[{required: true}]}>
<Input placeholder="Socks 代理的主机地址"/>
</Form.Item>
<Form.Item label="代理端口" name='socks-proxy-port'
rules={[{required: true}]}>
<InputNumber min={1} max={65535}
placeholder='Socks 代理的主机端口'/>
</Form.Item>
<input type='password' hidden={true}
autoComplete='new-password'/>
<Form.Item label="代理账号" name='socks-proxy-username'>
<Input autoComplete="off" placeholder="代理账号,没有可以不填"/>
</Form.Item>
<Form.Item label="代理密码" name='socks-proxy-password'>
<Input.Password autoComplete="off"
placeholder="代理密码,没有可以不填"/>
</Form.Item>
</> : undefined </> : undefined
} }
</Panel>
</>
}
</> : undefined </> : undefined
} }