I've working in a little project to automate the creation of SSH tunnels, so that we don't have to launch a Putty session with all the right tunnels. Lazy? perhaps, but there is nothing like laziness to spur novel solutions, not that this is one of these situations.
At any rate, back to the problem. With a multitude of tunnels and potentially SSH servers on the other side of the NAT divide, I wanted a configuration section like this:
1 <TunnelSection>
2 <host SSHServerHostname="tsg" username="user" SSHport="22" password="" privatekey="" privatekeypassphrase="">
3 <tunnels>
4 <tunnel name="tfs" localport="8081" remoteport="8080" destinationserver="tfs.dev.com" />
5 <tunnel name="sql" localport="14331" remoteport="1433" destinationserver="sql2008.dev.com" />
6 <tunnel name="crmapp" localport="81" remoteport="80" destinationserver="crmapp.dev.com" />
7 </tunnels>
8 </host>
9 <host SSHServerHostname="blade16" username="root" SSHport="22" password="" privatekey="" privatekeypassphrase="">
10 <tunnels>
11 <tunnel name="vnc" localport="5902" remoteport="5902" destinationserver="blade1.dev.com" />
12 </tunnels>
13 </host>
14 </TunnelSection>
The
example in the microsoft website does not quite do what I wanted to do, although, I suspect that I might have been able to twist it to get it working. At any rate, here is the code:
1 using System;
2 using System.Configuration;
3
4
5 namespace SSHTunnelWF
6 {
7
8
9 public class TunnelSection : ConfigurationSection
10 {
11
12 [ConfigurationProperty("", IsDefaultCollection = true)]
13 public HostCollection Tunnels
14 {
15 get
16 {
17 HostCollection hostCollection = (HostCollection)base[""];
18
19 return hostCollection;
20
21 }
22 }
23
24 }
25
26
27 public class HostCollection : ConfigurationElementCollection
28 {
29 public HostCollection()
30 {
31 HostConfigElement details = (HostConfigElement)CreateNewElement();
32 if (details.SSHServerHostname != "")
33 {
34 Add(details);
35 }
36 }
37
38 public override ConfigurationElementCollectionType CollectionType
39 {
40 get
41 {
42 return ConfigurationElementCollectionType.BasicMap;
43 }
44 }
45
46 protected override ConfigurationElement CreateNewElement()
47 {
48 return new HostConfigElement();
49 }
50
51 protected override Object GetElementKey(ConfigurationElement element)
52 {
53 return ((HostConfigElement)element).SSHServerHostname;
54 }
55
56 public HostConfigElement this[int index]
57 {
58 get
59 {
60 return (HostConfigElement)BaseGet(index);
61 }
62 set
63 {
64 if (BaseGet(index) != null)
65 {
66 BaseRemoveAt(index);
67 }
68 BaseAdd(index, value);
69 }
70 }
71
72 new public HostConfigElement this[string name]
73 {
74 get
75 {
76 return (HostConfigElement)BaseGet(name);
77 }
78 }
79
80 public int IndexOf(HostConfigElement details)
81 {
82 return BaseIndexOf(details);
83 }
84
85 public void Add(HostConfigElement details)
86 {
87 BaseAdd(details);
88 }
89 protected override void BaseAdd(ConfigurationElement element)
90 {
91 BaseAdd(element, false);
92 }
93
94 public void Remove(HostConfigElement details)
95 {
96 if (BaseIndexOf(details) >= 0)
97 BaseRemove(details.SSHServerHostname);
98 }
99
100 public void RemoveAt(int index)
101 {
102 BaseRemoveAt(index);
103 }
104
105 public void Remove(string name)
106 {
107 BaseRemove(name);
108 }
109
110 public void Clear()
111 {
112 BaseClear();
113 }
114
115 protected override string ElementName
116 {
117 get { return "host"; }
118 }
119 }
120
121
122 public class HostConfigElement:ConfigurationElement
123 {
124
125 [ConfigurationProperty("SSHServerHostname", IsRequired = true, IsKey = true)]
126 [StringValidator(InvalidCharacters = " ~!@#$%^&*()[]{}/;’\"|\\")]
127 public string SSHServerHostname
128 {
129 get { return (string)this["SSHServerHostname"]; }
130 set { this["SSHServerHostname"] = value; }
131 }
132
133 [ConfigurationProperty("username", IsRequired = true)]
134 [StringValidator(InvalidCharacters = " ~!@#$%^&*()[]{}/;’\"|\\")]
135 public string Username
136 {
137 get { return (string)this["username"]; }
138 set { this["username"] = value; }
139 }
140 [ConfigurationProperty("SSHport", IsRequired = true, DefaultValue = 22)]
141 [IntegerValidator(MinValue = 1, MaxValue = 65536)]
142 public int SSHPort
143 {
144 get { return (int)this["SSHport"]; }
145 set { this["SSHport"] = value; }
146 }
147
148 [ConfigurationProperty("password", IsRequired = false)]
149 public string Password
150 {
151 get { return (string)this["password"]; }
152 set { this["password"] = value; }
153 }
154
155 [ConfigurationProperty("privatekey", IsRequired = false)]
156 public string Privatekey
157 {
158 get { return (string)this["privatekey"]; }
159 set { this["privatekey"] = value; }
160 }
161
162 [ConfigurationProperty("privatekeypassphrase", IsRequired = false)]
163 public string Privatekeypassphrase
164 {
165 get { return (string)this["privatekeypassphrase"]; }
166 set { this["privatekeypassphrase"] = value; }
167 }
168
169 [ConfigurationProperty("tunnels", IsDefaultCollection = false)]
170 public TunnelCollection Tunnels
171 {
172 get { return (TunnelCollection)base["tunnels"]; }
173
174 }
175
176 }
177
178
179 public class TunnelCollection : ConfigurationElementCollection
180 {
181
182 public new TunnelConfigElement this[string name]
183 {
184 get
185 {
186 if (IndexOf(name) < 0) return null;
187
188 return (TunnelConfigElement)BaseGet(name);
189 }
190 }
191
192 public TunnelConfigElement this[int index]
193 {
194 get { return (TunnelConfigElement)BaseGet(index); }
195 }
196
197 public int IndexOf(string name)
198 {
199 name = name.ToLower();
200
201 for (int idx = 0; idx < base.Count; idx++)
202 {
203 if (this[idx].Name.ToLower() == name)
204 return idx;
205 }
206 return -1;
207 }
208
209 public override ConfigurationElementCollectionType CollectionType
210 {
211 get { return ConfigurationElementCollectionType.BasicMap; }
212 }
213
214 protected override ConfigurationElement CreateNewElement()
215 {
216 return new TunnelConfigElement();
217 }
218
219 protected override object GetElementKey(ConfigurationElement element)
220 {
221 return ((TunnelConfigElement)element).Name;
222 }
223
224 protected override string ElementName
225 {
226 get { return "tunnel"; }
227 }
228
229 }
230
231
232 public class TunnelConfigElement : ConfigurationElement
233 {
234
235 public TunnelConfigElement()
236 {
237 }
238
239 public TunnelConfigElement(string name, int localport, int remoteport, string destinationserver)
240 {
241
242 this.DestinationServer = destinationserver;
243 this.RemotePort = remoteport;
244 this.LocalPort = localport;
245 this.Name = name;
246 }
247
248 [ConfigurationProperty("name", IsRequired = true, IsKey = true, DefaultValue = "")]
249 public string Name
250 {
251 get { return (string)this["name"]; }
252 set { this["name"] = value; }
253 }
254
255 [ConfigurationProperty("localport", IsRequired = true, DefaultValue =1)]
256 [IntegerValidator(MinValue = 1, MaxValue = 65536)]
257 public int LocalPort
258 {
259 get { return (int)this["localport"]; }
260 set { this["localport"] = value; }
261 }
262
263 [ConfigurationProperty("remoteport", IsRequired = true, DefaultValue =1)]
264 [IntegerValidator(MinValue = 1, MaxValue = 65536)]
265 public int RemotePort
266 {
267 get { return (int)this["remoteport"]; }
268 set { this["remoteport"] = value; }
269 }
270
271 [ConfigurationProperty("destinationserver", IsRequired = true)]
272 [StringValidator(InvalidCharacters = " ~!@#$%^&*()[]{}/;’\"|\\")]
273 public string DestinationServer
274 {
275 get { return (string)this["destinationserver"]; }
276 set { this["destinationserver"] = value; }
277 }
278
279 }
280
281 }
The nesting of the collections is achieved by defining a ConfigurationProperty as a class that inherits from the ConfigurationElementCollection class, TunnelCollection in this case, as part of HostConfigElement, which contains all the configuration properties required by the host. This could be repeated ad infinitum by adding a new ConfigurationProperties to the TunnelConfigElement. Make sure that the ElementName of the collection is overridden and matches your config file, in this case
host for the HostCollection and tunnel for the TunnelCollection.
Here is the configuration file. Note that the type needs to reference the ConfigSection class, SSHTunnelWF.TunnelSection in this case, and the assembly name SSHTunnelWF:
1 <?xml version="1.0"?>
2 <configuration>
3 <configSections>
4 <section name="TunnelSection" type="SSHTunnelWF.TunnelSection,SSHTunnelWF" />
5 </configSections>
6 <startup>
7 <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/>
8 </startup>
9 <TunnelSection>
10 <host SSHServerHostname="tsg.edssdn.net" username="user" SSHport="22" password="pass" privatekey="" privatekeypassphrase="">
11 <tunnels>
12 <tunnel name="tfs" localport="8081" remoteport="8080" destinationserver="tfs2010.dev.com" />
13 <tunnel name="sql" localport="14331" remoteport="1433" destinationserver="sql2008.dev.com" />
14 <tunnel name="crm2011app" localport="81" remoteport="80" destinationserver="crm2011betaapp.dev.com" />
15 </tunnels>
16 </host>
17 <host SSHServerHostname="blade16" username="root" SSHport="22" password="pass" privatekey="" privatekeypassphrase="">
18 <tunnels>
19 <tunnel name="vnc" localport="5902" remoteport="5902" destinationserver="blade1.dev.com" />
20 </tunnels>
21 </host>
22 </TunnelSection>
23 </configuration>
Finally, you can access the contents of this custom configuration section like this:
TunnelSection tunnels = ConfigurationManager.GetSection("TunnelSection") as TunnelSection