SSH2.0 编程 ssh 协议进程的实现
之前为了自己做一套SSH,先自己实现了一套telnet。但经过这么多天的苦逼,发现以前的工作都是徒劳。ssh的协议很繁杂,核心的内容在于密码算法,而且自己很难在网上找到周全的细节讲解与详细的实现,只有靠自己刷RFC和问大神还有就是靠强X我的服务器艰难地完成。
现计算了下时间,自己做SSH耗费了进两个月的时间,虽然期间也夹着其他的繁杂事物,但自己在这方面确是是耗费了非常大的精力。因为这方面详细资料的匮乏,自己以前也几乎没有接触过密码学方面的东西,很多只有靠自己摸索,所以我得经常拿我自己的服务器来做黑盒测试,我现在服务器上的ss服务器日志全是一些非法连接的记录(—_—|||)。早知当初就不那么作死非要自己实现他的加密算法和过程,用openssl就很快搞定了。但我还是觉得这次做SSH的精力是我受益匪浅,不仅熟悉了各种加密,并且能靠自己实现并熟练应用了。可能这些对自己帮助不大,但至少和信安的小伙伴也有点吹牛的谈资了~
这篇文章希望能帮助到想了解ssh2.0协议或是亲手实现ssh协议的小伙伴。
首先对数据包的格式进行说明:
数据包由包长度(Packet Length)、填充长读(Padding Length)、信息代码(Msg code)、信息内容与填充值(Padding String) 这5部分组成。信息内容中的一些字符串以4字节长度+该长度数量的字符组成,数值按照网络序排列,例如:abc: 00 00 00 03 (char)a (char)b (char)c 。另外有一种大整数的情况,负数和字符串的表示方式一样,正数需要前导0,例如 4b64: 00 00 00 03 00 4b 64 。
ssh头的结构体:
struct sshhead { unsigned int tlen; unsigned char plen; unsigned char msgcode; sshhead(){tlen=6;} };
就拿通过ssh远程控制的一个完整个过程来讲,ssh的过程可分为以下3部分:
一、版本协商
二、算法协商与密钥交换
三、加密通信(可能含有2、3部分)
这其中第二部分是ssh最为核心的过程,该过程决定了以后通信所要使用的密钥,下面按顺序对每个部分对比着数据包进行详细的讲解并给出实现的过程。
一、版本协商:
在建立连接后,客户端与服务器分别向对方发送自己ssh的版本信息(这里的数据格式不同于其他包,只有一行版本号),以\r\n结束。版本的格式如下:
SSH-ssh协议版本-详细版本\r\n (几乎只有ssh协议版本之前的信息有效)
比如我linux上的就是:SSH-2.0-OpenSSH_5.3\r\n
Putty的是: SSH-2.0-PuTTY_Release_0.63\r\n
一般来说,在建立连接后,是先由服务器发版本号过来,单线程处理版本协商的朋友需要注意下。
在双方收到对方发来的版本号后,会根据两者之中最小的版本来进行接下来的通讯。
二、算法协商与秘钥交换:
这部分的内容将会占该文章总篇幅的一半以上。
首先给大家看下整个过程的数据包大概:
整个部分是从第6条开始到第15条结束,除去中间的非协议部分,总共有7条数据包。看起来只有这么几条数据包,但其中包含了非常多的过程与隐秘的信息。
1、算法协商:
位第6、9数据,分别为双发向对法发送的自己在不同密码需求上支持的算法。
该数据包的格式:
按顺序分别是:
cookie(随机的值,16byte)
kex_algorithms(秘钥租交换算法)
server_host_key_algorithms(服务器主机秘钥,正常情况用处不大,甚至可以不用)
encryption_algorithms_client_to_server(两端通信使用的加密算法)
encryption_algorithms_server_to_client
mac_algorithms_client_to_server(数据校验用的hash算法)
mac_algorithms_server_to_client
compression_algorithms_client_to_server(压缩算法)
compression_algorithms_server_to_client
languages_client_to_server
languages_server_to_client
first_kex_packet_follows
0(4byte整数,扩展用的)
每个算法类型可能会有多个不同的算法,这些算法之间使用逗号隔开。
现在双方知道对方支持的算法,但是应该怎样决定每个类型实际所使用的算法呢?
每个算法类型列表的第一个算法必须是首选的算法,服务器应以客户端的算法优先级作为考虑,就拿交换算法举例:
现在服务器有三个算法dh1,dh2,dh3
客户端有两个算法dh3,dh2
那么服务器的首选算法是dh1,而客户端是dh3,客户端此时知道服务器有dh3算法,因此客户端就确认使用dh3算法。服务器发现自己的首选算法与客户端不同,而自己拥有客户端的首选算法,因此服务器也确认使用dh3算法。
再看另一个情况
服务器:dh1,dh2,dh3
客户端:dh4,dh3,dh1
这时服务器没有客户端的首选算法,客户端会使用第二个算法dh3,此时服务器也支持第二个算法,双方将确定使用dh3算法。
如果服务器和客户端双方没有共同的算法,这次会话将会终止。
下面是代码实现和服务器之间的版本协商
#define KEI (char)20 #define NK (char)21 //算法名 #define VER "SSH-2.0-WCHRT_1.0\r\n" #define COOKIE "0123456789ABCDEF" #define VKEX "diffie-hellman-group-exchange-sha256" #define VSHK "ssh-rsa" #define VECS "aes128-cbc" #define VESC "aes128-cbc" #define VMCS "hmac-sha1" #define VMSC "hmac-sha1" #define VCCS "none" #define VCSC "none" #define VLCS "" #define VLSC "" #define KFPF ""
bool key_exchange() { sshhead sshh; sshh.msgcode=KEI; sshh.tlen-=4; //将算法列表与信息分别写入缓冲区 mstrin(COOKIE,sshh.tlen); mstrin(VKEX,sshh.tlen); mstrin(VSHK,sshh.tlen); mstrin(VECS,sshh.tlen); mstrin(VESC,sshh.tlen); mstrin(VMCS,sshh.tlen); mstrin(VMSC,sshh.tlen); mstrin(VCCS,sshh.tlen); mstrin(VCSC,sshh.tlen); mstrin(VLCS,sshh.tlen); mstrin(VLSC,sshh.tlen); //ed for(int i=0;i<5;sshh.tlen++,i++) { data[sshh.tlen]=(char)0; } //载荷的计算与总长度的写入都放在最后 //count padding length count_padding(sshh); sshheadin(sshh); //没有封装socket len=send(sock,data,sshh.tlen+4,0); mrecv(10); //printf("(%d)",len); if(data[5]==KEI) { return true; } return false; }
用到的一些功能函数:
//向缓冲区填充字符串,长度使用网络字节序 void mstrin(string s,unsigned int &tlen) { data[tlen]=(char)(s.length()/(256*256*256)); data[tlen+1]=(char)(s.length()/(256*256)); data[tlen+2]=(char)(s.length()/256); data[tlen+3]=(char)(s.length()); tlen+=4; for(int i=0;i<s.length();tlen++,i++) { data[tlen]=s[i]; } data[tlen]='\0'; } void sshheadin(sshhead &sshh) { sshh.tlen-=4; data[0]=(char)(sshh.tlen/(256*256*256)); data[1]=(char)(sshh.tlen/(256*256)); data[2]=(char)(sshh.tlen/256); data[3]=(char)(sshh.tlen); data[4]=(char)sshh.plen; data[5]=sshh.msgcode; } void count_padding(sshhead &sshh) { int k=2; if(sshh.tlen%8<4) { k=1; } sshh.plen=(sshh.tlen/8+k)*8-sshh.tlen; sshh.tlen=(sshh.tlen/8+k)*8; }
2、秘钥交换
在算法协商成功过后,双方便立马进行秘钥组的交换。ssh2.0版本所使用的秘钥组交换协议算法主要使用diffie-hellman-group-exchange-sha算法。
鉴于该部分内容特别多,我特意在另一篇单独的文章中予以详细介绍,再阅读下文前请先参考该文章:dh-gex-sha算法详解
我们数据包的第10到15条都是该部分的内容
1、dh key exchange init (C)
密钥交换初始化,由客户端先向服务器发送秘钥交换请求的数据包,告知开始秘钥交换。
//<<<<<<<<<<DH KEX INIT<<<<<<<<< sshh.tlen=6; sshh.msgcode=DHKEI; //payload mintin(0x1000,sshh.tlen); count_padding(sshh); sshheadin(sshh); len=send(sock,data,sshh.tlen+4,0); //dh: set I_C dhdata.set_i_c(string(data+4,sshh.tlen)); //dhdata.set_i_c(string(data,len)); //>>>>>>>>>>>>>>>>>>>>>>>>
2、dh key exchange reply (S)
服务器收到客户端发起交换的请求后,将自己用于dh算法的P、G发送给客户端,用于客户端生成dh公私钥。这里的P是一个大素数,而G是大于1的数,G不必过大,10位以内最后,因为按幂运算G能轻易生成特别大的数。
//<<<<<<<<<<DH KEX REPLAY<<<<<<<<< mrecv(10); if(data[5]!=DHKER) { puts("DH KEX REPLAY error"); return false; } //dh: set I_S dhdata.set_i_s(string(data+4,len-4)); //dhdata.set_i_s(string(data,len)); //dh: read P pos=6; intlen=readstrint(data+pos); pos+=4; Integer p=readstrbigint(data+pos,intlen); pos+=intlen; //dh: read G intlen=readstrint(data+pos); pos+=4; Integer g=readstrbigint(data+pos,intlen); pos+=intlen; //dh: set G and P dhdata.set_g_and_p(g,p); //cout<<dhdata.dh_p<<" "<<dhdata.dh_g<<endl; //>>>>>>>>>>>>>>>>>>>>>>>>
3、dh gex init (C)
客户端收到服务器发过来的P、G后,自己变成根据P、G生成并计算出自己的公钥e。这一步也只需要客户端将生成的e发送给服务器即可。
//<<<<<<<<<<DH GEX INIT<<<<<<<<< sshh.tlen=6; sshh.msgcode=DHGI; dhdata.comp_e(); string e=inttostr(dhdata.get_e(),256); mstrin(e,sshh.tlen); count_padding(sshh); sshheadin(sshh); len=send(sock,data,sshh.tlen+4,0); //debugstr(data,len); //>>>>>>>>>>>>>>>>>>>>>>>>
4、dhgex reply (S)
重要的来了,服务器收到了客户端发来的e后,便能计算出共享秘钥K,并根据现有信息计算出生成所需秘钥的H。
这个数据包里面含有如下信息:
KEX DH host key(K_S):
主机公钥,一般为rsa公钥。完整的格式为:总长度+算法名长度+算法名+证书(n)长度+证书(n)+公钥长度+公钥。
DH server f :
服务器的dh公钥值,客户端收到后便能用f计算出同样的共享秘钥K。
KEX DH H signature (签名后的H):
服务器用主机私钥对计算出的hash值H进行签名的结果。格式为:总长度+算法名长度+算法名+签名数据长度+签名值。
H的计算方法: H=hash(V_C||V_S||I_C||I_S||K_S||e||f||K);
按顺序用到的值(注意类型):
类型 | 值 | 说明 |
string | V_C | 客户端的初始报文(版本信息:SSH-2.0-xxx,不含结尾的CR和LF) |
string | V_S | 服务器的初始报文 |
string | I_C | 客户端 SSH_MSG_KEX_INIT的有效载荷(不含开头的数据长度值) |
string | I_S | 服务器的同上 |
string | K_S | 主机秘钥(dh gex reply(33)过程服务器发送host key (RSA公钥)) |
mpint | e | 客户端DH公钥 |
mpint | f | 服务器DH公钥 |
mpint | K | 共同DH计算结果 |
//<<<<<<<<<<DH GEX REPLAY<<<<<<<<< mrecv(10); if(data[5]!=DHGR) { puts("DH GEX REPLAY error"); system("pause"); return false; } int padlen=data[4]; //dh: set server host key pos=6; intlen=readstrint(data+pos);//host key all length dhdata.set_k_s(string(data+pos+4,intlen)); pos+=4; intlen=readstrint(data+pos);//host key name pos+=4; pos+=intlen; intlen=readstrint(data+pos);//get rsa e and n pos+=4; Integer ee=readstrbigint(data+pos,intlen); pos+=intlen; intlen=readstrint(data+pos); pos+=4; Integer nn=readstrbigint(data+pos,intlen);//set rsa e and n pos+=intlen; dhdata.set_e_and_n(ee,nn); //dh: set dh server f intlen=readstrint(data+pos); pos+=4; dhdata.set_f(readstrbigint(data+pos,intlen)); pos+=intlen; //dh: set shka_name pos+=4;//h's total length intlen=readstrint(data+pos); pos+=4; dhdata.set_shka_name(string(data+pos,intlen)); pos+=intlen; //dh: set server h intlen=readstrint(data+pos); pos+=4; dhdata.set_s_h(string(data+pos,intlen)); pos+=intlen; pos+=padlen; //and other MAC// dhdata.comp_k(); dhdata.comp_h();
5、new keys (C)
客户端收到服务器的信息后计算出K,并用同样的方式计算出H(服务器和客户端的H都是同一个值)。并使用服务器发过来的K_S验证服务器发过来的签名后的H,如果验证一致,则说明此次秘钥交换成功。客户端向服务器发送new key,标志秘钥交换过程的结束。如果此次秘钥交换是整个会话的第一次交换,则计算出的H也是整个会话的会话ID(session_id)。
秘钥基本信息在网络上的传输与交换,接下来就分别是服务器和客户端各自使用现有信息计算出以后加解密所要使用的秘钥。秘钥计算:
这里的加密秘钥指的是以后数据通信所用的秘钥,一般用aes算法。
计算方式:hash(K,H,单个字符,session_id);
单个字符指的是单个大写的ASCII字母,根据不同的加密秘钥选择不同的字符来计算。
字母 | 秘钥 |
'A' | 客户端到服务器的初始IV(CBC) |
'B' | 服务器到客户端的初始IV |
'C' | 客户端到服务器的加密秘钥(对称秘钥) |
'D' | 服务器到客户端的加密秘钥 |
'E' | 客户端到服务器的完整性秘钥(HMAC) |
'F' | 服务器到客户端的完整性秘钥 |
就以aes-cbc为例子,aes对称加解密所需要用到的值有初始IV与对称秘钥。这里的初始IV指的是cbc模式中加解密的初始向量,第二次加解密需要IV的值,以后的每次的加解密都要依赖于上一次加解密的数据。
三、加密通信
此时双方都拥有协商好的算法以及用于加解密的秘钥,现在开始所有传输的全部数据都要进行加密(包含总长度),并使用同样的。
在加密通信的过程中,双方允许重新发送KEX秘钥交换请求。这时整个秘钥交换过程的数据将会使用现有密钥加解密。在该次秘钥交换的过程中也会生成一个H值,但该H值不会影响到此次会话的session_id,session_id只是会话第一次秘钥交换生成的H值。在秘钥交换最后客户端发出new keys请求时。双方会放弃当前使用的秘钥,使用新协商的秘钥继续通信。
在远程数据的通信过程中,双方使用SSH_MSG_CHANNEL_DATA标志消息类型进行数据传输。
在秘钥交换完成后第一次对发送数据加密时,首先需要对AES向量进行初始化,即设置对应的IV。aes部分我使用的是CRYPTOPP的aes-cbc算法(在后文的有对该算法的封装)。
en_c_to_s.set_iv(dhdata.comp_encry_key(IVCSF,32)); en_c_to_s.set_k(dhdata.comp_encry_key(ECSF,32)); en_c_to_s.init(); de_s_to_c.set_iv(dhdata.comp_encry_key(IVSCF,32)); de_s_to_c.set_k(dhdata.comp_encry_key(ESCF,32)); de_s_to_c.init();
整个协议用到的主要加密算法的实现与封装:
//mycrypt.h #ifndef _MYCRYPT_H__ #define _MYCRYPT_H__ #include<cstring> #include<string> #include <iostream> #include<cmath> #include "integer.h" #include "files.h" #include "hex.h" #include "sha.h" #include "modes.h" #include "osrng.h" using namespace std; using namespace CryptoPP; Integer mkrandomnum(int len); string inttostr(Integer num,unsigned int radix); string inttostrnum(Integer num,unsigned int radix); //大整数转mpint string inttompint(Integer num,unsigned int radix); string strtostrnum(string s); //大整数快速幂运算 Integer fastpower_comp(Integer a,Integer b,Integer c); //sha算法封装 class m_sha { public: string encode_sha1(string data); string encode_sha256(string data); }; //dh算法实现 class m_dh { public: Integer dh_g,dh_p,dh_x,dh_e; Integer dh_y,dh_f; Integer dh_k; void set_g_and_p(const Integer g,const Integer p) { dh_g=g; dh_p=p; } void set_y(Integer y) { dh_y=y; } void set_f(Integer f) { dh_f=f; } void comp_e(); Integer get_e() { return dh_e; } void comp_k(); Integer get_k() { return dh_k; } }; //rsa算法实现 class m_rsa { public: Integer rsa_e; Integer rsa_n; void set_e_and_n(Integer e,Integer n) { rsa_e=e; rsa_n=n; } Integer comp_rsa_result(Integer num); }; //dh gex协议算法实现 class m_dh_gex_sha:public m_dh,public m_sha,public m_rsa { public: string v_c,v_s; string i_c,i_s; string k_s; string dh_h,s_h; string shka_name; void set_v_c(string x) { v_c=x; } void set_v_s(string x) { v_s=x; } void set_i_c(string x) { i_c=x; } void set_i_s(string x) { i_s=x; } void set_k_s(string x) { k_s=x; } void set_s_h(string x) { s_h=x; } void set_shka_name(string x) { shka_name=x; } void comp_h(); string get_h() { return dh_h; } string comp_encry_key(char c,const int len); }; //aes-cbc算法封装 class m_aes_cbc { public: string aes_k; string aes_iv; void set_k(string x) { aes_k=x; } void set_iv(string x) { aes_iv=x; } CBC_Mode<AES>::Encryption *aes_Encryptor; CBC_Mode<AES>::Decryption aes_Decryptor; void init(); string encode(string data); string decode(string data); }; #endif //_MYCRYPT_H__
//mycrypt.cpp #include "mycrypt.h" Integer mkrandomnum(int len) { Integer re=0; for(int i=0;i<len;i++) { re*=10; re+=abs(rand())%10; } return re; } string inttostr(Integer num,unsigned int radix) { string s1=""; unsigned int k; while(num>0) { k=num%radix; num/=radix; //cout<<num<<" "; //printf("%d\n",k); s1+=(char)k; } string s2=""; for(int i=s1.length()-1;i>=0;i--) { s2+=s1[i]; } return s2; } string inttostrnum(Integer num,unsigned int radix) { string s1=""; unsigned int k; while(num>0) { k=num%radix; num/=radix; if(k<10) { s1+='0'+k; } else { s1+='a'+k-10; } } string s2=""; for(int i=s1.length()-1;i>=0;i--) { s2+=s1[i]; } return s2; } string inttompint(Integer num,unsigned int radix) { string s=""; s+=(char)0; s+=inttostr(num,radix); int len=s.length(); string k=""; while(len>0) { k+=(char)(len%256); len/=256; } while(k.length()<4) { k+=(char)0; } string re; re+=k[3]; re+=k[2]; re+=k[1]; re+=k[0]; re+=s; return re; } string strtostrnum(string s) { string re=""; int k,p; for(int i=0;i<s.length();i++) { p=s[i]; if(p<0) { p=(256+s[i]); } k=p/16; for(int j=0;j<2;j++) { // if(k<10) { re+=('0'+k); } else { re+=('a'+k-10); } k=p%16; } } return re; } int intlength(Integer num) { int re=0; while(num>0) { num/=10; re++; } return re; } Integer fastpower_comp(Integer a,Integer b,Integer c) { /*unused fast power Integer re=1; for(int i=0;i<b;i++) { re*=a; re%=c; } return re; */ //fast power Integer n=c; c=1; while(b!=0) { if(b%2!=0) { b=b-1; c=(c*a)%n; } else { b=b/2; a=(a*a)%n; } } return c; } void m_dh::comp_e() { dh_x=mkrandomnum(50)+1; dh_e=fastpower_comp(dh_g,dh_x,dh_p); /* cout<<"//"<<endl; cout<<dh_g<<endl; cout<<dh_x<<endl; cout<<dh_p<<endl; cout<<dh_e<<endl; cout<<"//"<<endl; */ } void m_dh::comp_k() { dh_k=fastpower_comp(dh_f,dh_x,dh_p); } Integer m_rsa::comp_rsa_result(Integer num) { return fastpower_comp(num,rsa_e,rsa_n); } void m_dh_gex_sha::comp_h() { string data=""; /* data+=strtostrnum(v_c); data+=strtostrnum(v_s); data+=strtostrnum(i_c); data+=strtostrnum(i_s); data+=strtostrnum(k_s); data+=inttostrnum(dh_e,16); data+=inttostrnum(dh_f,16); data+=inttostrnum(dh_k,16); */ data+=v_c; data+=v_s; data+=i_c; data+=i_s; data+=k_s; data+=inttompint(dh_e,16); data+=inttompint(dh_f,16); data+=inttompint(dh_k,16); //cout<<endl<<"|"<<data<<"||"<<endl; dh_h=encode_sha256(data); } string m_dh_gex_sha::comp_encry_key(char c,const int len) { string re; string data=""; data+=inttompint(dh_k,16); data+=dh_h; data+=c; data+=dh_h; re=encode_sha256(data); while(re.length()<len) { data=inttompint(dh_k,16); data+=dh_h; data+=re; re+=encode_sha256(data); } while(re.length()>len) { re.pop_back(); } return re; } string m_sha::encode_sha1(string data) { string hash; SHA1 sha1; HashFilter hash_filter (sha1); hash_filter.Attach(new HexEncoder(new StringSink(hash), false)); hash_filter.Put((byte *)data.c_str(),data.length()); hash_filter.MessageEnd(); return hash; } string m_sha::encode_sha256(string data) { string hash; SHA256 sha256; HashFilter hash_filter (sha256); hash_filter.Attach(new HexEncoder(new StringSink(hash), false)); hash_filter.Put((byte *)data.c_str(),data.length()); hash_filter.MessageEnd(); return hash; } void m_aes_cbc::init() { aes_Encryptor=new CBC_Mode<AES>::Encryption((unsigned char *)aes_k.c_str(), aes_k.length(), (unsigned char *)aes_iv.c_str()); aes_Decryptor=new CBC_Mode<AES>::Decryption ((unsigned char *)aes_k.c_str(), aes_k.length(), (unsigned char *)aes_iv.c_str()); } string m_aes_cbc::encode(string data) { string re; StringSource(data, true, new StreamTransformationFilter(*aes_Encryptor, new StringSink(re), BlockPaddingSchemeDef::BlockPaddingScheme::ONE_AND_ZEROS_PADDING, true) ); return re; } string m_aes_cbc::decode(string data) { string re; StringSource(data, true, new StreamTransformationFilter(*aes_Decryptor, new StringSink(re), BlockPaddingSchemeDef::BlockPaddingScheme::ONE_AND_ZEROS_PADDING, true) ); return re; }
ssh的实现就到此终于结束了,截图留念。
笔者在之初就想使用crypto++来帮助实现ssh过程的密码算法。而刚接触这东西完全搞不懂怎么用,什么编码器、生成器、过滤器、sink...这些概念根本就不懂,网上的使用文档直接就拿这一堆概念加上一堆组合出来的代码来实现一个加密算,没有什么密码学知识,想要快速掌握crypto++几乎是不可能的,当时研究了很久就只是会使用它的hash加密。而后自己硬着头皮实现了整个dh-gex,到后面aes后,发现自己能很自然得理解crypto++的用法了,便自己封装了crypto++的aes算法供使用。
总之都是好事,以后遇到其他的基于ssl的协议与应用就应能很轻松地理解与实现了。
下一篇: Linux 中的 SSH 远程访问和控制
推荐阅读
-
C#编程:构建QUIC协议的客户端与服务器端实现
-
【2022新手指南】Java编程进阶之路 - 六、技术架构篇 ### MySQL索引底层解析与优化实战 - 你会讲解MySQL索引的数据结构吗?性能调优技巧知多少? - Redis深度揭秘:你知道多少?从基础到哨兵、主从复制全梳理 - Redis持久化及哨兵模式详解,还有集群搭建和Leader选举黑箱打开 - Zookeeper是个啥?特性和应用场景大公开 - ZooKeeper集群搭建攻略及 Leader选举、读写一致性、共享锁实现细节 - 探究ZooKeeper中的Leader选举机制及其在分布式环境中的作用 - Zab协议深入剖析:原理、功能与在Zookeeper中的核心地位 - RabbitMQ全方位解读:工作模式、消费限流、可靠投递与配置策略 - 设计者视角:RabbitMQ过期时间、死信队列与延时队列实践指南 - RocketMQ特性和应用场景揭示:理解其精髓与差异化优势 - Kafka详细介绍:特性及广泛应用于实时数据处理的场景解析 - ElasticSearch实力揭秘:特性概述与作为搜索引擎的广泛应用 - MongoDB认知升级:非关系型数据库的优势阐述,安装与使用实战教学 - BIO/NIO/AIO网络模型对比:掌握它们的区别与在网络编程中的实际应用 - Netty带你飞:理解其超快速度背后的秘密,包括线程模型分析 - 网络通信黑科技:Netty编解码原理与常用编解码器的应用,Protostuff实战演示 - 解密Netty粘包与拆包现象,怎样有效应对这一常见问题 - 自定义Netty心跳检测机制,轻松调整检测间隔时间的艺术 - Dubbo轻骑兵介绍:核心特性概览,服务降级实战与其实现益处 - Dubbo三大神器解读:本地存根与本地伪装的实战运用与优势呈现 ----------------------- 七、结语与回顾
-
如何通过Haproxy实现HTTP、SSH和MSTSC协议的共享连接管理
-
go语言Socket编程-Socket编程 什么是Socket Socket,英文含义是插座、插孔,一般称之为套接字,用于描述IP地址和端口。可以实现不同程序间的数据通信。 Socket起源于Unix,而Unix基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。Socket就是该模式的一个实现,网络的Socket数据传输是一种特殊的I/O,Socket也是一种文件描述符。Socket也具有一个类似于打开文件的函数调用:Socket,该函数返回一个整型的Socket描述符,随后的连接建立、数据传输等操作都是通过该Socket实现的。 套接字的内核实现较为复杂,不宜在学习初期深入学习,了解到如下结构足矣。 套接字通讯原理示意 在TCP/IP协议中,“IP地址+TCP或UDP端口号”唯一标识网络通讯中的一个进程。“IP地址+端口号”就对应一个socket。欲建立连接的两个进程各自有一个socket来标识,那么这两个socket组成的socket pair就唯一标识一个连接。因此可以用Socket来描述网络连接的一对一关系。 常用的Socket类型有两种:流式Socket(SOCK_STREAM)和数据报式Socket(SOCK_DGRAM)。流式是一种面向连接的Socket,针对于面向连接的TCP服务应用;数据报式Socket是一种无连接的Socket,对应于无连接的UDP服务应用。 网络应用程序设计模式 C/S模式 传统的网络应用设计模式,客户机(client)/服务器(server)模式。需要在通讯两端各自部署客户机和服务器来完成数据通信。 B/S模式 浏览器(Browser)/服务器(Server)模式。只需在一端部署服务器,而另外一端使用每台PC都默认配置的浏览器即可完成数据的传输。 优缺点 对于C/S模式来说,其优点明显。客户端位于目标主机上可以保证性能,将数据缓存至客户端本地,从而提高数据传输效率。且,一般来说客户端和服务器程序由一个开发团队创作,所以他们之间所采用的协议相对灵活。可以在标准协议的基础上根据需求裁剪及定制。例如,腾讯所采用的通信协议,即为ftp协议的修改剪裁版。 因此,传统的网络应用程序及较大型的网络应用程序都首选C/S模式进行开发。如,知名的网络游戏魔兽世界。3D画面,数据量庞大,使用C/S模式可以提前在本地进行大量数据的缓存处理,从而提高观感。 C/S模式的缺点也较突出。由于客户端和服务器都需要有一个开发团队来完成开发。工作量将成倍提升,开发周期较长。另外,从用户角度出发,需要将客户端安插至用户主机上,对用户主机的安全性构成威胁。这也是很多用户不愿使用C/S模式应用程序的重要原因。 B/S模式相比C/S模式而言,由于它没有独立的客户端,使用标准浏览器作为客户端,其工作开发量较小。只需开发服务器端即可。另外由于其采用浏览器显示数据,因此移植性非常好,不受平台限制。如早期的偷菜游戏,在各个平台上都可以完美运行。 B/S模式的缺点也较明显。由于使用第三方浏览器,因此网络应用支持受限。另外,没有客户端放到对方主机上,缓存数据不尽如人意,从而传输数据量受到限制。应用的观感大打折扣。第三,必须与浏览器一样,采用标准http协议进行通信,协议选择不灵活。 因此在开发过程中,模式的选择由上述各自的特点决定。根据实际需求选择应用程序设计模式。 简单的C/S模型通信 Server端:Listen函数 func Listen(network, address string) (Listener, error) network:选用的协议:TCP、UDP, 如:“tcp”或 “udp” address:IP地址+端口号, 如:“127.0.0.1:8000”或 “:8000” Listener 接口: type Listener interface { Accept (Conn, error) Close error Addr Addr } Conn 接口: type Conn interface { Read(b byte) (n int, err error) Write(b byte) (n int, err error) Close error LocalAddr Addr RemoteAddr Addr SetDeadline(t time.Time) error SetReadDeadline(t time.Time) error SetWriteDeadline(t time.Time) error } 参看 [<u>https://studygolang.com/pkgdoc</u>](https://studygolang.com/pkgdoc) 中文帮助文档中的demo: 示例代码:TCP服务器.go package main import ( "net" "fmt" ) func main { // 创建监听 listener, err:= net.Listen("tcp", ":8000") if err != nil { fmt.Println("listen err:", err) return } defer listener.Close // 主协程结束时,关闭listener fmt.Println("服务器等待客户端建立连接...") // 等待客户端连接请求 conn, err := listener.Accept if err != nil { fmt.Println("accept err:", err) return } defer conn.Close // 使用结束,断开与客户端链接 fmt.Println("客户端与服务器连接建立成功...") // 接收客户端数据 buf := make(byte, 1024) // 创建1024大小的缓冲区,用于read n, err := conn.Read(buf) if err != nil { fmt.Println("read err:", err) return } fmt.Println("服务器读到:", string(buf[:n])) // 读多少,打印多少。 }
-
SSH2.0 编程 ssh 协议进程的实现
-
ssh工作流程及原理-SSH(Secure Shell Protocol,安全的壳程序协议),它可以通过数据包加密技术将等待传输的数据包加密后再传输到网络上。ssh协议本身提供两个服务器功能:一个是类似telnet的远程连接使用shell的服务器;另一个就是类似ftp服务的sftp-server,提供更安全的ftp服务。 连接加密技术简介 目前常见的网络数据包加密技术通常是通过“非对称密钥系统”来处理的。主要通过两把不一样的公钥与私钥来进行加密与解密的过程。 公钥(public key):提供给远程主机进行数据加密的行为,所有人都可获得你的公钥来将数据加密。 私钥(private key):远程主机使用你的公钥加密的数据,在本地端就能够使用私钥来进行解密。私钥只有自己拥有。 SSH工作过程:在整个通讯过程中,为实现SSH的安全连接,服务端与客户端要经历如下五个阶段: 版本号协商阶段 SSH目前包括SSH1和SSH2两个版本,双方通过版本协商确定使用的版本 密钥和算法协商阶段 SSH支持多种加密算法,双方根据本端和对端支持的算法,协商出最终使用的算法 认证阶段 SSH客户端向服务器端发起认证请求,服务器端对客户端进行认证 会话请求阶段 认证通过后,客户端向服务器端发送会话请求 交互会话阶段 会话请求通过后,服务器端和客户端进行信息的交互 一、版本协商阶段 服务器端打开端口22,等待客户端连接; 客户端向服务器端发起TCP初始连接请求,TCP连接建立后,服务器向客户端发送第一个报文,包括版本标志字符串,格式为“SSH-<主协议版本号>.<次协议版本号>.<软件版本号>”,协议版本号由主版本号和次版本号组成,软件版本号主要是为调试使用。 客户端收到报文后,解析该数据包,如果服务器的协议版本号比自己的低,且客户端能支持服务器端的低版本,就使用服务器端的低版本协议号,否则使用自己的协议版本号。 客户端回应服务器一个报文,包含了客户端决定使用的协议版本号。服务器比较客户端发来的版本号,决定是否能同客户端一起工作。如果协商成功,则进入密钥和算法协商阶段,否则服务器断开TCP连接。 说明:上述报文都是采用明文方式传输。 二、密钥和算法协商阶段 服务器端和客户端分别发送算法协商报文给对端,报文中包含自己支持的公钥算法列表、加密算法列表、MAC(Message Authentication Code,消息验证码)算法列表、压缩算法列表等等。 服务器端和客户端根据对端和本端支持的算法列表得出最终使用的算法。 服务器端和客户端利用DH交换(Diffie-Hellman Exchange)算法、主机密钥对等参数,生成会话密钥和会话ID。 由此,服务器端和客户端就取得了相同的会话密钥和会话ID。对于后续传输的数据,两端都会使用会话密钥进行加密和解密,保证了数据传送的安全。在认证阶段,两端会使用会话用于认证过程。 会话密钥的生成: 客户端需要使用适当的客户端程序来请求连接服务器,服务器将服务器的公钥发送给客户端。(服务器的公钥产生过程:服务器每次启动sshd服务时,该服务会主动去找/etc/ssh/ssh_host*文件,若系统刚装完,由于没有这些公钥文件,因此sshd会主动去计算出这些需要的公钥文件,同时也会计算出服务器自己所需要的私钥文件。) 服务器生成会话ID,并将会话ID发给客户端。 若客户端第一次连接到此服务器,则会将服务器的公钥数据记录到客户端的用户主目录内的~/.ssh/known_hosts。若是已经记录过该服务器的公钥数据,则客户端会去比对此次接收到的与之前的记录是否有差异。客户端生成会话密钥,并用服务器的公钥加密后,发送给服务器。 ****服务器用自己的私钥将收到的数据解密,获得会话密钥。 服务器和客户端都知道了会话密钥,以后的传输都将被会话密钥加密。 三、认证阶段 SSH提供两种认证方法: 基于口令的认证(password认证):客户端向服务器发出password认证请求,将用户名和密码加密后发送给服务器,服务器将该信息解密后得到用户名和密码的明文,与设备上保存的用户名和密码进行比较,并返回认证成功或失败消息。 基于密钥的认证(publickey认证):客户端产生一对公共密钥,将公钥保存到将要登录的服务器上的那个账号的家目录的.ssh/authorized_keys文件中。认证阶段:客户端首先将公钥传给服务器端。服务器端收到公钥后会与本地该账号家目录下的authorized_keys中的公钥进行对比,如果不相同,则认证失败;否则服务端生成一段随机字符串,并先后用客户端公钥和会话密钥对其加密,发送给客户端。客户端收到后将解密后的随机字符串用会话密钥发送给服务器。如果发回的字符串与服务器端之前生成的一样,则认证通过,否则,认证失败。 注:服务器端对客户端进行认证,如果认证失败,则向客户端发送认证失败消息,其中包含可以再次认证的方法列表。客户端从认证方法列表中选取一种认证方法再次进行认证,该过程反复进行。直到认证成功或者认证次数达到上限,服务器关闭连接为止。实例
-
SSH 协议详解:安全远程访问的守护者(C/C++ 代码实现)
-
SSH2.0 编程 ssh 协议进程实施(转)
-
Dropbear - SSH 协议的另一种开放源代码实现方式
-
Python 的 Crip(并发、并发的收益率实现、收益率来自、并发的绿色子实现、异步编程、线程和进程、并发比较、简要总结)