├── .gitignore ├── LICENSE ├── README.md ├── attribute ├── attr_common.go ├── attr_common_test.go ├── attr_duplex.go ├── attr_duplex_test.go ├── attr_ipv4.go ├── attr_ipv4_test.go ├── attr_mac.go ├── attr_mac_test.go ├── attr_replystatus.go ├── attr_replystatus_test.go ├── attr_speed.go ├── attr_speed_test.go ├── attr_string.go ├── attr_string_test.go ├── attr_vlan.go ├── attr_vlan_test.go └── doc.go ├── cmd ├── debugswitch │ ├── README.md │ └── main.go ├── enumerate-vlans │ ├── enumerate-vlans_test.go │ └── main.go ├── l2t_ss │ ├── README.md │ └── main.go └── test │ └── main.go ├── communicate ├── communicate.go ├── communicate_test.go ├── doc.go ├── ticker.go └── ticker_test.go ├── foozler ├── doc.go ├── output.go └── output_test.go ├── go.mod ├── go.sum ├── message ├── doc.go ├── message.go └── message_test.go └── target ├── builder.go ├── builder_test.go ├── doc.go ├── errors.go ├── queries.go ├── target.go └── target_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.swp 3 | cmd/enumerate-vlans/enumerate-vlans 4 | cmd/l2t_ss/l2t_ss 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Chris Marget 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cisco-l2t 2 | 3 | [![GoDoc][godoc-badge]][godoc] 4 | 5 | [godoc-badge]: https://pkg.go.dev/badge/github.com/chrismarget/cisco-l2t 6 | [godoc]: https://pkg.go.dev/github.com/chrismarget/cisco-l2t 7 | 8 | This is some tooling for working with the Cisco layer 2 traceroute service on 9 | Catalyst switches. 10 | 11 | Cisco has released [an advisory](https://tools.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-20190925-l2-traceroute) 12 | that explains how to secure this service. 13 | 14 | The layer 2 traceroute service is a UDP listener (port 2228) on (all?) 15 | Catalysts. It leaks information about configured VLANs, MAC address tables, 16 | interface names/speed/duplex, and CDP neighbors. 17 | 18 | There is no requirement for L2 adjacency. The service is available via routed 19 | paths, provided that the querier can reach an L3 address on the target machine. 20 | 21 | This library can create, send, receive and parse L2T messages. L2T messages 22 | allow you to enumerate configured VLANs, interrogate the L2 forwarding table, 23 | inquire about neighbors, interface names, speeds and duplex settings. All this 24 | without any credentials. 25 | 26 | There's an example program for enumerating VLANs on a far away (no L2 adjacency 27 | required) switch: 28 | 29 | ```sh 30 | $ ./enumerate-vlans 192.168.150.96 31 | 86 VLANs found: 1-37 40-45 50-51 53-55 71-77 81-87 91-93 98-105 132 308-309 530 960-961 1100-1102 2000-2002 2222. 32 | $ 33 | ``` 34 | 35 | Lots more examples (and a detailed readme) in the 36 | [cmd/lt2_ss directory](cmd/l2t_ss). 37 | 38 | Truly mapping a Catalyst-based L2 topology will require some creativity because 39 | you can't interrogate the MAC population directly. Guessing well-known MAC 40 | addresses, swapping the src/dst query order (to elicit different error 41 | responses), noting STP root bridge address (and incrementing it on a per-vlan 42 | basis), etc... are all probably the kinds of things that a clever application 43 | using this library would be interested in doing. 44 | 45 | The server implementation in switches is a little weird because it replies from 46 | the client-facing interface, not from the interface the client talked to. This 47 | means that the 5-tuple associated with the reply packet doesn't necessarily 48 | match the query packet. This makes it NAT un-friendly, and introduced a bunch 49 | of socket-layer challenges that this library attempts to handle as gracefully aspossible. For stealthy network analysis, the client should run with a firewall 50 | configuration that avoids generating ICMP unreachables. 51 | -------------------------------------------------------------------------------- /attribute/attr_common.go: -------------------------------------------------------------------------------- 1 | package attribute 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "math" 7 | "strconv" 8 | ) 9 | 10 | type ( 11 | AttrType byte 12 | attrCategory byte 13 | ) 14 | 15 | const ( 16 | TLsize = 2 17 | MinAttrLen = 3 18 | 19 | // It's not completely clear that these first two types 20 | // are really SRC and DST... The different query types seem to 21 | // change the meaning of these attributes. So, that's fun. 22 | // Even the switch doesn't seem to know. From some debugs: 23 | // Aug 15 22:39:46.529: L2_ATTR_SRC_MAC 24 | // attr->type : 1 attr->len : 8 attr->val : ffff.ffff.ffff 25 | // Aug 15 22:39:46.529: L2_ATTR_DST_MAC 26 | // attr->type : 2 attr->len : 8 attr->val : b8f6.b115.3a57 27 | // ...then later (but in the same millisecond)... 28 | // Aug 15 22:39:46.529: trace_request->type : l2t_request_src 29 | // Aug 15 22:39:46.529: trace_request->src_mac : b8f6.b115.3a57 30 | // trace_request->dst_mac : ffff.ffff.ffff 31 | // Best guess: src and dst are types 1 and 2, but the trace_request 32 | // module treats ignores the type field, relies on order. 33 | SrcMacType = AttrType(1) 34 | DstMacType = AttrType(2) 35 | VlanType = AttrType(3) 36 | DevNameType = AttrType(4) 37 | DevTypeType = AttrType(5) 38 | DevIPv4Type = AttrType(6) 39 | InPortNameType = AttrType(7) 40 | OutPortNameType = AttrType(8) 41 | InPortSpeedType = AttrType(9) 42 | OutPortSpeedType = AttrType(10) 43 | InPortDuplexType = AttrType(11) 44 | OutPortDuplexType = AttrType(12) 45 | NbrIPv4Type = AttrType(13) 46 | SrcIPv4Type = AttrType(14) 47 | ReplyStatusType = AttrType(15) 48 | NbrDevIDType = AttrType(16) 49 | 50 | duplexCategory = attrCategory(1) 51 | ipv4Category = attrCategory(2) 52 | macCategory = attrCategory(3) 53 | speedCategory = attrCategory(4) 54 | replyStatusCategory = attrCategory(5) 55 | stringCategory = attrCategory(6) 56 | vlanCategory = attrCategory(7) 57 | ) 58 | 59 | var ( 60 | AttrTypeString = map[AttrType]string{ 61 | SrcMacType: "L2_ATTR_SRC_MAC", // 6 Byte MAC address 62 | DstMacType: "L2_ATTR_DST_MAC", // 6 Byte MAC address 63 | VlanType: "L2_ATTR_VLAN", // 2 Byte VLAN number 64 | DevNameType: "L2_ATTR_DEV_NAME", // Null terminated string 65 | DevTypeType: "L2_ATTR_DEV_TYPE", // Null terminated string 66 | DevIPv4Type: "L2_ATTR_DEV_IP", // 4 Byte IP Address 67 | InPortNameType: "L2_ATTR_INPORT_NAME", // Null terminated string 68 | OutPortNameType: "L2_ATTR_OUTPORT_NAME", // Null terminated string 69 | InPortSpeedType: "L2_ATTR_INPORT_SPEED", // 4 Bytes 70 | OutPortSpeedType: "L2_ATTR_OUTPORT_SPEED", // 4 Bytes 71 | InPortDuplexType: "L2_ATTR_INPORT_DUPLEX", // 1 Byte 72 | OutPortDuplexType: "L2_ATTR_OUTPORT_DUPLEX", // 1 Byte 73 | NbrIPv4Type: "L2_ATTR_NBR_IP", // 4 Byte IP Address 74 | SrcIPv4Type: "L2_ATTR_SRC_IP", // 4 Byte IP Address 75 | ReplyStatusType: "L2_ATTR_REPLY_STATUS", // 1 Byte reply status 76 | NbrDevIDType: "L2_ATTR_NBR_DEV_ID", // Null terminated string 77 | } 78 | 79 | AttrTypePrettyString = map[AttrType]string{ 80 | SrcMacType: "source MAC address", 81 | DstMacType: "destination MAC address", 82 | VlanType: "VLAN", 83 | DevNameType: "device name", 84 | DevTypeType: "device type", 85 | DevIPv4Type: "device IPv4 address", 86 | InPortNameType: "ingress port name", 87 | OutPortNameType: "egress port name", 88 | InPortSpeedType: "ingress port speed", 89 | OutPortSpeedType: "egress port speed", 90 | InPortDuplexType: "ingress port duplex", 91 | OutPortDuplexType: "egress port duplex", 92 | NbrIPv4Type: "neighbor IPv4 address", 93 | SrcIPv4Type: "source IPv4 address", 94 | ReplyStatusType: "reply status", 95 | NbrDevIDType: "neighbor device ID", 96 | } 97 | 98 | attrCategoryByType = map[AttrType]attrCategory{ 99 | SrcMacType: macCategory, 100 | DstMacType: macCategory, 101 | VlanType: vlanCategory, 102 | DevNameType: stringCategory, 103 | DevTypeType: stringCategory, 104 | DevIPv4Type: ipv4Category, 105 | InPortNameType: stringCategory, 106 | OutPortNameType: stringCategory, 107 | InPortSpeedType: speedCategory, 108 | OutPortSpeedType: speedCategory, 109 | InPortDuplexType: duplexCategory, 110 | OutPortDuplexType: duplexCategory, 111 | NbrIPv4Type: ipv4Category, 112 | SrcIPv4Type: ipv4Category, 113 | ReplyStatusType: replyStatusCategory, 114 | NbrDevIDType: stringCategory, 115 | } 116 | 117 | attrLenByCategory = map[attrCategory]int{ 118 | duplexCategory: 3, 119 | ipv4Category: 6, 120 | macCategory: 8, 121 | speedCategory: 6, 122 | replyStatusCategory: 3, 123 | stringCategory: -1, 124 | vlanCategory: 4, 125 | } 126 | 127 | attrCategoryString = map[attrCategory]string{ 128 | duplexCategory: "interface duplex", 129 | ipv4Category: "IPv4 address", 130 | macCategory: "MAC address", 131 | speedCategory: "interface speed", 132 | replyStatusCategory: "reply status", 133 | stringCategory: "string", 134 | vlanCategory: "VLAN", 135 | } 136 | ) 137 | 138 | // MarshalAttribute returns a []byte containing a wire 139 | // format representation of the supplied attribute. 140 | func MarshalAttribute(a Attribute) []byte { 141 | t := byte(a.Type()) 142 | l := byte(a.Len()) 143 | b := a.Bytes() 144 | return append([]byte{t, l}, b...) 145 | } 146 | 147 | // UnmarshalAttribute returns an Attribute of the appropriate 148 | // kind, depending on what's in the first byte (attribute type marker) 149 | func UnmarshalAttribute(b []byte) (Attribute, error) { 150 | observedLength := len(b) 151 | if observedLength < MinAttrLen { 152 | return nil, fmt.Errorf("undersize attribute, cannot unmarshal %d bytes (%d byte minimum)", observedLength, MinAttrLen) 153 | } 154 | 155 | if observedLength > math.MaxUint8 { 156 | return nil, fmt.Errorf("oversize attribute, cannot unmarshal %d bytes (%d byte maximum)", observedLength, math.MaxUint8) 157 | } 158 | 159 | claimedLength := int(b[1]) 160 | if observedLength != claimedLength { 161 | return nil, fmt.Errorf("cannot unmarshal attribute. length field says %d bytes, got %d bytes", observedLength, claimedLength) 162 | } 163 | 164 | t := AttrType(b[0]) 165 | switch attrCategoryByType[t]{ 166 | case duplexCategory: 167 | return &duplexAttribute{attrType: t, attrData: b[2:]}, nil 168 | case ipv4Category: 169 | return &ipv4Attribute{attrType: t, attrData: b[2:]}, nil 170 | case macCategory: 171 | return &macAttribute{attrType: t, attrData: b[2:]}, nil 172 | case replyStatusCategory: 173 | return &replyStatusAttribute{attrType: t, attrData: b[2:]}, nil 174 | case speedCategory: 175 | return &speedAttribute{attrType: t, attrData: b[2:]}, nil 176 | case stringCategory: 177 | return &stringAttribute{attrType: t, attrData: b[2:]}, nil 178 | case vlanCategory: 179 | return &vlanAttribute{attrType: t, attrData: b[2:]}, nil 180 | } 181 | 182 | return nil, fmt.Errorf("cannot umarshal attribute of unknown type %d", t) 183 | } 184 | 185 | // Attribute represents an attribute field from a 186 | // Cisco L2 Traceroute packet. 187 | type Attribute interface { 188 | 189 | // Type returns the Attribute's type 190 | Type() AttrType 191 | 192 | // Len returns the attribute's length. It includes the length 193 | // of the payload, plus 2 more bytes to cover the Type and 194 | // Length bytes in the header. This is the same value that 195 | // appears in the length field of the attribute in wire format. 196 | Len() uint8 197 | 198 | // String returns the attribute payload in printable format. 199 | // It does not include any descriptive wrapper stuff, does 200 | // well when combined with something from AttrTypePrettyString. 201 | String() string 202 | 203 | // Validate returns an error if the attribute is malformed. 204 | Validate() error 205 | 206 | // Bytes returns a []byte containing the attribute payload. 207 | Bytes() []byte 208 | } 209 | 210 | // AttrBuilder builds L2T attributes. 211 | // Calling SetType is mandatory. 212 | // Calling one of the other "Set" methods is required 213 | // for most values of "AttrType" 214 | type AttrBuilder interface { 215 | // SetType sets the attribute type. 216 | SetType(AttrType) AttrBuilder 217 | 218 | // SetString configures the attribute with a string value. 219 | // 220 | // Use it for attributes belonging to these categories: 221 | // duplexCategory: "Half" / "Full" / "Auto" 222 | // ipv4Category: "x.x.x.x" 223 | // macCategory: "xx:xx:xx:xx:xx:xx" 224 | // replyStatusCategory "Success" / "Source Mac address not found" 225 | // speedCategory: "10Mb/s" / "1Gb/s" / "10Tb/s" 226 | // stringcategory: "whatever" 227 | // vlanCategory: "100" 228 | SetString(string) AttrBuilder 229 | 230 | // SetInt configures the attribute with an int value. 231 | // 232 | // Use it for attributes belonging to these categories: 233 | // duplexCategory 234 | // ipv4Category 235 | // replyStatusCategory 236 | // speedCategory 237 | // vlanCategory 238 | SetInt(uint32) AttrBuilder 239 | 240 | // SetBytes configures the attribute with a byte slice. 241 | // 242 | // Use it for attributes belonging to any category. 243 | SetBytes([]byte) AttrBuilder 244 | 245 | // Build builds attribute based on the AttrType and one of 246 | // the payloads configured with a "Set" method. 247 | Build() (Attribute, error) 248 | } 249 | 250 | type defaultAttrBuilder struct { 251 | attrType AttrType 252 | typeHasBeenSet bool 253 | stringPayload string 254 | stringHasBeenSet bool 255 | intPayload uint32 256 | intHasBeenSet bool 257 | bytesPayload []byte 258 | bytesHasBeenSet bool 259 | } 260 | 261 | func NewAttrBuilder() AttrBuilder { 262 | return &defaultAttrBuilder{} 263 | } 264 | 265 | func (o *defaultAttrBuilder) SetType(t AttrType) AttrBuilder { 266 | o.attrType = t 267 | o.typeHasBeenSet = true 268 | return o 269 | } 270 | 271 | func (o *defaultAttrBuilder) SetString(s string) AttrBuilder { 272 | o.stringPayload = s 273 | o.stringHasBeenSet = true 274 | return o 275 | } 276 | 277 | func (o *defaultAttrBuilder) SetInt(i uint32) AttrBuilder { 278 | o.intPayload = i 279 | o.intHasBeenSet = true 280 | return o 281 | } 282 | 283 | func (o *defaultAttrBuilder) SetBytes(b []byte) AttrBuilder { 284 | o.bytesPayload = b 285 | o.bytesHasBeenSet = true 286 | return o 287 | } 288 | 289 | func (o *defaultAttrBuilder) Build() (Attribute, error) { 290 | if o.typeHasBeenSet != true { 291 | return nil, errors.New("`Attribute.Build()' called without first having set attribute type") 292 | } 293 | 294 | switch o.attrType { 295 | case SrcMacType, DstMacType: 296 | return o.newMacAttribute() 297 | case VlanType: 298 | return o.newVlanAttribute() 299 | case DevNameType, DevTypeType, InPortNameType, OutPortNameType, NbrDevIDType: 300 | return o.newStringAttribute() 301 | case DevIPv4Type, NbrIPv4Type, SrcIPv4Type: 302 | return o.newIpv4Attribute() 303 | case InPortSpeedType, OutPortSpeedType: 304 | return o.newSpeedAttribute() 305 | case InPortDuplexType, OutPortDuplexType: 306 | return o.newDuplexAttribute() 307 | case ReplyStatusType: 308 | return o.newReplyStatusAttribute() 309 | } 310 | return nil, fmt.Errorf("cannot build, unrecognized attribute type `%d'", o.attrType) 311 | } 312 | 313 | // checkTypeLen checks an attribute's Attribute.Type() and Attribute.Len() 314 | // output against norms for the supplied category. 315 | func checkTypeLen(a Attribute, category attrCategory) error { 316 | // Check the supplied attribute against the supplied category 317 | if attrCategoryByType[a.Type()] != category { 318 | return fmt.Errorf("expected '%s' category attribute, got '%s'", attrCategoryString[attrCategoryByType[a.Type()]], AttrTypeString[a.Type()]) 319 | } 320 | 321 | // An attribute should never be less than 3 bytes (including TL header) 322 | if a.Len() < MinAttrLen { 323 | return fmt.Errorf("undersize attribute: got %d bytes, need at least %d bytes", a.Len(), MinAttrLen) 324 | } 325 | 326 | // l2t attribute length field is only a single byte. We better 327 | // not have more data than can be described by that byte. 328 | if a.Len() > math.MaxUint8 { 329 | msg := fmt.Sprintf("oversize attribute: got %d bytes, max %d bytes", a.Len(), math.MaxUint8) 330 | return errors.New(msg) 331 | } 332 | 333 | // Some attribute types have variable lengths. 334 | // Their attrLenByCategory entry is -1 (unknown). 335 | // Only length check affirmative (not -1) sizes. 336 | expectedLen := attrLenByCategory[attrCategoryByType[a.Type()]] 337 | if expectedLen >= MinAttrLen { 338 | if int(a.Len()) != expectedLen { 339 | return fmt.Errorf("%s attribute should be exactly %d bytes, got %d bytes", AttrTypeString[a.Type()], expectedLen, a.Len()) 340 | } 341 | } 342 | return nil 343 | } 344 | 345 | // LocationOfAttributeByType returns the index of the first instance 346 | // of an AttrType within a slice, or -1 if not found 347 | func LocationOfAttributeByType(s []Attribute, aType AttrType) int { 348 | for i, a := range s { 349 | if a.Type() == aType { 350 | return i 351 | } 352 | } 353 | return -1 354 | } 355 | 356 | // AttrStringToType attempts to convert a string to an AttrType. 357 | // You can give it a string-y number ("2") or a known AttrType 358 | // label ("L2T_REQUEST_SRC"). 359 | // Unknown labels and out-of-range string-y numbers produce an error. 360 | func AttrStringToType(in string) (AttrType, error) { 361 | inAsInt, err := strconv.Atoi(in) 362 | if err == nil { 363 | if inAsInt >= 0 && inAsInt <= math.MaxUint8 { 364 | return AttrType(inAsInt), nil 365 | } else { 366 | return 0, fmt.Errorf("value %d out of range", inAsInt) 367 | } 368 | } else { 369 | for t, s := range AttrTypeString { 370 | if s == in { 371 | return t, nil 372 | } 373 | } 374 | } 375 | return 0, fmt.Errorf("unknown attribute type %s", in) 376 | } 377 | 378 | // SortAttributes sorts a map (what you'd get from message.Attributes()) 379 | // into a slice with the attributes in numerical order. 380 | func SortAttributes(in map[AttrType]Attribute) []Attribute { 381 | var out []Attribute 382 | for i := 0; i <= math.MaxUint8; i++ { 383 | if a, ok := in[AttrType(i)]; ok { 384 | out = append(out, a) 385 | } 386 | } 387 | return out 388 | } 389 | -------------------------------------------------------------------------------- /attribute/attr_common_test.go: -------------------------------------------------------------------------------- 1 | package attribute 2 | 3 | import ( 4 | "math" 5 | "reflect" 6 | "strconv" 7 | "testing" 8 | ) 9 | 10 | func getAttrsByCategory(category attrCategory) []AttrType { 11 | var attrTypesToTest []AttrType 12 | for k, v := range attrCategoryByType { 13 | if v == category { 14 | attrTypesToTest = append(attrTypesToTest, k) 15 | } 16 | } 17 | return attrTypesToTest 18 | } 19 | 20 | func TestUnMarshalAttribute_BadData(t *testing.T) { 21 | var testData [][]byte 22 | testData = append(testData, []byte{}) 23 | for i := 0; i <= math.MaxUint8; i++ { 24 | testData[0] = append(testData[0], 1) // oversize (fill 1st instance to 256 bytes) 25 | } 26 | testData = append(testData, []byte{}) // undersize 27 | testData = append(testData, []byte{1}) // undersize 28 | testData = append(testData, []byte{1, 2}) // undersize 29 | testData = append(testData, []byte{1, 8, 0, 0, 0, 0, 0}) // wrong length 30 | 31 | for t := 0; t <= math.MaxUint8; t++ { 32 | if _, ok := attrCategoryByType[AttrType(t)]; !ok { 33 | testData = append(testData, []byte{byte(t), 3, 1}) // fill testData with bogus types 34 | } 35 | } 36 | 37 | for d, _ := range testData { 38 | _, err := UnmarshalAttribute(testData[d]) 39 | if err == nil { 40 | t.Fatalf("bad data should have produced an error") 41 | } 42 | } 43 | } 44 | 45 | func TestUnmarshalAttribute(t *testing.T) { 46 | var testData [][]byte 47 | var testType []AttrType 48 | var testString []string 49 | 50 | testData = append(testData, []byte{1, 8, 1, 2, 3, 4, 5, 6}) 51 | testType = append(testType, SrcMacType) 52 | testString = append(testString, "01:02:03:04:05:06") 53 | 54 | testData = append(testData, []byte{2, 8, 2, 3, 4, 5, 6, 7}) 55 | testType = append(testType, DstMacType) 56 | testString = append(testString, "02-03-04-05-06-07") 57 | 58 | testData = append(testData, []byte{3, 4, 1, 1}) 59 | testType = append(testType, VlanType) 60 | testString = append(testString, "257") 61 | 62 | testData = append(testData, []byte{4, 9, 104, 101, 108, 108, 111, 49, 0}) 63 | testType = append(testType, DevNameType) 64 | testString = append(testString, "hello1") 65 | 66 | testData = append(testData, []byte{5, 9, 104, 101, 108, 108, 111, 50, 0}) 67 | testType = append(testType, DevTypeType) 68 | testString = append(testString, "hello2") 69 | 70 | testData = append(testData, []byte{6, 6, 1, 2, 3, 4}) 71 | testType = append(testType, DevIPv4Type) 72 | testString = append(testString, "1.2.3.4") 73 | 74 | testData = append(testData, []byte{7, 9, 104, 101, 108, 108, 111, 51, 0}) 75 | testType = append(testType, InPortNameType) 76 | testString = append(testString, "hello3") 77 | 78 | testData = append(testData, []byte{8, 9, 104, 101, 108, 108, 111, 52, 0}) 79 | testType = append(testType, OutPortNameType) 80 | testString = append(testString, "hello4") 81 | 82 | testData = append(testData, []byte{9, 6, 0, 0, 0, 4}) 83 | testType = append(testType, InPortSpeedType) 84 | testString = append(testString, "10gbps") 85 | 86 | testData = append(testData, []byte{10, 6, 0, 0, 0, 5}) 87 | testType = append(testType, OutPortSpeedType) 88 | testString = append(testString, "100gb/s") 89 | 90 | testData = append(testData, []byte{11, 3, 0}) 91 | testType = append(testType, InPortDuplexType) 92 | testString = append(testString, "auto") 93 | 94 | testData = append(testData, []byte{12, 3, 1}) 95 | testType = append(testType, OutPortDuplexType) 96 | testString = append(testString, "half") 97 | 98 | testData = append(testData, []byte{13, 6, 10, 11, 12, 13}) 99 | testType = append(testType, NbrIPv4Type) 100 | testString = append(testString, "10.11.12.13") 101 | 102 | testData = append(testData, []byte{14, 6, 20, 21, 22, 23}) 103 | testType = append(testType, SrcIPv4Type) 104 | testString = append(testString, "20.21.22.23") 105 | 106 | testData = append(testData, []byte{15, 3, 8}) 107 | testType = append(testType, ReplyStatusType) 108 | testString = append(testString, "Destination Mac address not found") 109 | 110 | testData = append(testData, []byte{16, 9, 104, 101, 108, 108, 111, 53, 0}) 111 | testType = append(testType, NbrDevIDType) 112 | testString = append(testString, "hello5") 113 | 114 | for i, _ := range testData { 115 | result, err := UnmarshalAttribute(testData[i]) 116 | if err != nil { 117 | t.Fatal(err) 118 | } 119 | 120 | expected, err := NewAttrBuilder().SetType(testType[i]).SetString(testString[i]).Build() 121 | if err != nil { 122 | t.Fatal(err) 123 | } 124 | 125 | if !reflect.DeepEqual(expected, result) { 126 | t.Fatalf("structures don't match") 127 | } 128 | } 129 | } 130 | 131 | func TestAttrStringToType(t *testing.T) { 132 | badInDigits := []string{"-1", "256"} 133 | for _, bad := range badInDigits { 134 | _, err := AttrStringToType(bad) 135 | if err == nil { 136 | t.Fatalf("%s should have produced an error", bad) 137 | } 138 | } 139 | 140 | badInStrings := []string{"foo", "whatever"} 141 | for _, bad := range badInStrings { 142 | _, err := AttrStringToType(bad) 143 | if err == nil { 144 | t.Fatalf("%s should have produced an error", bad) 145 | } 146 | } 147 | 148 | var goodInDigits []string 149 | for i := 0; i <= math.MaxUint8; i++ { 150 | goodInDigits = append(goodInDigits, strconv.Itoa(i)) 151 | } 152 | for _, good := range goodInDigits { 153 | _, err := AttrStringToType(good) 154 | if err != nil { 155 | t.Fatalf("%s should not have caused an error", good) 156 | } 157 | } 158 | 159 | var goodInStrings []string 160 | for _, s := range AttrTypeString { 161 | goodInStrings = append(goodInStrings, s) 162 | } 163 | for _, good := range goodInStrings { 164 | _, err := AttrStringToType(good) 165 | if err != nil { 166 | t.Fatalf("%s should not have caused an error", good) 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /attribute/attr_duplex.go: -------------------------------------------------------------------------------- 1 | package attribute 2 | 3 | import ( 4 | "fmt" 5 | "errors" 6 | "strings" 7 | ) 8 | 9 | type ( 10 | portDuplex byte 11 | ) 12 | 13 | const ( 14 | autoDuplex = portDuplex(0) 15 | halfDuplex = portDuplex(1) 16 | fullDuplex = portDuplex(2) 17 | ) 18 | 19 | var ( 20 | portDuplexToString = map[portDuplex]string{ 21 | autoDuplex: "Auto", 22 | halfDuplex: "Half", 23 | fullDuplex: "Full", 24 | } 25 | ) 26 | 27 | type duplexAttribute struct { 28 | attrType AttrType 29 | attrData []byte 30 | } 31 | 32 | func (o duplexAttribute) Type() AttrType { 33 | return o.attrType 34 | } 35 | 36 | func (o duplexAttribute) Len() uint8 { 37 | return uint8(TLsize + len(o.attrData)) 38 | } 39 | 40 | func (o duplexAttribute) String() string { 41 | return portDuplexToString[portDuplex(o.attrData[0])] 42 | } 43 | 44 | func (o duplexAttribute) Validate() error { 45 | err := checkTypeLen(o, duplexCategory) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | if _, ok := portDuplexToString[portDuplex(o.attrData[0])]; !ok { 51 | return fmt.Errorf("`%#x' not a valid payload for %s", o.attrData[0], AttrTypeString[o.attrType]) 52 | } 53 | 54 | return nil 55 | } 56 | 57 | func (o duplexAttribute) Bytes() []byte { 58 | return o.attrData 59 | } 60 | 61 | // newDuplexAttribute returns a new attribute from duplexCategory 62 | func (o *defaultAttrBuilder) newDuplexAttribute() (Attribute, error) { 63 | var duplexByte byte 64 | var success bool 65 | switch { 66 | case o.stringHasBeenSet: 67 | for portDuplex, duplexString := range portDuplexToString { 68 | if strings.ToLower(o.stringPayload) == strings.ToLower(duplexString) { 69 | duplexByte = byte(portDuplex) 70 | success = true 71 | } 72 | } 73 | if !success { 74 | return nil, fmt.Errorf("string payload `%s' unrecognized for duplex type", o.stringPayload) 75 | } 76 | case o.intHasBeenSet: 77 | for portDuplex, _ := range portDuplexToString { 78 | if uint8(o.intPayload) == uint8(portDuplex) { 79 | duplexByte = byte(portDuplex) 80 | success = true 81 | } 82 | } 83 | if !success { 84 | return nil, fmt.Errorf("int payload `%d' unrecognized for duplex type", o.intPayload) 85 | } 86 | case o.bytesHasBeenSet: 87 | if len(o.bytesPayload) != 1 { 88 | return nil, errors.New("bytes payload invalid length for creating duplex attribute") 89 | } 90 | duplexByte = o.bytesPayload[0] 91 | default: 92 | return nil, fmt.Errorf("cannot build, no attribute payload found for category %s attribute", attrCategoryString[duplexCategory]) 93 | } 94 | 95 | a := &duplexAttribute{ 96 | attrType: o.attrType, 97 | attrData: []byte{duplexByte}, 98 | } 99 | 100 | err := a.Validate() 101 | if err != nil { 102 | return nil, err 103 | } 104 | 105 | return a, nil 106 | } 107 | -------------------------------------------------------------------------------- /attribute/attr_duplex_test.go: -------------------------------------------------------------------------------- 1 | package attribute 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "math" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | func TestDuplexAttribute_String(t *testing.T) { 12 | var ( 13 | duplexStringTestData = map[string]portDuplex{ 14 | "Auto": portDuplex(0), 15 | "Half": portDuplex(1), 16 | "Full": portDuplex(2), 17 | } 18 | ) 19 | 20 | for _, duplexAttrType := range getAttrsByCategory(duplexCategory) { 21 | for expected, data := range duplexStringTestData { 22 | testAttr := duplexAttribute{ 23 | attrType: duplexAttrType, 24 | attrData: []byte{byte(data)}, 25 | } 26 | result := testAttr.String() 27 | if result != expected { 28 | t.Fatalf("expected %s, got %s", expected, result) 29 | } 30 | } 31 | } 32 | } 33 | 34 | func TestDuplexAttribute_Validate_WithGoodData(t *testing.T) { 35 | goodData := [][]byte{ 36 | []byte{0}, 37 | []byte{1}, 38 | []byte{2}, 39 | } 40 | for _, duplexAttrType := range getAttrsByCategory(duplexCategory) { 41 | for _, testData := range goodData { 42 | testAttr := duplexAttribute{ 43 | attrType: duplexAttrType, 44 | attrData: testData, 45 | } 46 | err := testAttr.Validate() 47 | if err != nil { 48 | t.Fatalf(err.Error()+"\n"+"Supposed good data %s produced error for %s.", 49 | fmt.Sprintf("%v", []byte(testAttr.attrData)), AttrTypeString[duplexAttrType]) 50 | } 51 | } 52 | } 53 | } 54 | 55 | func TestDuplexAttribute_Validate_WithBadData(t *testing.T) { 56 | badData := [][]byte{ 57 | nil, 58 | []byte{}, 59 | []byte{0, 0}, 60 | } 61 | 62 | for i := 3; i <= math.MaxUint8; i++ { 63 | badData = append(badData, []byte{byte(i)}) 64 | } 65 | 66 | for _, duplexAttrType := range getAttrsByCategory(duplexCategory) { 67 | for _, testData := range badData { 68 | testAttr := duplexAttribute{ 69 | attrType: duplexAttrType, 70 | attrData: testData, 71 | } 72 | 73 | err := testAttr.Validate() 74 | if err == nil { 75 | t.Fatalf("Bad data %s in %s did not error.", 76 | fmt.Sprintf("%v", []byte(testAttr.attrData)), AttrTypeString[duplexAttrType]) 77 | } 78 | } 79 | } 80 | } 81 | 82 | func TestNewAttrBuilder_Duplex(t *testing.T) { 83 | for _, duplexAttrType := range getAttrsByCategory(duplexCategory) { 84 | for k, v := range portDuplexToString { 85 | expected := []byte{byte(duplexAttrType), 3, byte(k)} 86 | byInt, err := NewAttrBuilder().SetType(duplexAttrType).SetInt(uint32(k)).Build() 87 | if err != nil { 88 | t.Fatal(err) 89 | } 90 | byString, err := NewAttrBuilder().SetType(duplexAttrType).SetString(v).Build() 91 | if err != nil { 92 | t.Fatal(err) 93 | } 94 | byStringLower, err := NewAttrBuilder().SetType(duplexAttrType).SetString(strings.ToLower(v)).Build() 95 | if err != nil { 96 | t.Fatal(err) 97 | } 98 | byStringUpper, err := NewAttrBuilder().SetType(duplexAttrType).SetString(strings.ToUpper(v)).Build() 99 | if err != nil { 100 | t.Fatal(err) 101 | } 102 | byByte, err := NewAttrBuilder().SetType(duplexAttrType).SetBytes([]byte{byte(k)}).Build() 103 | if err != nil { 104 | t.Fatal(err) 105 | } 106 | if bytes.Compare(expected, MarshalAttribute(byInt)) != 0 { 107 | t.Fatal("Attributes don't match") 108 | } 109 | if bytes.Compare(byInt.Bytes(), byString.Bytes()) != 0 { 110 | t.Fatal("Attributes don't match") 111 | } 112 | if bytes.Compare(byString.Bytes(), byStringLower.Bytes()) != 0 { 113 | t.Fatal("Attributes don't match") 114 | } 115 | if bytes.Compare(byStringLower.Bytes(), byStringUpper.Bytes()) != 0 { 116 | t.Fatal("Attributes don't match") 117 | } 118 | if bytes.Compare(byStringUpper.Bytes(), byByte.Bytes()) != 0 { 119 | t.Fatal("Attributes don't match") 120 | } 121 | if bytes.Compare(byByte.Bytes(), byInt.Bytes()) != 0 { 122 | t.Fatal("Attributes don't match") 123 | } 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /attribute/attr_ipv4.go: -------------------------------------------------------------------------------- 1 | package attribute 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "net" 7 | ) 8 | 9 | type ipv4Attribute struct { 10 | attrType AttrType 11 | attrData []byte 12 | } 13 | 14 | func (o ipv4Attribute) Type() AttrType { 15 | return o.attrType 16 | } 17 | 18 | func (o ipv4Attribute) Len() uint8 { 19 | return uint8(TLsize + len(o.attrData)) 20 | } 21 | 22 | func (o ipv4Attribute) String() string { 23 | return net.IP(o.attrData).String() 24 | } 25 | 26 | func (o ipv4Attribute) Validate() error { 27 | err := checkTypeLen(o, ipv4Category) 28 | if err != nil { 29 | return err 30 | } 31 | return nil 32 | } 33 | 34 | func (o ipv4Attribute) Bytes() []byte { 35 | return o.attrData 36 | } 37 | 38 | // newIpv4Attribute returns a new attribute from ipv4Category 39 | func (o *defaultAttrBuilder) newIpv4Attribute() (Attribute, error) { 40 | var err error 41 | ipv4Bytes := make([]byte, 4) 42 | switch { 43 | case o.stringHasBeenSet: 44 | b := net.ParseIP(o.stringPayload) 45 | if b == nil { 46 | return nil, fmt.Errorf("cannot convert `%s' to an IPv4 address", o.stringPayload) 47 | } 48 | ipv4Bytes = b[len(b)-4:] 49 | case o.bytesHasBeenSet: 50 | if len(o.bytesPayload) != 4 { 51 | return nil, fmt.Errorf("attempt to configure IPv4 attribute with %d byte payload", len(o.bytesPayload)) 52 | } 53 | ipv4Bytes = o.bytesPayload 54 | case o.intHasBeenSet: 55 | binary.BigEndian.PutUint32(ipv4Bytes, o.intPayload) 56 | default: 57 | return nil, fmt.Errorf("cannot build, no attribute payload found for category %s attribute", attrCategoryString[ipv4Category]) 58 | } 59 | 60 | a := &ipv4Attribute{ 61 | attrType: o.attrType, 62 | attrData: ipv4Bytes, 63 | } 64 | 65 | err = a.Validate() 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | return a, nil 71 | } 72 | -------------------------------------------------------------------------------- /attribute/attr_ipv4_test.go: -------------------------------------------------------------------------------- 1 | package attribute 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | ) 8 | 9 | func TestIpv4Attribute_String(t *testing.T) { 10 | var ( 11 | ipv4StringTestData = map[string][]byte{ 12 | "0.0.0.0": []byte{0, 0, 0, 0}, 13 | "1.2.3.4": []byte{1, 2, 3, 4}, 14 | "192.168.34.56": []byte{192, 168, 34, 56}, 15 | "224.1.2.3": []byte{224, 1, 2, 3}, 16 | "255.255.255.255": []byte{255, 255, 255, 255}, 17 | } 18 | ) 19 | 20 | for _, ipv4AttrType := range getAttrsByCategory(ipv4Category) { 21 | for expected, data := range ipv4StringTestData { 22 | testAttr := ipv4Attribute{ 23 | attrType: ipv4AttrType, 24 | attrData: data, 25 | } 26 | result := testAttr.String() 27 | if result != expected { 28 | t.Fatalf("expected %s, got %s", expected, result) 29 | } 30 | } 31 | } 32 | } 33 | 34 | func TestIpv4Attribute_Validate_WithGoodData(t *testing.T) { 35 | goodData := [][]byte{ 36 | []byte{0, 0, 0, 0}, 37 | []byte{1, 2, 3, 4}, 38 | []byte{192, 168, 34, 56}, 39 | []byte{224, 1, 2, 3}, 40 | []byte{255, 255, 255, 255}, 41 | } 42 | 43 | for _, ipv4AttrType := range getAttrsByCategory(ipv4Category) { 44 | for _, testData := range goodData { 45 | testAttr := ipv4Attribute{ 46 | attrType: ipv4AttrType, 47 | attrData: testData, 48 | } 49 | err := testAttr.Validate() 50 | if err != nil { 51 | t.Fatalf(err.Error()+"\n"+"Supposed good data %s produced error for %s.", 52 | fmt.Sprintf("%v", []byte(testAttr.attrData)), AttrTypeString[ipv4AttrType]) 53 | } 54 | } 55 | } 56 | } 57 | 58 | func TestIpv4Attribute_Validate_WithBadData(t *testing.T) { 59 | goodData := [][]byte{ 60 | nil, 61 | []byte{}, 62 | []byte{0}, 63 | []byte{0, 0}, 64 | []byte{0, 0, 0}, 65 | []byte{0, 0, 0, 0, 0}, 66 | } 67 | 68 | for _, ipv4AttrType := range getAttrsByCategory(ipv4Category) { 69 | for _, testData := range goodData { 70 | testAttr := ipv4Attribute{ 71 | attrType: ipv4AttrType, 72 | attrData: testData, 73 | } 74 | 75 | err := testAttr.Validate() 76 | if err == nil { 77 | t.Fatalf("Bad data %s in %s did not error.", 78 | fmt.Sprintf("%v", []byte(testAttr.attrData)), AttrTypeString[ipv4AttrType]) 79 | } 80 | } 81 | } 82 | } 83 | 84 | func TestNewAttrBuilder_Ipv4(t *testing.T) { 85 | intData := uint32(3232238605) 86 | stringData := "192.168.12.13" 87 | byteData := []byte{192, 168, 12, 13} 88 | for _, ipv4AttrType := range getAttrsByCategory(ipv4Category) { 89 | expected := []byte{byte(ipv4AttrType), 6, 192, 168, 12, 13} 90 | byInt, err := NewAttrBuilder().SetType(ipv4AttrType).SetInt(intData).Build() 91 | if err != nil { 92 | t.Fatal(err) 93 | } 94 | byString, err := NewAttrBuilder().SetType(ipv4AttrType).SetString(stringData).Build() 95 | if err != nil { 96 | t.Fatal(err) 97 | } 98 | byByte, err := NewAttrBuilder().SetType(ipv4AttrType).SetBytes(byteData).Build() 99 | if err != nil { 100 | t.Fatal(err) 101 | } 102 | if bytes.Compare(expected, MarshalAttribute(byInt)) != 0 { 103 | t.Fatal("Attributes don't match") 104 | } 105 | if bytes.Compare(byInt.Bytes(), byString.Bytes()) != 0 { 106 | t.Fatal("Attributes don't match") 107 | } 108 | if bytes.Compare(byString.Bytes(), byByte.Bytes()) != 0 { 109 | t.Fatal("Attributes don't match") 110 | } 111 | if bytes.Compare(byByte.Bytes(), byInt.Bytes()) != 0 { 112 | t.Fatal("Attributes don't match") 113 | } 114 | } 115 | } 116 | 117 | func TestIpv4Attribute_StringBadData(t *testing.T) { 118 | var ( 119 | badStringTestData = []string{ 120 | "hello", 121 | "0", 122 | "1", 123 | "-1", 124 | "4294967295", 125 | "4294967296", 126 | "4294967297", 127 | "1-1-1-1", 128 | "1:1:1:1", 129 | "1.256.3.4", 130 | } 131 | ) 132 | 133 | for _, ipv4AttrType := range getAttrsByCategory(ipv4Category) { 134 | for _, s := range badStringTestData { 135 | _, err := NewAttrBuilder().SetType(ipv4AttrType).SetString(s).Build() 136 | if err == nil { 137 | t.Fatalf("setting IPv4 attribute data with `%s' did not error", s) 138 | } 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /attribute/attr_mac.go: -------------------------------------------------------------------------------- 1 | package attribute 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | ) 7 | 8 | type macAttribute struct { 9 | attrType AttrType 10 | attrData []byte 11 | } 12 | 13 | func (o macAttribute) Type() AttrType { 14 | return o.attrType 15 | } 16 | 17 | func (o macAttribute) Len() uint8 { 18 | return uint8(TLsize + len(o.attrData)) 19 | } 20 | 21 | func (o macAttribute) String() string { 22 | address := net.HardwareAddr(o.attrData).String() 23 | 24 | switch o.attrType { 25 | case SrcMacType: 26 | return address 27 | case DstMacType: 28 | return address 29 | } 30 | return "" 31 | } 32 | 33 | func (o macAttribute) Validate() error { 34 | err := checkTypeLen(o, macCategory) 35 | if err != nil { 36 | return err 37 | } 38 | 39 | return nil 40 | } 41 | 42 | func (o macAttribute) Bytes() []byte { 43 | return o.attrData 44 | } 45 | 46 | // newMacAttribute returns a new attribute from macCategory 47 | func (o *defaultAttrBuilder) newMacAttribute() (Attribute, error) { 48 | var err error 49 | var macAddr net.HardwareAddr 50 | switch { 51 | case o.bytesHasBeenSet: 52 | macAddr = o.bytesPayload 53 | case o.stringHasBeenSet: 54 | macAddr, err = net.ParseMAC(o.stringPayload) 55 | if err != nil { 56 | return nil, err 57 | } 58 | default: 59 | return nil, fmt.Errorf("cannot build, no attribute payload found for category %s attribute", attrCategoryString[macCategory]) 60 | } 61 | 62 | a := &macAttribute{ 63 | attrType: o.attrType, 64 | attrData: macAddr, 65 | } 66 | 67 | err = a.Validate() 68 | if err != nil { 69 | return nil, err 70 | } 71 | 72 | return a, nil 73 | } 74 | -------------------------------------------------------------------------------- /attribute/attr_mac_test.go: -------------------------------------------------------------------------------- 1 | package attribute 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | ) 8 | 9 | func TestMacAttribute_String(t *testing.T) { 10 | var ( 11 | macStringTestData = map[string][]byte{ 12 | "00:00:00:00:00:00": []byte{0, 0, 0, 0, 0, 0}, 13 | "01:02:03:04:05:06": []byte{1, 2, 3, 4, 5, 6}, 14 | "ff:ff:ff:ff:ff:ff": []byte{255, 255, 255, 255, 255, 255}, 15 | } 16 | ) 17 | 18 | for _, macAttrType := range getAttrsByCategory(macCategory) { 19 | for expected, data := range macStringTestData { 20 | testAttr := macAttribute{ 21 | attrType: macAttrType, 22 | attrData: data, 23 | } 24 | result := testAttr.String() 25 | if result != expected { 26 | t.Fatalf("expected %s, got %s", expected, result) 27 | } 28 | } 29 | } 30 | } 31 | 32 | func TestMacAttribute_Validate_WithGoodData(t *testing.T) { 33 | goodData := [][]byte{ 34 | []byte{0, 0, 0, 0, 0, 0}, 35 | []byte{1, 2, 3, 4, 5, 6}, 36 | []byte{255, 255, 255, 255, 255, 255}, 37 | } 38 | 39 | for _, macAttrType := range getAttrsByCategory(macCategory) { 40 | for _, testData := range goodData { 41 | testAttr := macAttribute{ 42 | attrType: macAttrType, 43 | attrData: testData, 44 | } 45 | err := testAttr.Validate() 46 | if err != nil { 47 | t.Fatalf(err.Error()+"\n"+"Supposed good data %s produced error for %s.", 48 | fmt.Sprintf("%v", []byte(testAttr.attrData)), AttrTypeString[macAttrType]) 49 | } 50 | } 51 | } 52 | } 53 | 54 | func TestMacAttribute_Validate_WithBadData(t *testing.T) { 55 | badData := [][]byte{ 56 | nil, 57 | []byte{}, 58 | []byte{0}, 59 | []byte{0, 0}, 60 | []byte{0, 0, 0}, 61 | []byte{0, 0, 0, 0}, 62 | []byte{0, 0, 0, 0, 0}, 63 | []byte{0, 0, 0, 0, 0, 0, 0}, 64 | } 65 | 66 | for _, macAttrType := range getAttrsByCategory(macCategory) { 67 | for _, testData := range badData { 68 | testAttr := macAttribute{ 69 | attrType: macAttrType, 70 | attrData: testData, 71 | } 72 | 73 | err := testAttr.Validate() 74 | if err == nil { 75 | t.Fatalf("Bad data %s in %s did not error.", 76 | fmt.Sprintf("%v", []byte(testAttr.attrData)), AttrTypeString[macAttrType]) 77 | } 78 | } 79 | } 80 | } 81 | 82 | func TestNewAttrBuilder_Mac(t *testing.T) { 83 | stringData1 := "00:01:02:FD:FE:FF" 84 | stringData2 := "00:01:02:fd:fe:ff" 85 | stringData3 := "00-01-02-FD-FE-FF" 86 | stringData4 := "00-01-02-fd-fe-ff" 87 | stringData5 := "0001.02FD.FEFF" 88 | stringData6 := "0001.02fd.feff" 89 | byteData := []byte{0, 1, 2, 253, 254, 255} 90 | for _, macAttrType := range getAttrsByCategory(macCategory) { 91 | expected := []byte{byte(macAttrType), 8, 0, 1, 2, 253, 254, 255} 92 | byString1, err := NewAttrBuilder().SetType(macAttrType).SetString(stringData1).Build() 93 | if err != nil { 94 | t.Fatal(err) 95 | } 96 | byString2, err := NewAttrBuilder().SetType(macAttrType).SetString(stringData2).Build() 97 | if err != nil { 98 | t.Fatal(err) 99 | } 100 | byString3, err := NewAttrBuilder().SetType(macAttrType).SetString(stringData3).Build() 101 | if err != nil { 102 | t.Fatal(err) 103 | } 104 | byString4, err := NewAttrBuilder().SetType(macAttrType).SetString(stringData4).Build() 105 | if err != nil { 106 | t.Fatal(err) 107 | } 108 | byString5, err := NewAttrBuilder().SetType(macAttrType).SetString(stringData5).Build() 109 | if err != nil { 110 | t.Fatal(err) 111 | } 112 | byString6, err := NewAttrBuilder().SetType(macAttrType).SetString(stringData6).Build() 113 | if err != nil { 114 | t.Fatal(err) 115 | } 116 | byByte, err := NewAttrBuilder().SetType(macAttrType).SetBytes(byteData).Build() 117 | if err != nil { 118 | t.Fatal(err) 119 | } 120 | if bytes.Compare(expected, MarshalAttribute(byByte)) != 0 { 121 | t.Fatal("Attributes don't match") 122 | } 123 | if bytes.Compare(byByte.Bytes(), byString1.Bytes()) != 0 { 124 | t.Fatal("Attributes don't match") 125 | } 126 | if bytes.Compare(byString1.Bytes(), byString2.Bytes()) != 0 { 127 | t.Fatal("Attributes don't match") 128 | } 129 | if bytes.Compare(byString2.Bytes(), byString3.Bytes()) != 0 { 130 | t.Fatal("Attributes don't match") 131 | } 132 | if bytes.Compare(byString3.Bytes(), byString4.Bytes()) != 0 { 133 | t.Fatal("Attributes don't match") 134 | } 135 | if bytes.Compare(byString4.Bytes(), byString5.Bytes()) != 0 { 136 | t.Fatal("Attributes don't match") 137 | } 138 | if bytes.Compare(byString5.Bytes(), byString6.Bytes()) != 0 { 139 | t.Fatal("Attributes don't match") 140 | } 141 | if bytes.Compare(byString6.Bytes(), byByte.Bytes()) != 0 { 142 | t.Fatal("Attributes don't match") 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /attribute/attr_replystatus.go: -------------------------------------------------------------------------------- 1 | package attribute 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | //Some stuff I found by strings-ing a binary: 9 | // 10 | //Multiple devices connected to %s port of %s [%i]. 11 | //Layer2 trace aborted. 12 | //Invalid src/dest received from %s [%i]. 13 | //Layer2 trace aborted. 14 | //Unable to locate port for src %e on %s [%i]. 15 | //Layer2 trace aborted. 16 | //Unable to locate port for dst %e on %s [%i]. 17 | //Layer2 trace aborted. 18 | //%e belongs to mutliple vlans on %s [%i]. 19 | //Layer2 trace aborted. 20 | //Source and destination vlan mismatch discovered on %s [%i]. 21 | //Layer2 trace aborted. 22 | //Failed to get any layer2 path information from %s [%i]. 23 | //Layer2 trace aborted. 24 | //Layer2 path not through %s [%i]. 25 | //Layer2 trace aborted. 26 | //Unknown return code %d from %s [%i]. 27 | // 28 | //Invalid mac address 29 | //Invalid ip address 30 | //Mac found on multiple vlans 31 | //Source and destination macs are on different vlans 32 | //Device has multiple CDP neighbours 33 | //CDP neighbour has no ip 34 | //No CDP neighbour 35 | //Source Mac address not found 36 | //Destination Mac address not found 37 | //Incorrect source interface specified 38 | //Incorrect destination interface specified 39 | //Device has Multiple CDP neighbours on source port 40 | //Device has Multiple CDP neighbours on destination port 41 | 42 | // observed reply status: 43 | // 1 - normal trace, end of the line (no cdp neighbor?) 44 | // 2 - normal trace, cdp neighbor returned 45 | // 3 - bogus vlan (debugs say "internal error") 46 | // 5 - multiple CDP neighbors 47 | // 7 - source mac not found (with 48 | 49 | const ( 50 | ReplyStatusSuccess = "Success" 51 | // ReplyStatusDstNotFound = "Destination Mac address not found" 52 | // ReplyStatusSrcNotFound = "Source Mac address not found" 53 | ReplyStatusUnknown = "Status unknown" 54 | 55 | // The following strings were found together by strings-ing an IOS image. 56 | // Leap of faith makes me think they're reply status attribute messages. 57 | // EDIT: 58 | // Nope. They're actually the return codes from the l2_get_trace_info() function 59 | // e.g. : "l2t_get_reply_status: l2t_get_trace_info() returned 9(No CDP neighbour)" 60 | replyStatusInvalidMac = "Invalid mac address" 61 | replyStatusInvalidIP = "Invalid ip address" 62 | replyStausMultipleVlan = "Mac found on multiple vlans" 63 | replyStatusDifferentVlan = "Source and destination macs are on different vlans" 64 | ReplyStatusMultipleNeighbors = "Device has multiple CDP neighbours" 65 | replyStatusNoNeighborIP = "CDP neighbour has no ip" 66 | ReplyStatusNoNeighbor = "No CDP neighbour" 67 | ReplyStatusSrcNotFound = "Source Mac address not found" // l2t attr type 0x0f data 0x07 68 | ReplyStatusDstNotFound = "Destination Mac address not found" // l2t attr type 0x0f data 0x08 69 | replyStatusWrongSrcInterface = "Incorrect source interface specified" 70 | replyStatusWrongDstInterface = "Incorrect destination interface specified" 71 | replyStatusSrcMultipleNeighbors = "Device has Multiple CDP neighbours on source port" 72 | replyStatusDstMultipleNeighbors = "Device has Multiple CDP neighbours on destination port" 73 | ) 74 | 75 | type ( 76 | replyStatus byte 77 | ) 78 | 79 | var ( 80 | replyStatusToString = map[replyStatus]string{ 81 | 1: ReplyStatusSuccess, 82 | 5: ReplyStatusMultipleNeighbors, 83 | 7: ReplyStatusSrcNotFound, 84 | 8: ReplyStatusDstNotFound, 85 | 9: ReplyStatusNoNeighbor, 86 | } 87 | ) 88 | 89 | type replyStatusAttribute struct { 90 | attrType AttrType 91 | attrData []byte 92 | } 93 | 94 | func (o replyStatusAttribute) Type() AttrType { 95 | return o.attrType 96 | } 97 | 98 | func (o replyStatusAttribute) Len() uint8 { 99 | return uint8(TLsize + len(o.attrData)) 100 | } 101 | 102 | func (o replyStatusAttribute) String() string { 103 | if status, ok := replyStatusToString[replyStatus(o.attrData[0])]; ok { 104 | return status 105 | } 106 | return fmt.Sprintf("%s (%d)", ReplyStatusUnknown, o.attrData[0]) 107 | } 108 | 109 | func (o replyStatusAttribute) Validate() error { 110 | err := checkTypeLen(o, replyStatusCategory) 111 | if err != nil { 112 | return err 113 | } 114 | return nil 115 | } 116 | 117 | func (o replyStatusAttribute) Bytes() []byte { 118 | return o.attrData 119 | } 120 | 121 | // newReplyStatusAttribute returns a new attribute from replyStatusCategory 122 | func (o *defaultAttrBuilder) newReplyStatusAttribute() (Attribute, error) { 123 | var replyStatusByte byte 124 | var success bool 125 | switch { 126 | case o.stringHasBeenSet: 127 | for replyStatus, replyStatusString := range replyStatusToString { 128 | if strings.ToLower(o.stringPayload) == strings.ToLower(replyStatusString) { 129 | replyStatusByte = byte(replyStatus) 130 | success = true 131 | } 132 | } 133 | if !success { 134 | return nil, fmt.Errorf("string payload `%s' unrecognized for reply status type", o.stringPayload) 135 | } 136 | case o.intHasBeenSet: 137 | replyStatusByte = uint8(o.intPayload) 138 | case o.bytesHasBeenSet: 139 | if len(o.bytesPayload) != 1 { 140 | return nil, fmt.Errorf("cannot use %d bytes to build a reply status attribute", len(o.bytesPayload)) 141 | } 142 | replyStatusByte = o.bytesPayload[0] 143 | default: 144 | return nil, fmt.Errorf("cannot build, no attribute payload found for category %s attribute", attrCategoryString[replyStatusCategory]) 145 | } 146 | 147 | a := &replyStatusAttribute{ 148 | attrType: o.attrType, 149 | attrData: []byte{replyStatusByte}, 150 | } 151 | 152 | err := a.Validate() 153 | if err != nil { 154 | return nil, err 155 | } 156 | 157 | return a, nil 158 | } 159 | -------------------------------------------------------------------------------- /attribute/attr_replystatus_test.go: -------------------------------------------------------------------------------- 1 | package attribute 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "math" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | func TestReplyStatusAttribute_String(t *testing.T) { 12 | replyStatusStringTestData := make(map[byte]string) 13 | 14 | // Preload all test data with "Status Unknown ()" 15 | for i := 0; i <= math.MaxUint8; i++ { 16 | replyStatusStringTestData[byte(i)] = fmt.Sprintf("Status unknown (%d)", i) 17 | } 18 | 19 | // Some of the preloaded test data has actual values. Fix 'em. 20 | replyStatusStringTestData[1] = "Success" 21 | replyStatusStringTestData[7] = "Source Mac address not found" 22 | replyStatusStringTestData[5] = "Device has multiple CDP neighbours" 23 | replyStatusStringTestData[8] = "Destination Mac address not found" 24 | replyStatusStringTestData[9] = "No CDP neighbour" 25 | 26 | for _, replyStatusAttrType := range getAttrsByCategory(replyStatusCategory) { 27 | for data, expected := range replyStatusStringTestData { 28 | testAttr := replyStatusAttribute{ 29 | attrType: replyStatusAttrType, 30 | attrData: []byte{data}, 31 | } 32 | result := testAttr.String() 33 | if result != expected { 34 | t.Fatalf("expected %s, got %s", expected, result) 35 | } 36 | } 37 | } 38 | } 39 | 40 | func TestReplyStatusAttribute_Validate_WithGoodData(t *testing.T) { 41 | var goodData [][]byte 42 | // test all possible values 43 | for i := 1; i <= math.MaxUint8; i++ { 44 | goodData = append(goodData, []byte{byte(i)}) 45 | } 46 | 47 | for _, replyStatusAttrType := range getAttrsByCategory(replyStatusCategory) { 48 | for _, testData := range goodData { 49 | testAttr := replyStatusAttribute{ 50 | attrType: replyStatusAttrType, 51 | attrData: testData, 52 | } 53 | err := testAttr.Validate() 54 | if err != nil { 55 | t.Fatalf(err.Error()+"\n"+"Supposed good data %s produced error for %s.", 56 | fmt.Sprintf("%v", []byte(testAttr.attrData)), AttrTypeString[replyStatusAttrType]) 57 | } 58 | } 59 | } 60 | } 61 | 62 | func TestReplyStatusAttribute_Validate_WithBadData(t *testing.T) { 63 | goodData := [][]byte{ 64 | nil, 65 | []byte{}, 66 | []byte{0, 0}, 67 | } 68 | 69 | for _, replyStatusAttrType := range getAttrsByCategory(replyStatusCategory) { 70 | for _, testData := range goodData { 71 | testAttr := replyStatusAttribute{ 72 | attrType: replyStatusAttrType, 73 | attrData: testData, 74 | } 75 | 76 | err := testAttr.Validate() 77 | if err == nil { 78 | t.Fatalf("Bad data %s in %s did not error.", 79 | fmt.Sprintf("%v", []byte(testAttr.attrData)), AttrTypeString[replyStatusAttrType]) 80 | } 81 | } 82 | } 83 | } 84 | 85 | func TestNewAttrBuilder_ReplyStatus(t *testing.T) { 86 | for _, replyStatusAttrType := range getAttrsByCategory(replyStatusCategory) { 87 | for k, v := range replyStatusToString { 88 | expected := []byte{byte(replyStatusAttrType), 3, byte(k)} 89 | byInt, err := NewAttrBuilder().SetType(replyStatusAttrType).SetInt(uint32(k)).Build() 90 | if err != nil { 91 | t.Fatal(err) 92 | } 93 | byString, err := NewAttrBuilder().SetType(replyStatusAttrType).SetString(v).Build() 94 | if err != nil { 95 | t.Fatal(err) 96 | } 97 | byStringLower, err := NewAttrBuilder().SetType(replyStatusAttrType).SetString(strings.ToLower(v)).Build() 98 | if err != nil { 99 | t.Fatal(err) 100 | } 101 | byStringUpper, err := NewAttrBuilder().SetType(replyStatusAttrType).SetString(strings.ToUpper(v)).Build() 102 | if err != nil { 103 | t.Fatal(err) 104 | } 105 | byByte, err := NewAttrBuilder().SetType(replyStatusAttrType).SetBytes([]byte{byte(k)}).Build() 106 | if err != nil { 107 | t.Fatal(err) 108 | } 109 | if bytes.Compare(expected, MarshalAttribute(byInt)) != 0 { 110 | t.Fatal("Attributes don't match") 111 | } 112 | if bytes.Compare(byInt.Bytes(), byByte.Bytes()) != 0 { 113 | t.Fatal("Attributes don't match") 114 | } 115 | if bytes.Compare(byByte.Bytes(), byString.Bytes()) != 0 { 116 | t.Fatal("Attributes don't match") 117 | } 118 | if bytes.Compare(byString.Bytes(), byStringLower.Bytes()) != 0 { 119 | t.Fatal("Attributes don't match") 120 | } 121 | if bytes.Compare(byStringLower.Bytes(), byStringUpper.Bytes()) != 0 { 122 | t.Fatal("Attributes don't match") 123 | } 124 | if bytes.Compare(byStringUpper.Bytes(), byInt.Bytes()) != 0 { 125 | t.Fatal("Attributes don't match") 126 | } 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /attribute/attr_speed.go: -------------------------------------------------------------------------------- 1 | package attribute 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "fmt" 7 | "math" 8 | "reflect" 9 | "strconv" 10 | "strings" 11 | "unicode" 12 | ) 13 | 14 | const ( 15 | autoSpeedString = "Auto" 16 | megaBitPerSecondSuffix = "Mbps" 17 | maxSpeedMbps = 100000000 18 | maxSpeedWireFormat = 9 19 | ) 20 | 21 | type speedAttribute struct { 22 | attrType AttrType 23 | attrData []byte 24 | } 25 | 26 | func (o speedAttribute) Type() AttrType { 27 | return o.attrType 28 | } 29 | 30 | func (o speedAttribute) Len() uint8 { 31 | return uint8(TLsize + len(o.attrData)) 32 | } 33 | 34 | func (o speedAttribute) String() string { 35 | // 32-bit zero is a special case 36 | if reflect.DeepEqual(o.attrData, []byte{0, 0, 0, 0}) { 37 | return autoSpeedString 38 | } 39 | 40 | return SpeedBytesToString(o.attrData) 41 | } 42 | 43 | func (o speedAttribute) Validate() error { 44 | err := checkTypeLen(o, speedCategory) 45 | if err != nil { 46 | return err 47 | } 48 | 49 | if binary.BigEndian.Uint32(o.attrData) > maxSpeedWireFormat { 50 | return fmt.Errorf("wire format speed `%d' exceeds maximum value (%d)", binary.BigEndian.Uint32(o.attrData), maxSpeedWireFormat) 51 | } 52 | 53 | speedVal := int(math.Pow(10, float64(binary.BigEndian.Uint32(o.attrData)))) 54 | if speedVal > maxSpeedMbps { 55 | return fmt.Errorf("interface speed `%d' exceeds maximum value (%d)", speedVal, maxSpeedMbps) 56 | } 57 | 58 | return nil 59 | } 60 | 61 | func (o speedAttribute) Bytes() []byte { 62 | return o.attrData 63 | } 64 | 65 | // newSpeedAttribute returns a new attribute from speedCategory 66 | func (o *defaultAttrBuilder) newSpeedAttribute() (Attribute, error) { 67 | var err error 68 | var speedBytes []byte 69 | switch { 70 | case o.stringHasBeenSet: 71 | speedBytes, err = SpeedStringToBytes(o.stringPayload) 72 | if err != nil { 73 | return nil, err 74 | } 75 | case o.bytesHasBeenSet: 76 | speedBytes = o.bytesPayload 77 | case o.intHasBeenSet: 78 | speedBytes, err = SpeedStringToBytes(strconv.Itoa(int(o.intPayload)) + megaBitPerSecondSuffix) 79 | if err != nil { 80 | return nil, err 81 | } 82 | default: 83 | return nil, fmt.Errorf("cannot build, no attribute payload found for category %s attribute", attrCategoryString[speedCategory]) 84 | } 85 | 86 | a := &speedAttribute{ 87 | attrType: o.attrType, 88 | attrData: speedBytes, 89 | } 90 | 91 | err = a.Validate() 92 | if err != nil { 93 | return nil, err 94 | } 95 | 96 | return a, nil 97 | } 98 | 99 | // SpeedBytesToString takes an input speed in wire format, 100 | // returns a string. 101 | // 102 | // {0,0,0,0} -> "Auto" 103 | // 104 | // {0,0,0,1} -> 10Mb/s 105 | // 106 | // {0,0,0,4} -> 10Gb/s 107 | func SpeedBytesToString(b []byte) string { 108 | // Default speed units is "Mb/s". Value of speedVal is logarithmic. 109 | // With large values we switch units and decrement the value accordingly. 110 | speedVal := binary.BigEndian.Uint32(b) 111 | var speedUnits string 112 | switch { 113 | case speedVal >= 3 && speedVal < 6: 114 | speedUnits = "Gb/s" 115 | speedVal -= 3 116 | case speedVal >= 6: 117 | speedUnits = "Tb/s" 118 | speedVal -= 6 119 | default: 120 | speedUnits = "Mb/s" 121 | } 122 | 123 | return strconv.Itoa(int(math.Pow(10, float64(speedVal)))) + speedUnits 124 | } 125 | 126 | // SpeedStringToBytes takes an input string, returns 127 | // speed as an Uint32 expressed in Mb/s 128 | // 129 | // Auto -> 0 130 | // 131 | // 10Mb/s -> 10 132 | // 133 | // 10Gb/s -> 10000 134 | func SpeedStringToBytes(s string) ([]byte, error) { 135 | // Did we get valid characters? 136 | for _, v := range s { 137 | if v > unicode.MaxASCII || !unicode.IsPrint(rune(v)) { 138 | return nil, errors.New("string contains invalid characters") 139 | } 140 | } 141 | 142 | // normalize instring 143 | s = strings.TrimSpace(strings.ToLower(s)) 144 | 145 | var suffixToMultiplier = map[string]int{ 146 | "auto": 0, 147 | "mb/s": 1, 148 | "mbps": 1, 149 | "mbs": 1, 150 | "mb": 1, 151 | "gb/s": 1000, 152 | "gbps": 1000, 153 | "gbs": 1000, 154 | "gb": 1000, 155 | "tb/s": 1000000, 156 | "tbps": 1000000, 157 | "tbs": 1000000, 158 | "tb": 1000000, 159 | } 160 | 161 | // Loop over suffixes, see if the supplied string matches one. 162 | for suffix, multiplier := range suffixToMultiplier { 163 | if strings.HasSuffix(s, suffix) { 164 | // Found one! Trim the suffix and whitespace from the string. 165 | trimmed := strings.TrimSpace(strings.TrimSuffix(s, suffix)) 166 | 167 | // If all we got was a suffix (maybe "mb/s", more likely "auto"), 168 | // set the speed value to "0" so we can do math on it later. 169 | if trimmed == "" { 170 | trimmed = "0" 171 | } 172 | 173 | // Now make sure we've only got math-y characters left in the string. 174 | if strings.Trim(trimmed, "0123456789.") != "" { 175 | return nil, fmt.Errorf("cannot parse `%s' as an interface speed", s) 176 | } 177 | 178 | // speedFloat will contain the value previously expressed by 179 | // the passed string, ignoring the suffix: 180 | // 181 | // If "100Gb/s", speedFloat will be float64(100) 182 | speedFloat, err := strconv.ParseFloat(trimmed, 32) 183 | if err != nil { 184 | return nil, err 185 | } 186 | 187 | // speedFloatMbps will contain the value previously expressed by 188 | // the passed string, corrected to account for the suffix: 189 | // 190 | // If "100Gb/s", speedFloat will be float64(100000) 191 | speedFloatMbps := speedFloat * float64(multiplier) 192 | 193 | // speedBytes will contain the value from the passed string 194 | // expressed in wire format. 195 | speedBytes := make([]byte, 4) 196 | binary.BigEndian.PutUint32(speedBytes, uint32(math.Log10(speedFloatMbps))) 197 | 198 | return speedBytes, nil 199 | } 200 | } 201 | 202 | return nil, fmt.Errorf("cannot parse speed string: `%s'", s) 203 | } 204 | -------------------------------------------------------------------------------- /attribute/attr_speed_test.go: -------------------------------------------------------------------------------- 1 | package attribute 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | ) 8 | 9 | func TestSpeedAttribute_String(t *testing.T) { 10 | var ( 11 | speedStringTestData = map[string][]byte{ 12 | "Auto": []byte{0, 0, 0, 0}, 13 | "10Mb/s": []byte{0, 0, 0, 1}, 14 | "100Mb/s": []byte{0, 0, 0, 2}, 15 | "1Gb/s": []byte{0, 0, 0, 3}, 16 | "10Gb/s": []byte{0, 0, 0, 4}, 17 | "100Gb/s": []byte{0, 0, 0, 5}, 18 | "1Tb/s": []byte{0, 0, 0, 6}, 19 | "10Tb/s": []byte{0, 0, 0, 7}, 20 | "100Tb/s": []byte{0, 0, 0, 8}, 21 | } 22 | ) 23 | 24 | for _, speedAttrType := range getAttrsByCategory(speedCategory) { 25 | for expected, data := range speedStringTestData { 26 | testAttr := speedAttribute{ 27 | attrType: speedAttrType, 28 | attrData: data, 29 | } 30 | result := testAttr.String() 31 | if result != expected { 32 | t.Fatalf("expected %s, got %s", expected, result) 33 | } 34 | } 35 | } 36 | } 37 | 38 | func TestSpeedAttribute_Validate_WithGoodData(t *testing.T) { 39 | goodData := [][]byte{ 40 | []byte{0, 0, 0, 0}, 41 | []byte{0, 0, 0, 1}, 42 | []byte{0, 0, 0, 2}, 43 | []byte{0, 0, 0, 3}, 44 | []byte{0, 0, 0, 4}, 45 | []byte{0, 0, 0, 5}, 46 | []byte{0, 0, 0, 6}, 47 | []byte{0, 0, 0, 7}, 48 | []byte{0, 0, 0, 8}, 49 | } 50 | 51 | for _, speedAttrType := range getAttrsByCategory(speedCategory) { 52 | for _, testData := range goodData { 53 | testAttr := speedAttribute{ 54 | attrType: speedAttrType, 55 | attrData: testData, 56 | } 57 | err := testAttr.Validate() 58 | if err != nil { 59 | t.Fatalf(err.Error()+"\n"+"Supposed good data %s produced error for %s.", 60 | fmt.Sprintf("%v", []byte(testAttr.attrData)), AttrTypeString[speedAttrType]) 61 | } 62 | } 63 | } 64 | } 65 | 66 | func TestSpeedAttribute_Validate_WithBadData(t *testing.T) { 67 | goodData := [][]byte{ 68 | nil, 69 | []byte{}, 70 | []byte{0, 0}, 71 | []byte{0, 0, 0}, 72 | []byte{0, 0, 0, 9}, 73 | []byte{0, 0, 0, 50}, 74 | []byte{0, 0, 1, 0}, 75 | []byte{0, 1, 0, 0}, 76 | []byte{1, 0, 0, 0}, 77 | []byte{255, 255, 255, 255}, 78 | []byte{0, 0, 0, 0, 0}, 79 | } 80 | 81 | for _, speedAttrType := range getAttrsByCategory(speedCategory) { 82 | for _, testData := range goodData { 83 | testAttr := speedAttribute{ 84 | attrType: speedAttrType, 85 | attrData: testData, 86 | } 87 | 88 | err := testAttr.Validate() 89 | if err == nil { 90 | t.Fatalf("Bad data %s in %s did not error.", 91 | fmt.Sprintf("%v", []byte(testAttr.attrData)), AttrTypeString[speedAttrType]) 92 | } 93 | } 94 | } 95 | } 96 | 97 | func TestNewAttrBuilder_Speed_String(t *testing.T) { 98 | testData := map[string]([]byte){ 99 | "auto": []byte{0, 0, 0, 0}, 100 | "Auto": []byte{0, 0, 0, 0}, 101 | "AUTO": []byte{0, 0, 0, 0}, 102 | "mb": []byte{0, 0, 0, 0}, 103 | "gb/s": []byte{0, 0, 0, 0}, 104 | 105 | "10mb": []byte{0, 0, 0, 1}, 106 | "10mbs": []byte{0, 0, 0, 1}, 107 | "10mbps": []byte{0, 0, 0, 1}, 108 | "10mb/s": []byte{0, 0, 0, 1}, 109 | 110 | "100mb": []byte{0, 0, 0, 2}, 111 | "100mbs": []byte{0, 0, 0, 2}, 112 | "100mbps": []byte{0, 0, 0, 2}, 113 | "100mb/s": []byte{0, 0, 0, 2}, 114 | 115 | "1000mb": []byte{0, 0, 0, 3}, 116 | "1000mbs": []byte{0, 0, 0, 3}, 117 | "1000mbps": []byte{0, 0, 0, 3}, 118 | "1000mb/s": []byte{0, 0, 0, 3}, 119 | "1gb": []byte{0, 0, 0, 3}, 120 | "1gbs": []byte{0, 0, 0, 3}, 121 | "1gbps": []byte{0, 0, 0, 3}, 122 | "1gb/s": []byte{0, 0, 0, 3}, 123 | 124 | "2500mb": []byte{0, 0, 0, 3}, 125 | "2500mbs": []byte{0, 0, 0, 3}, 126 | "2500mbps": []byte{0, 0, 0, 3}, 127 | "2500mb/s": []byte{0, 0, 0, 3}, 128 | "2.5gb": []byte{0, 0, 0, 3}, 129 | "2.5gbs": []byte{0, 0, 0, 3}, 130 | "2.5gbps": []byte{0, 0, 0, 3}, 131 | "2.5gb/s": []byte{0, 0, 0, 3}, 132 | 133 | "5000mb": []byte{0, 0, 0, 3}, 134 | "5000mbs": []byte{0, 0, 0, 3}, 135 | "5000mbps": []byte{0, 0, 0, 3}, 136 | "5000mb/s": []byte{0, 0, 0, 3}, 137 | "5gb": []byte{0, 0, 0, 3}, 138 | "5gbs": []byte{0, 0, 0, 3}, 139 | "5gbps": []byte{0, 0, 0, 3}, 140 | "5gb/s": []byte{0, 0, 0, 3}, 141 | 142 | "10000mb": []byte{0, 0, 0, 4}, 143 | "10000mbs": []byte{0, 0, 0, 4}, 144 | "10000mbps": []byte{0, 0, 0, 4}, 145 | "10000mb/s": []byte{0, 0, 0, 4}, 146 | "10gb": []byte{0, 0, 0, 4}, 147 | "10gbs": []byte{0, 0, 0, 4}, 148 | "10gbps": []byte{0, 0, 0, 4}, 149 | "10gb/s": []byte{0, 0, 0, 4}, 150 | 151 | "25000mb": []byte{0, 0, 0, 4}, 152 | "25000mbs": []byte{0, 0, 0, 4}, 153 | "25000mbps": []byte{0, 0, 0, 4}, 154 | "25000mb/s": []byte{0, 0, 0, 4}, 155 | "25gb": []byte{0, 0, 0, 4}, 156 | "25gbs": []byte{0, 0, 0, 4}, 157 | "25gbps": []byte{0, 0, 0, 4}, 158 | "25gb/s": []byte{0, 0, 0, 4}, 159 | 160 | "50000mb": []byte{0, 0, 0, 4}, 161 | "50000mbs": []byte{0, 0, 0, 4}, 162 | "50000mbps": []byte{0, 0, 0, 4}, 163 | "50000mb/s": []byte{0, 0, 0, 4}, 164 | "50gb": []byte{0, 0, 0, 4}, 165 | "50gbs": []byte{0, 0, 0, 4}, 166 | "50gbps": []byte{0, 0, 0, 4}, 167 | "50gb/s": []byte{0, 0, 0, 4}, 168 | 169 | "100000mb": []byte{0, 0, 0, 5}, 170 | "100000mbs": []byte{0, 0, 0, 5}, 171 | "100000mbps": []byte{0, 0, 0, 5}, 172 | "100000mb/s": []byte{0, 0, 0, 5}, 173 | "100gb": []byte{0, 0, 0, 5}, 174 | "100gbs": []byte{0, 0, 0, 5}, 175 | "100gbps": []byte{0, 0, 0, 5}, 176 | "100gb/s": []byte{0, 0, 0, 5}, 177 | 178 | "200000mb": []byte{0, 0, 0, 5}, 179 | "200000mbs": []byte{0, 0, 0, 5}, 180 | "200000mbps": []byte{0, 0, 0, 5}, 181 | "200000mb/s": []byte{0, 0, 0, 5}, 182 | "200gb": []byte{0, 0, 0, 5}, 183 | "200gbs": []byte{0, 0, 0, 5}, 184 | "200gbps": []byte{0, 0, 0, 5}, 185 | "200gb/s": []byte{0, 0, 0, 5}, 186 | 187 | "400000mb": []byte{0, 0, 0, 5}, 188 | "400000mbs": []byte{0, 0, 0, 5}, 189 | "400000mbps": []byte{0, 0, 0, 5}, 190 | "400000mb/s": []byte{0, 0, 0, 5}, 191 | "400gb": []byte{0, 0, 0, 5}, 192 | "400gbs": []byte{0, 0, 0, 5}, 193 | "400gbps": []byte{0, 0, 0, 5}, 194 | "400gb/s": []byte{0, 0, 0, 5}, 195 | } 196 | 197 | for _, speedAttrType := range getAttrsByCategory(speedCategory) { 198 | for s, b := range testData { 199 | testAttr, err := NewAttrBuilder().SetType(speedAttrType).SetString(s).Build() 200 | if err != nil { 201 | t.Fatal(err) 202 | } 203 | if bytes.Compare(testAttr.Bytes(), b) != 0 { 204 | t.Fatalf("speed attribute failed Bytes Comparison: %s vs %s", testAttr.Bytes(), b) 205 | } 206 | expected := append([]byte{byte(speedAttrType), 6}, b...) 207 | if bytes.Compare(MarshalAttribute(testAttr), expected) != 0 { 208 | t.Fatalf("speed attribute failed Marshal Comparison: %s vs %s", MarshalAttribute(testAttr), expected) 209 | } 210 | } 211 | } 212 | } 213 | 214 | func TestNewAttrBuilder_Speed_Int(t *testing.T) { 215 | testData := map[uint32]([]byte){ 216 | 0: []byte{0, 0, 0, 0}, 217 | 10: []byte{0, 0, 0, 1}, 218 | 100: []byte{0, 0, 0, 2}, 219 | 1000: []byte{0, 0, 0, 3}, 220 | 2500: []byte{0, 0, 0, 3}, 221 | 5000: []byte{0, 0, 0, 3}, 222 | 10000: []byte{0, 0, 0, 4}, 223 | 25000: []byte{0, 0, 0, 4}, 224 | 50000: []byte{0, 0, 0, 4}, 225 | 100000: []byte{0, 0, 0, 5}, 226 | 400000: []byte{0, 0, 0, 5}, 227 | } 228 | 229 | for _, speedAttrType := range getAttrsByCategory(speedCategory) { 230 | for i, b := range testData { 231 | testAttr, err := NewAttrBuilder().SetType(speedAttrType).SetInt(i).Build() 232 | if err != nil { 233 | t.Fatal(err) 234 | } 235 | if bytes.Compare(testAttr.Bytes(), b) != 0 { 236 | t.Fatalf("speed attribute failed Bytes Comparison: %s vs %s", testAttr.Bytes(), b) 237 | } 238 | expected := append([]byte{byte(speedAttrType), 6}, b...) 239 | if bytes.Compare(MarshalAttribute(testAttr), expected) != 0 { 240 | t.Fatalf("speed attribute failed Marshal Comparison: %s vs %s", MarshalAttribute(testAttr), expected) 241 | } 242 | } 243 | } 244 | } 245 | 246 | func TestNewAttrBuilder_Speed_Bytes(t *testing.T) { 247 | testData := [][]byte{ 248 | []byte{0, 0, 0, 0}, 249 | []byte{0, 0, 0, 1}, 250 | []byte{0, 0, 0, 2}, 251 | []byte{0, 0, 0, 3}, 252 | []byte{0, 0, 0, 4}, 253 | []byte{0, 0, 0, 5}, 254 | []byte{0, 0, 0, 6}, 255 | []byte{0, 0, 0, 7}, 256 | []byte{0, 0, 0, 8}, 257 | } 258 | 259 | for _, speedAttrType := range getAttrsByCategory(speedCategory) { 260 | for _, b := range testData { 261 | testAttr, err := NewAttrBuilder().SetType(speedAttrType).SetBytes(b).Build() 262 | if err != nil { 263 | t.Fatal(err) 264 | } 265 | if bytes.Compare(testAttr.Bytes(), b) != 0 { 266 | t.Fatalf("speed attribute failed Bytes Comparison: %s vs %s", testAttr.Bytes(), b) 267 | } 268 | expected := append([]byte{byte(speedAttrType), 6}, b...) 269 | if bytes.Compare(MarshalAttribute(testAttr), expected) != 0 { 270 | t.Fatalf("speed attribute failed Marshal Comparison: %s vs %s", MarshalAttribute(testAttr), expected) 271 | } 272 | } 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /attribute/attr_string.go: -------------------------------------------------------------------------------- 1 | package attribute 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "math" 7 | "strconv" 8 | "strings" 9 | "unicode" 10 | ) 11 | 12 | const ( 13 | stringTerminator = byte('\x00') // strings are null-teriminated 14 | maxStringWithoutTerminator = 252 15 | ) 16 | 17 | type stringAttribute struct { 18 | attrType AttrType 19 | attrData []byte 20 | } 21 | 22 | func (o stringAttribute) Type() AttrType { 23 | return o.attrType 24 | } 25 | 26 | func (o stringAttribute) Len() uint8 { 27 | return uint8(TLsize + len(o.attrData)) 28 | } 29 | 30 | func (o stringAttribute) String() string { 31 | return string(o.attrData[:len(o.attrData)-1]) 32 | } 33 | 34 | func (o stringAttribute) Validate() error { 35 | err := checkTypeLen(o, stringCategory) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | // Underlength? 41 | if int(o.Len()) < TLsize+len(string(stringTerminator)) { 42 | return fmt.Errorf("underlength string: got %d bytes (min %d)", o.Len(), TLsize+len(string(stringTerminator))) 43 | } 44 | 45 | // Overlength? 46 | if o.Len() > math.MaxUint8 { 47 | return fmt.Errorf("overlength string: got %d bytes (max %d)", o.Len(), math.MaxUint8) 48 | } 49 | 50 | // Ends with string terminator? 51 | if !strings.HasSuffix(string(o.attrData), string(stringTerminator)) { 52 | return fmt.Errorf("string missing termination character ` %#x'", string(stringTerminator)) 53 | 54 | } 55 | 56 | // Printable? 57 | for _, v := range o.attrData[:(len(o.attrData) - 1)] { 58 | if v > unicode.MaxASCII || !unicode.IsPrint(rune(v)) { 59 | return errors.New("string is not printable.") 60 | } 61 | } 62 | 63 | return nil 64 | } 65 | 66 | func (o stringAttribute) Bytes() []byte { 67 | return o.attrData 68 | } 69 | 70 | // newsStringAttribute returns a new attribute from stringCategory 71 | func (o *defaultAttrBuilder) newStringAttribute() (Attribute, error) { 72 | var stringBytes []byte 73 | switch { 74 | case o.bytesHasBeenSet: 75 | stringBytes = o.bytesPayload 76 | case o.intHasBeenSet: 77 | stringBytes = []byte(strconv.Itoa(int(o.intPayload)) + string(stringTerminator)) 78 | case o.stringHasBeenSet: 79 | stringBytes = []byte(o.stringPayload + string(stringTerminator)) 80 | default: 81 | return nil, fmt.Errorf("cannot build, no attribute payload found for category %s attribute", attrCategoryString[stringCategory]) 82 | } 83 | 84 | a := &stringAttribute{ 85 | attrType: o.attrType, 86 | attrData: stringBytes, 87 | } 88 | 89 | err := a.Validate() 90 | if err != nil { 91 | return nil, err 92 | } 93 | 94 | return a, nil 95 | } 96 | -------------------------------------------------------------------------------- /attribute/attr_string_test.go: -------------------------------------------------------------------------------- 1 | package attribute 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "math/rand" 7 | "testing" 8 | "time" 9 | "unicode" 10 | ) 11 | 12 | func TestStringAttribute_String(t *testing.T) { 13 | // build the character set to be used when generating strings 14 | var stringRunes []rune 15 | for i := 0; i <= unicode.MaxASCII; i++ { 16 | if unicode.IsPrint(rune(i)) { 17 | stringRunes = append(stringRunes, rune(i)) 18 | } 19 | } 20 | 21 | // stringStringTestData map contains test attribute data 22 | // ([]byte{"f","o","o",stringTerminator}) and the expected 23 | // Attribute.String() result ("foo") 24 | stringStringTestData := make(map[string]string) 25 | 26 | // first string to test is empty string 27 | stringStringTestData[string(stringTerminator)] = string("") 28 | 29 | // next string to test is maximum size random string 30 | runeSlice := make([]rune, maxStringWithoutTerminator) 31 | for c := range runeSlice { 32 | runeSlice[c] = stringRunes[rand.Intn(len(stringRunes))] 33 | } 34 | stringStringTestData[string(runeSlice)+string(stringTerminator)] = string(runeSlice) 35 | 36 | // Let's add 98 more random strings of random length 37 | rand.Seed(time.Now().UnixNano()) 38 | for i := 1; i <= 98; i++ { 39 | strlen := rand.Intn(maxStringWithoutTerminator) 40 | runeSlice := make([]rune, strlen) 41 | // make a slice of random "stringy" runes, "i" bytes long 42 | for c := range runeSlice { 43 | runeSlice[c] = stringRunes[rand.Intn(len(stringRunes))] 44 | } 45 | stringStringTestData[string(runeSlice)+string(stringTerminator)] = string(runeSlice) 46 | } 47 | 48 | for _, stringAttrType := range getAttrsByCategory(stringCategory) { 49 | for data, expected := range stringStringTestData { 50 | testAttr := stringAttribute{ 51 | attrType: stringAttrType, 52 | attrData: []byte(data), 53 | } 54 | result := testAttr.String() 55 | if result != expected { 56 | t.Fatalf("expected %s, got %s", expected, result) 57 | } 58 | } 59 | } 60 | } 61 | 62 | func TestStringAttribute_Validate_WithGoodData(t *testing.T) { 63 | // build the character set to be used when generating strings 64 | var stringRunes []rune 65 | for i := 0; i <= unicode.MaxASCII; i++ { 66 | if unicode.IsPrint(rune(i)) { 67 | stringRunes = append(stringRunes, rune(i)) 68 | } 69 | } 70 | 71 | // first example of good data is an empy string (terminator only) 72 | goodData := [][]byte{[]byte{stringTerminator}} 73 | 74 | // Now lets build 3 goodData entries for each allowed character. 75 | // Lengths will be 1, , and maxStringWithoutTerminator. 76 | for _, c := range stringRunes { 77 | testData := make([]byte, maxStringWithoutTerminator) 78 | for i := 0; i < maxStringWithoutTerminator; i++ { 79 | testData[i] = byte(c) 80 | } 81 | short := 1 82 | medium := rand.Intn(maxStringWithoutTerminator) 83 | //medium := 5 84 | long := maxStringWithoutTerminator 85 | 86 | var addMe string 87 | 88 | addMe = string(testData[0:short]) + string(stringTerminator) 89 | goodData = append(goodData, []byte(addMe)) 90 | addMe = string(testData[0:medium]) + string(stringTerminator) 91 | goodData = append(goodData, []byte(addMe)) 92 | addMe = string(testData[0:long]) + string(stringTerminator) 93 | goodData = append(goodData, []byte(addMe)) 94 | } 95 | 96 | for _, stringAttrType := range getAttrsByCategory(stringCategory) { 97 | for _, testData := range goodData { 98 | testAttr := stringAttribute{ 99 | attrType: stringAttrType, 100 | attrData: testData, 101 | } 102 | err := testAttr.Validate() 103 | if err != nil { 104 | t.Fatalf(err.Error()+"\n"+"Supposed good data %s produced error for %s.", 105 | fmt.Sprintf("%v", []byte(testAttr.attrData)), AttrTypeString[stringAttrType]) 106 | } 107 | } 108 | } 109 | } 110 | 111 | func TestStringAttribute_Validate_WithBadData(t *testing.T) { 112 | // build the character set to be used when generating bogus strings 113 | var badStringRunes []rune 114 | for i := 0; i <= unicode.MaxASCII; i++ { 115 | if !unicode.IsPrint(rune(i)) { 116 | badStringRunes = append(badStringRunes, rune(i)) 117 | } 118 | } 119 | 120 | badData := [][]byte{ 121 | nil, // unterminated 122 | []byte{}, // unterminated 123 | []byte{65}, // unterminated 124 | []byte{65, 0, 0}, //embedded terminator 125 | []byte{65, 0, 65}, //embedded terminator 126 | []byte{65, 0, 65, 0}, //embedded terminator 127 | } 128 | 129 | // Add some properly terminated strings containing bogus characters 130 | for _, bsr := range badStringRunes { 131 | badString := []byte(string(bsr) + string(stringTerminator)) 132 | badData = append(badData, badString) 133 | } 134 | 135 | for _, stringAttrType := range getAttrsByCategory(stringCategory) { 136 | for _, testData := range badData { 137 | testAttr := stringAttribute{ 138 | attrType: stringAttrType, 139 | attrData: testData, 140 | } 141 | 142 | err := testAttr.Validate() 143 | if err == nil { 144 | t.Fatalf("Bad data %s in %s did not error.", 145 | fmt.Sprintf("%v", []byte(testAttr.attrData)), AttrTypeString[stringAttrType]) 146 | } 147 | } 148 | } 149 | } 150 | 151 | func TestNewAttrBuilder_String(t *testing.T) { 152 | intData := uint32(1234567890) 153 | stringData := "1234567890" 154 | byteData := []byte{49, 50, 51, 52, 53, 54, 55, 56, 57, 48, 00} 155 | for _, stringAttrType := range getAttrsByCategory(stringCategory) { 156 | expected := []byte{byte(stringAttrType), 13, 49, 50, 51, 52, 53, 54, 55, 56, 57, 48, 00} 157 | byInt, err := NewAttrBuilder().SetType(stringAttrType).SetInt(intData).Build() 158 | if err != nil { 159 | t.Fatal(err) 160 | } 161 | byString, err := NewAttrBuilder().SetType(stringAttrType).SetString(stringData).Build() 162 | if err != nil { 163 | t.Fatal(err) 164 | } 165 | byByte, err := NewAttrBuilder().SetType(stringAttrType).SetBytes(byteData).Build() 166 | if err != nil { 167 | t.Fatal(err) 168 | } 169 | if bytes.Compare(expected, MarshalAttribute(byInt)) != 0 { 170 | t.Fatal("Attributes don't match 1") 171 | } 172 | if bytes.Compare(byInt.Bytes(), byString.Bytes()) != 0 { 173 | t.Fatal("Attributes don't match 2") 174 | } 175 | if bytes.Compare(byString.Bytes(), byByte.Bytes()) != 0 { 176 | t.Fatal("Attributes don't match 3") 177 | } 178 | if bytes.Compare(byByte.Bytes(), byInt.Bytes()) != 0 { 179 | t.Fatal("Attributes don't match 4") 180 | } 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /attribute/attr_vlan.go: -------------------------------------------------------------------------------- 1 | package attribute 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "strconv" 7 | ) 8 | 9 | const ( 10 | minVLAN = 1 11 | maxVLAN = 4094 12 | DefaultVlan = 1 13 | ) 14 | 15 | type vlanAttribute struct { 16 | attrType AttrType 17 | attrData []byte 18 | } 19 | 20 | func (o vlanAttribute) Type() AttrType { 21 | return o.attrType 22 | } 23 | 24 | func (o vlanAttribute) Len() uint8 { 25 | return uint8(TLsize + len(o.attrData)) 26 | } 27 | 28 | func (o vlanAttribute) String() string { 29 | vlan := binary.BigEndian.Uint16(o.attrData[0:2]) 30 | return strconv.Itoa(int(vlan)) 31 | } 32 | 33 | func (o vlanAttribute) Validate() error { 34 | err := checkTypeLen(o, vlanCategory) 35 | if err != nil { 36 | return err 37 | } 38 | 39 | vlan := binary.BigEndian.Uint16(o.attrData) 40 | if vlan > maxVLAN || vlan < minVLAN { 41 | return fmt.Errorf("VLAN %d value out of range", vlan) 42 | } 43 | 44 | return nil 45 | } 46 | 47 | func (o vlanAttribute) Bytes() []byte { 48 | return o.attrData 49 | } 50 | 51 | // newVlanAttribute returns a new attribute from vlanCategory 52 | func (o *defaultAttrBuilder) newVlanAttribute() (Attribute, error) { 53 | var err error 54 | vlanBytes := make([]byte, 2) 55 | switch { 56 | case o.stringHasBeenSet: 57 | vlan, err := strconv.Atoi(o.stringPayload) 58 | if err != nil { 59 | return nil, err 60 | } 61 | binary.BigEndian.PutUint16(vlanBytes, uint16(vlan)) 62 | case o.intHasBeenSet: 63 | vlan := int(o.intPayload) 64 | binary.BigEndian.PutUint16(vlanBytes, uint16(vlan)) 65 | case o.bytesHasBeenSet: 66 | vlanBytes = o.bytesPayload 67 | default: 68 | return nil, fmt.Errorf("cannot build, no attribute payload found for category %s attribute", attrCategoryString[vlanCategory]) 69 | } 70 | 71 | a := &vlanAttribute{ 72 | attrType: o.attrType, 73 | attrData: vlanBytes, 74 | } 75 | 76 | err = a.Validate() 77 | if err != nil { 78 | return nil, err 79 | } 80 | 81 | return a, nil 82 | } 83 | -------------------------------------------------------------------------------- /attribute/attr_vlan_test.go: -------------------------------------------------------------------------------- 1 | package attribute 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "fmt" 7 | "strconv" 8 | "testing" 9 | ) 10 | 11 | func TestVlanAttribute_String(t *testing.T) { 12 | vlanStringTestData := make(map[string][]byte, maxVLAN) 13 | 14 | // fill vlanStringTestData with values like: 15 | // "1025" -> []byte{04, 01} 16 | for i := minVLAN; i <= maxVLAN; i++ { 17 | b := make([]byte, 2) 18 | binary.BigEndian.PutUint16(b, uint16(i)) 19 | vlanStringTestData[strconv.Itoa(i)] = b 20 | } 21 | 22 | _ = vlanStringTestData 23 | 24 | for _, vlanAttrType := range getAttrsByCategory(vlanCategory) { 25 | for expected, data := range vlanStringTestData { 26 | testAttr := vlanAttribute{ 27 | attrType: vlanAttrType, 28 | attrData: data, 29 | } 30 | result := testAttr.String() 31 | if result != expected { 32 | t.Fatalf("expected %s, got %s", expected, result) 33 | } 34 | } 35 | } 36 | } 37 | 38 | func TestVlanAttribute_Validate_WithGoodData(t *testing.T) { 39 | var goodData [][]byte 40 | for i := minVLAN; i <= maxVLAN; i++ { 41 | b := make([]byte, 2) 42 | binary.BigEndian.PutUint16(b, uint16(i)) 43 | goodData = append(goodData, b) 44 | } 45 | 46 | for _, vlanAttrType := range getAttrsByCategory(vlanCategory) { 47 | for _, testData := range goodData { 48 | testAttr := vlanAttribute{ 49 | attrType: vlanAttrType, 50 | attrData: testData, 51 | } 52 | err := testAttr.Validate() 53 | if err != nil { 54 | t.Fatalf(err.Error()+"\n"+"Supposed good data %s produced error for %s.", 55 | fmt.Sprintf("%v", []byte(testAttr.attrData)), AttrTypeString[vlanAttrType]) 56 | } 57 | } 58 | } 59 | } 60 | 61 | func TestVlanAttribute_Validate_WithBadData(t *testing.T) { 62 | badData := [][]byte{ 63 | nil, 64 | []byte{}, 65 | []byte{0}, 66 | []byte{0, 0}, 67 | []byte{255, 255}, 68 | []byte{0, 0, 0}, 69 | } 70 | 71 | for _, vlanAttrType := range getAttrsByCategory(vlanCategory) { 72 | for _, testData := range badData { 73 | testAttr := vlanAttribute{ 74 | attrType: vlanAttrType, 75 | attrData: testData, 76 | } 77 | 78 | err := testAttr.Validate() 79 | if err == nil { 80 | t.Fatalf("Bad data %s in %s did not error.", 81 | fmt.Sprintf("%v", []byte(testAttr.attrData)), AttrTypeString[vlanAttrType]) 82 | } 83 | } 84 | } 85 | } 86 | 87 | func TestNewAttrBuilder_Vlan(t *testing.T) { 88 | for _, vlanAttrType := range getAttrsByCategory(vlanCategory) { 89 | var v uint16 90 | vBytes := make([]byte, 2) 91 | for v = 1; v <= 4094; v++ { 92 | binary.BigEndian.PutUint16(vBytes, v) 93 | expectedMarshal := []byte{byte(vlanAttrType), 4, vBytes[0], vBytes[1]} 94 | byInt, err := NewAttrBuilder().SetType(vlanAttrType).SetInt(uint32(v)).Build() 95 | if err != nil { 96 | t.Fatal(err) 97 | } 98 | byString, err := NewAttrBuilder().SetType(vlanAttrType).SetString(strconv.Itoa(int(v))).Build() 99 | if err != nil { 100 | t.Fatal(err) 101 | } 102 | byByte, err := NewAttrBuilder().SetType(vlanAttrType).SetBytes(vBytes).Build() 103 | if err != nil { 104 | t.Fatal(err) 105 | } 106 | 107 | if bytes.Compare(expectedMarshal, MarshalAttribute(byInt)) != 0 { 108 | t.Fatal("Attributes don't match") 109 | } 110 | if bytes.Compare(byInt.Bytes(), byByte.Bytes()) != 0 { 111 | t.Fatal("Attributes don't match") 112 | } 113 | if bytes.Compare(byByte.Bytes(), byString.Bytes()) != 0 { 114 | t.Fatal("Attributes don't match") 115 | } 116 | if bytes.Compare(byString.Bytes(), byInt.Bytes()) != 0 { 117 | t.Fatal("Attributes don't match") 118 | } 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /attribute/doc.go: -------------------------------------------------------------------------------- 1 | // Package attribute provides functionality for building and parsing 2 | // Cisco Layer 2 Traceroute message attributes. 3 | package attribute 4 | -------------------------------------------------------------------------------- /cmd/debugswitch/README.md: -------------------------------------------------------------------------------- 1 | # debugswitch 2 | An application that connects to a Cisco switch, enables l2t debug logging, 3 | and then prints all debug output. 4 | -------------------------------------------------------------------------------- /cmd/debugswitch/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "golang.org/x/crypto/ssh" 7 | "log" 8 | "os/user" 9 | "time" 10 | 11 | "github.com/chrismarget/cisco-l2t/foozler" 12 | "github.com/stephen-fox/sshutil" 13 | "github.com/stephen-fox/userutil" 14 | ) 15 | 16 | func main() { 17 | address := flag.String("a", "", "The address") 18 | flag.Parse() 19 | 20 | username, err := userutil.GetUserInput("Username", userutil.PromptOptions{ 21 | ShouldHideInput: true, 22 | }) 23 | if err != nil { 24 | log.Fatalln(err.Error()) 25 | } 26 | if len(username) == 0 { 27 | u, err := user.Current() 28 | if err != nil { 29 | log.Fatalln(err.Error()) 30 | } 31 | 32 | username = u.Username 33 | } 34 | 35 | password, err := userutil.GetUserInput("Password", userutil.PromptOptions{ 36 | ShouldHideInput: true, 37 | }) 38 | 39 | onHostKey := func(i sshutil.SSHHostKeyPromptInfo) bool { 40 | b, _ := userutil.GetYesOrNoUserInput(i.UserFacingPrompt, userutil.PromptOptions{}) 41 | return b 42 | } 43 | 44 | clientConfig := &ssh.ClientConfig{ 45 | User: username, 46 | Auth: []ssh.AuthMethod{ 47 | ssh.Password(password), 48 | }, 49 | HostKeyCallback: sshutil.ImitateSSHClientHostKeyCallBack(onHostKey), 50 | Timeout: 10 * time.Second, 51 | } 52 | 53 | clientConfig.Ciphers = append(clientConfig.Ciphers, "aes128-cbc") 54 | 55 | d, err := foozler.ConnectTo(*address, 22, clientConfig) 56 | if err != nil { 57 | log.Fatalln(err.Error()) 58 | } 59 | defer d.Close() 60 | 61 | log.Println("ready") 62 | d.Enable() 63 | 64 | for { 65 | select { 66 | case err := <-d.Wait(): 67 | if err != nil { 68 | log.Fatalf("session ended - %s", err.Error()) 69 | } 70 | return 71 | case s := <-d.Output(): 72 | fmt.Println(s) 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /cmd/enumerate-vlans/enumerate-vlans_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | func TestPrintResults(t *testing.T) { 6 | testData := []int{1,2,3,4} 7 | printResults(testData) 8 | 9 | } 10 | -------------------------------------------------------------------------------- /cmd/enumerate-vlans/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "github.com/cheggaaa/pb/v3" 7 | "github.com/chrismarget/cisco-l2t/attribute" 8 | "github.com/chrismarget/cisco-l2t/message" 9 | "github.com/chrismarget/cisco-l2t/target" 10 | "log" 11 | "net" 12 | "os" 13 | "sort" 14 | ) 15 | 16 | const ( 17 | vlanMin = 1 18 | vlanMax = 4094 19 | ) 20 | 21 | func queryTemplate(t target.Target) (message.Msg, error) { 22 | srcIpAttr, err := attribute.NewAttrBuilder(). 23 | SetType(attribute.SrcIPv4Type). 24 | SetString(t.GetLocalIp().String()). 25 | Build() 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | msg, err := message.TestMsg() 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | msg.SetAttr(srcIpAttr) 36 | return msg, nil 37 | } 38 | 39 | func buildQueries(t target.Target) ([]message.Msg, error) { 40 | var queries []message.Msg 41 | for v := vlanMin; v <= vlanMax; v++ { 42 | template, err := queryTemplate(t) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | vlanAttr, err := attribute.NewAttrBuilder(). 48 | SetType(attribute.VlanType). 49 | SetInt(uint32(v)). 50 | Build() 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | template.SetAttr(vlanAttr) 56 | queries = append(queries, template) 57 | } 58 | return queries, nil 59 | } 60 | 61 | func enumerate_vlans(t target.Target) ([]int, []error, error) { 62 | queries, err := buildQueries(t) 63 | if err != nil { 64 | return nil, nil, err 65 | } 66 | 67 | var found []int 68 | var errors []error 69 | // progress bar and bar channel 70 | bar := pb.StartNew(len(queries)) 71 | pChan := make(chan struct{}) 72 | go func() { 73 | for _ = range pChan { 74 | bar.Increment() 75 | } 76 | }() 77 | 78 | // go do work 79 | result := t.SendBulkUnsafe(queries, pChan) 80 | bar.Finish() 81 | 82 | // how'd we do? 83 | for _, r := range result { 84 | if r.Err != nil { 85 | // Error? This message goes on the doOver slice 86 | errors = append(errors, err) 87 | continue 88 | } 89 | err := r.Msg.Validate() 90 | if err != nil { 91 | // Message validation error? This message goes on the doOver slice 92 | errors = append(errors, err) 93 | continue 94 | } 95 | replyStatus := r.Msg.GetAttr(attribute.ReplyStatusType) 96 | if replyStatus == nil { 97 | // No reply status attribute? This message goes on the doOver slice 98 | errors = append(errors, fmt.Errorf("no reply status: %s", r.Msg.String())) 99 | continue 100 | } 101 | // So far so good. Check to see whether the replyStatus indicates the vlan exists 102 | if replyStatus.String() == attribute.ReplyStatusSrcNotFound { 103 | found = append(found, r.Index+vlanMin) 104 | } 105 | } 106 | 107 | return found, errors, nil 108 | } 109 | 110 | func printResults(found []int) { 111 | sort.Ints(found) 112 | fmt.Printf("\n%d VLANs found:", len(found)) 113 | var somefound bool 114 | if len(found) > 0 { 115 | somefound = true 116 | } 117 | // Pretty print results 118 | var a []int 119 | // iterate over found VLAN numbers 120 | for _, v := range found { 121 | // Not the first one, right? 122 | if len(a) == 0 { 123 | // First VLAN. Initial slice population. 124 | a = append(a, v) 125 | } else { 126 | // Not the first VLAN, do sequential check 127 | if a[len(a)-1]+1 == v { 128 | // this VLAN is the next one in sequence 129 | a = append(a, v) 130 | } else { 131 | // there was a sequence gap, do some printing. 132 | if len(a) == 1 { 133 | // Just one number? Print it. 134 | fmt.Printf(" %d", a[0]) 135 | } else { 136 | // More than one numbers? Print as a range. 137 | fmt.Printf(" %d-%d", a[0], a[len(a)-1]) 138 | } 139 | a = []int{v} 140 | } 141 | } 142 | } 143 | switch { 144 | case len(a) == 1: 145 | // Just one number? Print it. 146 | fmt.Printf(" %d", a[0]) 147 | case len(a) > 1: 148 | // More than one numbers? Print as a range. 149 | fmt.Printf(" %d-%d", a[0], a[len(a)-1]) 150 | } 151 | 152 | if somefound { 153 | fmt.Printf("\n") 154 | } else { 155 | fmt.Printf("\n") 156 | } 157 | 158 | } 159 | 160 | func main() { 161 | flag.Parse() 162 | if flag.NArg() != 1 { 163 | log.Println("You need to specify a target switch") 164 | os.Exit(1) 165 | } 166 | 167 | t, err := target.TargetBuilder(). 168 | AddIp(net.ParseIP(flag.Arg(0))). 169 | Build() 170 | if err != nil { 171 | fmt.Println(err) 172 | os.Exit(2) 173 | } 174 | fmt.Print(t.String(), "\n") 175 | 176 | found, msgerrs, err := enumerate_vlans(t) 177 | if err != nil { 178 | fmt.Println(err) 179 | os.Exit(3) 180 | } 181 | 182 | printResults(found) 183 | if len(msgerrs) != 0 { 184 | log.Println("message errors encountered:") 185 | for _, e := range msgerrs { 186 | log.Println(e) 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /cmd/l2t_ss/README.md: -------------------------------------------------------------------------------- 1 | # l2t_ss 2 | 3 | ## Layer 2 Traceroute Simple Sender 4 | This program sends L2T messages, listens for responses, prints the responses. 5 | 6 | It doesn't use many of the attribute and builder methods available in this 7 | library because they're validated and this sender is deliberately unsafe. 8 | It can generate bogus messages, might be useful for fuzzing the L2T service 9 | on a switch. 10 | 11 | The minimal valid CLI arguments sends a correctly formatted (but invalid) 12 | message to the target switch: 13 | ``` 14 | l2t_ss 15 | ``` 16 | 17 | This message consists of a header only and looks like: 18 | ``` 19 | 0x 02 01 00 05 00 20 | -- -- ----- -- 21 | | | | +---> Attribute Count: 0 22 | | | | 23 | | | +---------> Total Message Length: 5 bytes 24 | | | 25 | | +------------> Message Version: 1 26 | | 27 | +---------------> Message Type: 2 (L2T_REQUEST_SRC) 28 | ``` 29 | 30 | The header values are automatically calculated, but can each be overwritten 31 | with the `-t`, `-l`, `-v` and `-c` options. 32 | 33 | Attributes are added with the `-a` option, which takes one of two forms: 34 | ``` 35 | -a : 36 | -a hexstring 37 | ``` 38 | 39 | The `-a type:` form can be articulated with a type number 40 | (uint8), or a type name (see attribute/attr_common.go). String payloads are 41 | automatically formatted according to the requested type, and attribute 42 | lengths are calculated. 43 | 44 | The `-a hexstring` form is correctly formatted when it includes type, length 45 | and payload. 46 | 47 | A `SrcMacType` payload containing MAC address FF:FF:FF:FF:FF:FF can be 48 | articulated as: 49 | ``` 50 | -a 1:ffff.ffff.ffff 51 | -a SrcMacType:ff:ff:ff:ff:ff:ff 52 | -a 0108ffffffffffff 53 | ``` 54 | 55 | Attributes are packed into outgoing messages in the order they're provided 56 | on the CLI. 57 | 58 | The `-p` option causes the program to attempt to print the outgoing message. 59 | This may or may not be possible, depending on whether the message is valid. 60 | 61 | ## Some complete examples: 62 | 63 | #### Determine whether VLAN 100 exists on a switch: 64 | ``` 65 | $ ./l2t_ss -p -t 2 -a 1:ffff.ffff.ffff -a 2:ffff.ffff.ffff -a 3:100 -a 14: 66 | Sending: L2T_REQUEST_SRC (2) with 4 attributes (31 bytes) 67 | 1 L2_ATTR_SRC_MAC ff:ff:ff:ff:ff:ff 68 | 2 L2_ATTR_DST_MAC ff:ff:ff:ff:ff:ff 69 | 3 L2_ATTR_VLAN 100 70 | 14 L2_ATTR_SRC_IP 71 | Received: L2T_REPLY_SRC (4) with 4 attributes (34 bytes) 72 | 4 L2_ATTR_DEV_NAME <- switch reveals its hostname 73 | 5 L2_ATTR_DEV_TYPE <- switch reveals its platform type 74 | 6 L2_ATTR_DEV_IP <- this might be a new IP address we didn't know about 75 | 15 L2_ATTR_REPLY_STATUS Source Mac address not found <- VLAN 100 exists (would be 'Status unknown (3)' otherwise) 76 | ``` 77 | 78 | #### Determine whether MAC address 0011.2233.4455 exists within VLAN 100: 79 | ``` 80 | $ ./l2t_ss chris$ ./l2t_ss -t 2 -a 1:ffff.ffff.ffff -a 2:0011.2233.4455 -a 3:100 -a 81 | Received: L2T_REPLY_SRC (4) with 6 attributes (62 bytes) 82 | 4 L2_ATTR_DEV_NAME <- switch reveals its hostname 83 | 5 L2_ATTR_DEV_TYPE <- switch reveals its platform type 84 | 6 L2_ATTR_DEV_IP <- this might be a new IP address we didn't know about 85 | 13 L2_ATTR_NBR_IP <- switch reveals CDP management IP of neighbor switch 86 | 15 L2_ATTR_REPLY_STATUS Status unknown (2) <- MAC exists (would be 'Source Mac address not found' otherwise) 87 | 16 L2_ATTR_NBR_DEV_ID <- switch reveals host/domain name of neighbor switch 88 | ``` 89 | 90 | #### Queries of type 1 reveal even more: 91 | ``` 92 | $ ./l2t_ss chris$ ./l2t_ss -t 1 -a 1: -a 2: -a 3:100 -a 14:0.0.0.0 93 | Received: L2T_REPLY_DST (3) with 12 attributes (96 bytes) 94 | 4 L2_ATTR_DEV_NAME <- switch reveals its hostname 95 | 5 L2_ATTR_DEV_TYPE <- switch reveals its platform type 96 | 6 L2_ATTR_DEV_IP <- this might be a new IP address we didn't know about 97 | 7 L2_ATTR_INPORT_NAME Gi1/2 <- interface facing toward attr_type 1 MAC address 98 | 8 L2_ATTR_OUTPORT_NAME Gi1/2 <- interface facing toward attr_type 2 MAC address 99 | 9 L2_ATTR_INPORT_SPEED 1Gb/s <- this is crazy, right? 100 | 10 L2_ATTR_OUTPORT_SPEED 1Gb/s <- these values don't render if speeds are "auto" 101 | 11 L2_ATTR_INPORT_DUPLEX Auto <- duplex command wasn't typed in the config 102 | 12 L2_ATTR_OUTPORT_DUPLEX Auto <- otherwise we'd see 'full' or 'half' 103 | 13 L2_ATTR_NBR_IP 192.168.56.12 <- neighbor switch IP - we should poke this one next! 104 | 15 L2_ATTR_REPLY_STATUS Status unknown (2) <- still working on all the reply status codes 105 | 16 L2_ATTR_NBR_DEV_ID switch3.company.com <- the neighbor's hostname 106 | ``` 107 | -------------------------------------------------------------------------------- /cmd/l2t_ss/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "encoding/hex" 7 | "flag" 8 | "fmt" 9 | "github.com/chrismarget/cisco-l2t/attribute" 10 | "github.com/chrismarget/cisco-l2t/communicate" 11 | "github.com/chrismarget/cisco-l2t/message" 12 | "log" 13 | "net" 14 | "os" 15 | "strings" 16 | "time" 17 | ) 18 | 19 | const ( 20 | attrSep = ":" 21 | defaultVersion = 1 22 | typeFlag = "t" 23 | typeFlagHelp = "message type override: 0 - 255" 24 | verFlag = "v" 25 | verFlagHelp = "message version override: 0 - 255" 26 | lenFlag = "l" 27 | lenFlagHelp = "message length override: 0 - 65535 (default )" 28 | attrCountFlag = "c" 29 | attrCountFlagHelp = "message attribute count override: 0 255 (default )" 30 | attrFlag = "a" 31 | attrFlagHelp = "attribute string form 'type:value' or raw TLV hex string" 32 | printFlag = "p" 33 | printFlagHelp = "attempt to parse/print outbound message (unsafe if sending broken messages)" 34 | rttFlag = "r" 35 | rttFlagHelp = "estimate of round-trip latency in milliseconds (default 500)" 36 | vasiliFlag = "V" 37 | vasiliFlagHelp = "One ping only, Mr. Vasili" 38 | usageTextCmd = "[options] \n" 39 | usageTextExplain = "The following examples both create the same message:\n" + 40 | " -a 2:0004.f284.dbbf -a 1:00:50:56:98:e2:12 -a 3:18 -a 14:192.168.1.2 \n" + 41 | " -t 2 -v 1 -l 31 -c 4 -a 02080004f284dbbf -a 010800505698e212 -a 03040012 -a 0e06c0a80102 " 42 | ) 43 | 44 | type attrStringFlags []string 45 | 46 | func (i *attrStringFlags) String() string { 47 | return "string representation of attrStringFlag" 48 | } 49 | func (i *attrStringFlags) Set(value string) error { 50 | *i = append(*i, value) 51 | return nil 52 | } 53 | 54 | func getAttFromStringOption(in string) (attribute.Attribute, error) { 55 | result := strings.SplitN(in, attrSep, 2) 56 | aType, err := attribute.AttrStringToType(result[0]) 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | att, err := attribute.NewAttrBuilder().SetType(aType).SetString(result[1]).Build() 62 | if err != nil { 63 | return nil, err 64 | } 65 | return att, nil 66 | } 67 | 68 | func getAttBytesFromHexStringOption(in string) ([]byte, error) { 69 | payload := make([]byte, hex.DecodedLen(len(in))) 70 | _, err := hex.Decode(payload, []byte(in)) 71 | if err != nil { 72 | return nil, err 73 | } 74 | return payload, nil 75 | } 76 | 77 | func getAttsBytesFromStrings(in []string) ([]byte, error) { 78 | var result []byte 79 | for _, s := range in { 80 | if strings.Contains(s, attrSep) { 81 | // attribute string option of the form type:value like: 82 | // -a 3:101 (vlan 101) 83 | // -a 2:0000.1111.2222 (destination mac 00:00:11:11:22:22) 84 | // -a L2_ATTR_SRC_MAC:00:00:11:11:22:22 (source mac00:00:11:11:22:22) 85 | att, err := getAttFromStringOption(s) 86 | if err != nil { 87 | return result, err 88 | } 89 | result = append(result, byte(att.Type())) 90 | result = append(result, att.Len()) 91 | result = append(result, att.Bytes()...) 92 | } else { 93 | // attribute string in hex payload TLV form like: 94 | // -a 03040065 (vlan 101 where vlantype=3, len=4, value=0x0065 (101) 95 | b, err := getAttBytesFromHexStringOption(s) 96 | if err != nil { 97 | return result, err 98 | } 99 | result = append(result, b...) 100 | } 101 | } 102 | 103 | return result, nil 104 | } 105 | 106 | func flagProvided(name string) bool { 107 | var found bool 108 | flag.Visit(func(f *flag.Flag) { 109 | if f.Name == name { 110 | found = true 111 | } 112 | }) 113 | return found 114 | } 115 | 116 | func buildMsgBytes(t uint8, v uint8, l uint16, c uint8, asf attrStringFlags) ([]byte, error) { 117 | payload, err := getAttsBytesFromStrings(asf) 118 | if err != nil { 119 | log.Println(err) 120 | os.Exit(11) 121 | } 122 | 123 | bb := bytes.Buffer{} 124 | 125 | if flagProvided(typeFlag) { 126 | _, err = bb.Write([]byte{t}) 127 | } else { 128 | _, err = bb.Write([]byte{uint8(message.RequestDst)}) 129 | } 130 | if err != nil { 131 | log.Println(err) 132 | os.Exit(12) 133 | } 134 | 135 | if flagProvided(verFlag) { 136 | _, err = bb.Write([]byte{v}) 137 | } else { 138 | _, err = bb.Write([]byte{uint8(defaultVersion)}) 139 | } 140 | if err != nil { 141 | log.Println(err) 142 | os.Exit(13) 143 | } 144 | 145 | b := make([]byte, 2) 146 | if flagProvided(lenFlag) { 147 | binary.BigEndian.PutUint16(b, l) 148 | } else { 149 | binary.BigEndian.PutUint16(b, uint16(len(payload)+5)) 150 | } 151 | _, err = bb.Write(b) 152 | if err != nil { 153 | log.Println(err) 154 | os.Exit(14) 155 | } 156 | 157 | if flagProvided(attrCountFlag) { 158 | _, err = bb.Write([]byte{c}) 159 | } else { 160 | _, err = bb.Write([]byte{uint8(len(asf))}) 161 | } 162 | if err != nil { 163 | log.Println(err) 164 | os.Exit(15) 165 | } 166 | 167 | _, err = bb.Write(payload) 168 | if err != nil { 169 | log.Println(err) 170 | os.Exit(16) 171 | } 172 | 173 | return bb.Bytes(), nil 174 | } 175 | 176 | func main() { 177 | var attrStringFlags attrStringFlags 178 | flag.Var(&attrStringFlags, attrFlag, attrFlagHelp) 179 | msgType := flag.Int(typeFlag, int(message.RequestSrc), typeFlagHelp) 180 | msgVer := flag.Int(verFlag, int(message.Version1), verFlagHelp) 181 | msgLen := flag.Int(lenFlag, 0, lenFlagHelp) 182 | msgAC := flag.Int(attrCountFlag, 0, attrCountFlagHelp) 183 | doPrint := flag.Bool(printFlag, false, printFlagHelp) 184 | rttGuess := flag.Int(rttFlag, 500, rttFlagHelp) 185 | vasili := flag.Bool(vasiliFlag, false, vasiliFlagHelp) 186 | 187 | flag.Usage = func() { 188 | _, _ = fmt.Fprintf(flag.CommandLine.Output(), 189 | "\nUsage:\n %s %s\n%s\n\nOptions:\n", 190 | os.Args[0], 191 | usageTextCmd, 192 | usageTextExplain, 193 | ) 194 | flag.PrintDefaults() 195 | } 196 | 197 | flag.Parse() 198 | if flag.NArg() != 1 { 199 | flag.Usage() 200 | os.Exit(1) 201 | } 202 | 203 | payload, err := buildMsgBytes( 204 | uint8(*msgType), 205 | uint8(*msgVer), 206 | uint16(*msgLen), 207 | uint8(*msgAC), 208 | attrStringFlags, 209 | ) 210 | if err != nil { 211 | log.Println(err) 212 | os.Exit(2) 213 | } 214 | 215 | if *doPrint { 216 | outMsg, err := message.UnmarshalMessageUnsafe(payload) 217 | if err != nil { 218 | log.Println(err) 219 | os.Exit(3) 220 | } 221 | 222 | fmt.Printf("Sending: %s\n", outMsg.String()) 223 | for _, a := range attribute.SortAttributes(outMsg.Attributes()) { 224 | fmt.Printf(" %2d %-20s %s\n", a.Type(), attribute.AttrTypeString[a.Type()], a.String()) 225 | } 226 | } 227 | 228 | sendThis := communicate.SendThis{ 229 | Payload: payload, 230 | Destination: &net.UDPAddr{ 231 | IP: net.ParseIP(flag.Arg(0)), 232 | Port: communicate.CiscoL2TPort, 233 | }, 234 | RttGuess: time.Duration(*rttGuess) * time.Millisecond, 235 | Vasili: *vasili, 236 | } 237 | 238 | result := communicate.Communicate(sendThis, nil) 239 | if result.Err != nil { 240 | log.Println(result.Err) 241 | os.Exit(3) 242 | } 243 | 244 | inMsg, err := message.UnmarshalMessage(result.ReplyData) 245 | if err != nil { 246 | log.Println(result.Err) 247 | os.Exit(3) 248 | } 249 | 250 | fmt.Printf("Received: %s\n", inMsg.String()) 251 | for _, a := range attribute.SortAttributes(inMsg.Attributes()) { 252 | fmt.Printf(" %2d %-20s %s\n", a.Type(), attribute.AttrTypeString[a.Type()], a.String()) 253 | } 254 | 255 | os.Exit(0) 256 | } 257 | -------------------------------------------------------------------------------- /cmd/test/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/chrismarget/cisco-l2t/attribute" 6 | "github.com/chrismarget/cisco-l2t/message" 7 | "os" 8 | ) 9 | 10 | // enumerate vlans: 11 | // requestDst 12 | // both MACs set to ffff.ffff.ffff 13 | // iterate over vlans : 3 (no vlan) / 7 (vlan exists) 14 | // 15 | 16 | func main() { 17 | var a attribute.Attribute 18 | var err error 19 | 20 | builder := message.NewMsgBuilder() 21 | builder.SetType(message.RequestSrc) 22 | 23 | //a, err = attribute.NewAttrBuilder().SetType(attribute.SrcMacType).SetString("0030.18a0.1243").Build() 24 | a, err = attribute.NewAttrBuilder().SetType(attribute.SrcMacType).SetString("ffff.ffff.ffff").Build() 25 | if err != nil { 26 | fmt.Println(err) 27 | os.Exit(1) 28 | } 29 | builder.SetAttr(a) 30 | 31 | a, err = attribute.NewAttrBuilder().SetType(attribute.DstMacType).SetString("ffff.ffff.ffff").Build() 32 | if err != nil { 33 | fmt.Println(err) 34 | os.Exit(2) 35 | } 36 | builder.SetAttr(a) 37 | 38 | a, err = attribute.NewAttrBuilder().SetType(attribute.VlanType).SetInt(43).Build() 39 | if err != nil { 40 | fmt.Println(err) 41 | os.Exit(3) 42 | } 43 | builder.SetAttr(a) 44 | 45 | msg := builder.Build() 46 | 47 | err = msg.Validate() 48 | if err != nil { 49 | fmt.Println(err) 50 | os.Exit(7) 51 | } 52 | 53 | //response, respondent, err := communicate.Communicate(msg, &net.UDPAddr{IP: []byte{192, 168, 96, 167}}) 54 | //if err != nil { 55 | // fmt.Println(err) 56 | // os.Exit(8) 57 | //} 58 | // 59 | //log.Println(respondent.IP.String()) 60 | //log.Println(message.MsgTypeToString[response.Type()]) 61 | //for _, a := range response.Attributes() { 62 | // log.Println(attribute.AttrTypePrettyString[a.Type()], a.String()) 63 | //} 64 | } 65 | -------------------------------------------------------------------------------- /communicate/communicate.go: -------------------------------------------------------------------------------- 1 | package communicate 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "strconv" 7 | "time" 8 | ) 9 | 10 | const ( 11 | inBufferSize = 65536 12 | InitialRTTGuess = 250 * time.Millisecond 13 | MaxRTT = 2500 * time.Millisecond 14 | UdpProtocol = "udp4" 15 | CiscoL2TPort = 2228 16 | ) 17 | 18 | type SendResult struct { 19 | Attempts int 20 | Err error 21 | Aborted bool 22 | Rtt time.Duration 23 | SentTo net.IP // the address we tried talking to 24 | SentFrom net.IP // our IP address 25 | ReplyFrom net.IP // the address they replied from 26 | ReplyData []byte 27 | } 28 | 29 | type receiveResult struct { 30 | err error 31 | // Rtt time.Duration 32 | replyFrom net.IP // the address they replied from 33 | replyData []byte 34 | } 35 | 36 | // temporaryErr returns a boolean indicating whether the receiveResult has an error 37 | // of the Timeout variety 38 | func (o receiveResult) temporaryErr() bool { 39 | if o.err != nil { 40 | if result, ok := o.err.(net.Error); ok && result.Temporary() { 41 | return true 42 | } 43 | } 44 | return false 45 | } 46 | 47 | // timeoutErr returns a boolean indicating whether the receiveResult has an error 48 | // of the Timeout variety 49 | func (o receiveResult) timeoutErr() bool { 50 | if o.err != nil { 51 | if result, ok := o.err.(net.Error); ok && result.Timeout() { 52 | return true 53 | } 54 | } 55 | return false 56 | } 57 | 58 | type SendThis struct { 59 | Payload []byte 60 | Destination *net.UDPAddr 61 | ExpectReplyFrom net.IP 62 | RttGuess time.Duration 63 | Vasili bool // one ping only 64 | MaxWait time.Duration 65 | } 66 | 67 | // GetOutgoingIpForDestination returns a net.IP representing the local interface 68 | // that's best suited for talking to the passed target address 69 | func GetOutgoingIpForDestination(t net.IP) (net.IP, error) { 70 | c, err := net.Dial("udp4", t.String()+":1") 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | return c.LocalAddr().(*net.UDPAddr).IP, c.Close() 76 | } 77 | 78 | // getRemote returns the remote *net.UDPAddr associated with a 79 | // connected UDP socket. 80 | func getRemote(in net.UDPConn) (*net.UDPAddr, error) { 81 | if in.RemoteAddr() == nil { 82 | // not a connected socket - un-answerable question 83 | return nil, fmt.Errorf("un-connected socket doesn't have a remote address") 84 | } 85 | 86 | hostString, portString, err := net.SplitHostPort(in.RemoteAddr().String()) 87 | if err != nil { 88 | return &net.UDPAddr{}, err 89 | } 90 | 91 | IP := net.ParseIP(hostString) 92 | if IP == nil { 93 | return &net.UDPAddr{}, fmt.Errorf("could not parse host: %s", hostString) 94 | } 95 | 96 | port, err := strconv.Atoi(portString) 97 | if err != nil { 98 | return &net.UDPAddr{}, err 99 | } 100 | 101 | return &net.UDPAddr{ 102 | IP: IP, 103 | Port: port, 104 | }, nil 105 | } 106 | 107 | // receiveOneMsg loops until a "good" inbound message arrives on the socket, 108 | // or the socket times out. It ignores alien replies (packets not from 109 | // expectedSource) unless expectedSource is . It is guaranteed to 110 | // write to the result channel exactly once. 111 | func receiveOneMsg(cxn *net.UDPConn, expectedSource net.IP, result chan<- receiveResult) { 112 | buffIn := make([]byte, inBufferSize) 113 | var err error 114 | var received int 115 | var respondent *net.UDPAddr 116 | var connected bool 117 | 118 | // pre-load respondent info for the connected socket case, because 119 | // the UDPConn.Read() call doesn't give us this data. 120 | if cxn.RemoteAddr() != nil { // "connected" socket 121 | connected = true 122 | respondent, err = getRemote(*cxn) 123 | if err != nil { 124 | result <- receiveResult{err: err} 125 | return 126 | } 127 | } 128 | 129 | // read until we have some data to return. 130 | for received == 0 { 131 | // read from the socket using the appropriate call 132 | switch connected { 133 | case true: 134 | received, err = cxn.Read(buffIn) 135 | case false: 136 | received, respondent, err = cxn.ReadFromUDP(buffIn) 137 | } 138 | 139 | switch { 140 | case err != nil: 141 | result <- receiveResult{err: err} 142 | return 143 | case received >= len(buffIn): // Unexpectedly large read 144 | result <- receiveResult{err: fmt.Errorf("got full buffer: %d bytes", len(buffIn))} 145 | return 146 | case expectedSource != nil && !expectedSource.Equal(respondent.IP): 147 | // Alien reply. Ignore. 148 | received = 0 149 | } 150 | } 151 | 152 | result <- receiveResult{ 153 | replyFrom: respondent.IP, 154 | replyData: buffIn[:received], 155 | } 156 | } 157 | 158 | // transmit writes bytes to the specified destination using the 159 | // provided *net.UDPConn. 160 | func transmit(cxn *net.UDPConn, destination *net.UDPAddr, payload []byte) error { 161 | var n int 162 | var err error 163 | if cxn.RemoteAddr() != nil { 164 | // connected socket created by net.DialUDP() just call Write() 165 | n, err = cxn.Write(payload) 166 | } else { 167 | // non-connected socket created by net.ListenUDP() 168 | // include the Destination when calling WriteToUDP() 169 | n, err = cxn.WriteToUDP(payload, destination) 170 | } 171 | 172 | pLen := len(payload) 173 | switch { 174 | case err != nil: 175 | return err 176 | case n < pLen: 177 | return fmt.Errorf("short write to socket, only manged %d of %d bytes", n, pLen) 178 | case n > pLen: 179 | return fmt.Errorf("long write to socket, only wanted %d bytes, wrote %d bytes", pLen, n) 180 | } 181 | return nil 182 | } 183 | 184 | // Communicate sends a message via UDP socket, collects a reply. It retransmits 185 | // the message as needed. The input structure's SendThis.ExpectReplyFrom is 186 | // optional. 187 | // 188 | // If SendThis.ExpectReplyFrom is populated and matches 189 | // SendThis.Destination.IP, then a "connected" UDP socket (which can respond to 190 | // incoming ICMP unreachables) is used. 191 | // 192 | // If SendThis.ExpectReplyFrom is populated and doesn't match 193 | // SendThis.Destination.IP, then a "non-connected" (listener) UDP socket is 194 | // used, and replies are filtered so that only datagrams sourced by 195 | // SendThis.ExpectReplyFrom are considered. 196 | // 197 | // if SendThis.ExpectReplyFrom is nil, then a "non-connected" (listener) UDP 198 | // socket is used and incoming datagrams from any source are considered valid 199 | // replies. 200 | // 201 | // Close the quit channel to abort the operation. This channel can be nil if 202 | // no need to abort. The operation is aborted by setting the receive timeout 203 | // to "now". This has a side-effect of returning a timeout error on abort via 204 | // the quit channel. 205 | func Communicate(out SendThis, quit chan struct{}) SendResult { 206 | // determine the local interface IP 207 | ourIp, err := GetOutgoingIpForDestination(out.Destination.IP) 208 | if err != nil { 209 | return SendResult{Err: err} 210 | } 211 | 212 | // create the socket 213 | var cxn *net.UDPConn 214 | switch out.Destination.IP.Equal(out.ExpectReplyFrom) { 215 | case true: 216 | cxn, err = net.DialUDP(UdpProtocol, &net.UDPAddr{IP: ourIp}, out.Destination) 217 | if err != nil { 218 | return SendResult{Err: err} 219 | } 220 | case false: 221 | cxn, err = net.ListenUDP(UdpProtocol, &net.UDPAddr{IP: ourIp}) 222 | if err != nil { 223 | return SendResult{Err: err} 224 | } 225 | } 226 | 227 | replyChan := make(chan receiveResult, 1) 228 | go receiveOneMsg(cxn, out.ExpectReplyFrom, replyChan) 229 | 230 | // socket timeout stuff 231 | var rtt time.Duration 232 | if out.MaxWait > 0 { 233 | rtt = out.MaxWait 234 | } else { 235 | rtt = MaxRTT 236 | } 237 | start := time.Now() 238 | end := start.Add(rtt) 239 | err = cxn.SetReadDeadline(end) 240 | if err != nil { 241 | return SendResult{Err: err} 242 | } 243 | 244 | var outstandingMsgs int 245 | defer func() { 246 | go closeListenerAfterNReplies(cxn, outstandingMsgs, end) 247 | }() 248 | 249 | // retransmit backoff timer tells us when to re-send. Use the supplied 250 | // estimate only if it appears to be grounded in reality. 251 | var bot *BackoffTicker 252 | if out.RttGuess > time.Millisecond { 253 | bot = NewBackoffTicker(out.RttGuess) 254 | } else { 255 | bot = NewBackoffTicker(InitialRTTGuess) 256 | } 257 | defer bot.Stop() 258 | 259 | // keep track of whether the caller aborted us via the quit 260 | // channel 261 | var aborted bool 262 | 263 | // keep sending until... something happens 264 | for { 265 | select { 266 | case <-bot.C: // send again on RTO expiration 267 | if !out.Vasili || outstandingMsgs == 0 { 268 | err := transmit(cxn, out.Destination, out.Payload) 269 | if err != nil { 270 | return SendResult{Err: err} 271 | } 272 | outstandingMsgs++ 273 | } 274 | case result := <-replyChan: // reply or timeout 275 | if !result.timeoutErr() { // are we here because of a reply? 276 | // decrement outstanding counter on inbound reply 277 | outstandingMsgs-- 278 | } 279 | return SendResult{ 280 | Attempts: outstandingMsgs + 1, 281 | Aborted: aborted, 282 | Err: result.err, 283 | Rtt: time.Now().Sub(start), 284 | SentTo: out.Destination.IP, 285 | ReplyFrom: result.replyFrom, 286 | ReplyData: result.replyData, 287 | } 288 | case <-quit: // abort 289 | aborted = true 290 | err := cxn.SetReadDeadline(time.Now()) 291 | if err != nil { 292 | // note that this return happens only if the call to 293 | // SetReadDeadline errored (unlikely). The return on abort 294 | // happens on the next loop iteration via timeout error on 295 | // replyChan. 296 | return SendResult{Err: err} 297 | } 298 | } 299 | } 300 | } 301 | 302 | // closeListenerAfterNReplies closes the specified *net.UDPConn after reading 303 | // the specified number of replies, or reaching the specified deadline 304 | // - whichever happens first. 305 | // 306 | // This allows us to gracefully handle replies to outstanding messages, 307 | // rather than closing the socket, and forcing the operating system to 308 | // send ICMP "f-off" replies. 309 | func closeListenerAfterNReplies(cxn *net.UDPConn, pendingReplies int, deadline time.Time) { 310 | // restore the socket deadline (may have been changed due to abort) 311 | setDeadlineErr := cxn.SetReadDeadline(deadline) 312 | if setDeadlineErr != nil { 313 | timeRemaining := deadline.Sub(time.Now()) 314 | if timeRemaining < 0 { 315 | _ = cxn.Close() 316 | return 317 | } 318 | go func() { 319 | time.Sleep(timeRemaining) 320 | _ = cxn.Close() 321 | }() 322 | } 323 | 324 | isNetDialSocket := cxn.RemoteAddr() != nil 325 | buffIn := make([]byte, inBufferSize) 326 | 327 | // collect pending replies -- we really don't care what happens here. 328 | for i := 0; i < pendingReplies; i++ { 329 | // read from the socket using the appropriate call 330 | // Yes I am not handling the error. 331 | // The only thing that matters is running out the loop. 332 | if isNetDialSocket { 333 | _, _ = cxn.Read(buffIn) 334 | } else { 335 | _, _, _ = cxn.ReadFromUDP(buffIn) 336 | } 337 | } 338 | 339 | _ = cxn.Close() 340 | } 341 | -------------------------------------------------------------------------------- /communicate/communicate_test.go: -------------------------------------------------------------------------------- 1 | package communicate 2 | 3 | import ( 4 | "bytes" 5 | "github.com/chrismarget/cisco-l2t/attribute" 6 | "github.com/chrismarget/cisco-l2t/message" 7 | "log" 8 | "math/rand" 9 | "net" 10 | "strconv" 11 | "strings" 12 | "testing" 13 | "time" 14 | ) 15 | 16 | func TestGetOutgoingIpForDestination(t *testing.T) { 17 | d := net.ParseIP("127.0.0.1") 18 | s, err := GetOutgoingIpForDestination(d) 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | if !d.Equal(s) { 23 | t.Fatalf("addresses don't match: %s and %s", d, s) 24 | } 25 | } 26 | 27 | func TestReplyTimeout(t *testing.T) { 28 | start := time.Now() 29 | bot := NewBackoffTicker(100 * time.Millisecond) 30 | ticks := 0 31 | for ticks < 7 { 32 | select { 33 | case <-bot.C: 34 | } 35 | ticks++ 36 | } 37 | duration := time.Now().Sub(start) 38 | expectedMin := 3100 * time.Millisecond 39 | expectedMax := 3300 * time.Millisecond 40 | if duration < expectedMin { 41 | t.Fatalf("expected this to take about 3200ms, but it took %s", duration) 42 | } 43 | if duration > expectedMax { 44 | t.Fatalf("expected this to take about 3200ms, but it took %s", duration) 45 | } 46 | } 47 | 48 | func TestTransmit(t *testing.T) { 49 | destinationIP := net.ParseIP("192.168.254.254") 50 | 51 | ourIp, err := GetOutgoingIpForDestination(destinationIP) 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | 56 | cxn, err := net.ListenUDP(UdpProtocol, &net.UDPAddr{IP: ourIp}) 57 | if err != nil { 58 | t.Fatal(err) 59 | } 60 | 61 | destination := net.UDPAddr{ 62 | IP: destinationIP, 63 | Port: 2228, 64 | } 65 | 66 | testData := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} 67 | 68 | err = transmit(cxn, &destination, testData) 69 | if err != nil { 70 | t.Fatal(err) 71 | } 72 | 73 | err = cxn.Close() 74 | if err != nil { 75 | t.Fatal(err) 76 | } 77 | } 78 | 79 | func TestReceive(t *testing.T) { 80 | ip := net.ParseIP("127.0.0.1") 81 | 82 | listenSock, err := net.ListenUDP(UdpProtocol, &net.UDPAddr{IP: ip}) 83 | if err != nil { 84 | t.Fatal(err) 85 | } 86 | 87 | destPort, err := strconv.Atoi(strings.Split(listenSock.LocalAddr().String(), ":")[1]) 88 | if err != nil { 89 | t.Fatal(err) 90 | } 91 | 92 | destination := net.UDPAddr{ 93 | IP: ip, 94 | Port: destPort, 95 | } 96 | 97 | sendSock, err := net.ListenUDP(UdpProtocol, nil) 98 | if err != nil { 99 | t.Fatal(err) 100 | } 101 | 102 | replyChan := make(chan receiveResult, 1) 103 | go receiveOneMsg(listenSock, ip, replyChan) 104 | 105 | testData := make([]byte, 25) 106 | _, err = rand.Read(testData) 107 | if err != nil { 108 | t.Fatal(err) 109 | } 110 | 111 | err = transmit(sendSock, &destination, testData) 112 | if err != nil { 113 | t.Fatal(err) 114 | } 115 | 116 | result := <-replyChan 117 | if result.err != nil { 118 | t.Fatal(result.err) 119 | } 120 | 121 | if !bytes.Equal(testData, result.replyData) { 122 | log.Fatalf("received data doesn't match sent data") 123 | } 124 | 125 | log.Println(testData) 126 | log.Println(result.replyData) 127 | 128 | } 129 | 130 | func TestCommunicateQuit(t *testing.T) { 131 | start := time.Now() 132 | destination := net.UDPAddr{ 133 | IP: net.ParseIP("192.168.254.254"), 134 | Port: 2228, 135 | } 136 | 137 | ourIp, err := GetOutgoingIpForDestination(destination.IP) 138 | if err != nil { 139 | t.Fatal(err) 140 | } 141 | 142 | ourIpAttr, err := attribute.NewAttrBuilder(). 143 | SetType(attribute.SrcIPv4Type). 144 | SetString(ourIp.String()). 145 | Build() 146 | if err != nil { 147 | t.Fatal(err) 148 | } 149 | 150 | testMsg, err := message.TestMsg() 151 | if err != nil { 152 | t.Fatal(err) 153 | } 154 | 155 | err = testMsg.Validate() 156 | if err != nil { 157 | t.Fatal(err) 158 | } 159 | 160 | payload := testMsg.Marshal([]attribute.Attribute{ourIpAttr}) 161 | 162 | out := SendThis{ 163 | Payload: payload, 164 | Destination: &destination, 165 | ExpectReplyFrom: destination.IP, 166 | } 167 | 168 | quit := make(chan struct{}) 169 | limit := 500 * time.Millisecond 170 | 171 | go func() { 172 | time.Sleep((limit / 10) * 9) 173 | close(quit) 174 | }() 175 | 176 | in := Communicate(out, quit) 177 | duration := time.Now().Sub(start) 178 | 179 | if in.Err != nil { 180 | // timeout errors are the kind we want here. 181 | if result, ok := in.Err.(net.Error); ok && result.Timeout(){ 182 | log.Println("We expected this timeout: ",in.Err) 183 | } else { 184 | t.Fatal(in.Err) 185 | } 186 | } 187 | 188 | if !in.Aborted { 189 | t.Fatalf("return doesn't indicate abort") 190 | } 191 | 192 | if duration < limit { 193 | log.Println("overall execution time okay") 194 | } else { 195 | t.Fatalf("Read should have completed in about %s, took longer than %s.", ((limit / 10) * 9), limit) 196 | } 197 | } 198 | 199 | func TestGoAwayBostonDial(t *testing.T) { 200 | destination := net.UDPAddr{ 201 | IP: net.ParseIP("10.201.12.66"), 202 | Port: 2228, 203 | } 204 | 205 | ourIp, err := GetOutgoingIpForDestination(destination.IP) 206 | if err != nil { 207 | t.Fatal(err) 208 | } 209 | 210 | ourIpAttr, err := attribute.NewAttrBuilder(). 211 | SetType(attribute.SrcIPv4Type). 212 | SetString(ourIp.String()). 213 | Build() 214 | if err != nil { 215 | t.Fatal(err) 216 | } 217 | 218 | testMsg, err := message.TestMsg() 219 | if err != nil { 220 | t.Fatal(err) 221 | } 222 | 223 | err = testMsg.Validate() 224 | if err != nil { 225 | t.Fatal(err) 226 | } 227 | 228 | payload :=testMsg.Marshal([]attribute.Attribute{ourIpAttr}) 229 | 230 | out := SendThis{ 231 | Payload: payload, 232 | Destination: &destination, 233 | ExpectReplyFrom: destination.IP, 234 | } 235 | 236 | in := Communicate(out, nil) 237 | if in.Err != nil { 238 | if !strings.Contains(in.Err.Error(), "connection refused") { 239 | t.Fatal(in.Err) 240 | } 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /communicate/doc.go: -------------------------------------------------------------------------------- 1 | // Package communicate provides general tooling for communicating with 2 | // UDP listeners. 3 | package communicate 4 | -------------------------------------------------------------------------------- /communicate/ticker.go: -------------------------------------------------------------------------------- 1 | package communicate 2 | 3 | import ( 4 | "math" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | type BackoffTicker struct { 10 | ticker time.Ticker 11 | C chan time.Time 12 | stopChan chan struct{} 13 | } 14 | 15 | func (o *BackoffTicker) Stop() { 16 | close(o.stopChan) 17 | } 18 | 19 | func NewBackoffTicker(d time.Duration) *BackoffTicker { 20 | tockChan := make(chan time.Time, 1) 21 | tockChan <- time.Now() 22 | wg := &sync.WaitGroup{} 23 | wg.Add(1) 24 | stopChan := make(chan struct{}) 25 | 26 | go func() { 27 | wg.Done() 28 | ticker := time.NewTicker(d) 29 | var ticks float64 // these come at regular intervals 30 | var tocks float64 // these come at interval 0, 1, 31 | 32 | for mark := range ticker.C { 33 | select { 34 | case _, isOpen := <-stopChan: 35 | if !isOpen { 36 | ticker.Stop() 37 | return 38 | } 39 | default: 40 | } 41 | ticks++ 42 | if math.Pow(2, tocks) == ticks { 43 | select { 44 | case tockChan <- mark: 45 | default: 46 | } 47 | tocks++ 48 | } 49 | } 50 | }() 51 | 52 | wg.Wait() 53 | 54 | return &BackoffTicker{ 55 | C: tockChan, 56 | stopChan: stopChan, 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /communicate/ticker_test.go: -------------------------------------------------------------------------------- 1 | package communicate 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestBackoffTicker(t *testing.T) { 9 | type testData struct { 10 | ticks int 11 | initialInterval time.Duration 12 | estimate time.Duration 13 | } 14 | 15 | var testDataSet []testData 16 | testDataSet = append(testDataSet, 17 | testData{ 18 | ticks: 5, 19 | initialInterval: 100 * time.Millisecond, 20 | estimate: 800 * time.Millisecond, 21 | }) 22 | testDataSet = append(testDataSet, 23 | testData{ 24 | ticks: 8, 25 | initialInterval: 25 * time.Millisecond, 26 | estimate: 1600 * time.Millisecond, 27 | }) 28 | 29 | for _, test := range testDataSet { 30 | start := time.Now() 31 | bot := NewBackoffTicker(test.initialInterval) 32 | threshold := test.estimate + (test.estimate / 20) 33 | end := start.Add(threshold) 34 | 35 | tickCount := 0 36 | for time.Now().Before(end) && tickCount < test.ticks { 37 | <-bot.C 38 | tickCount++ 39 | } 40 | elapsed := time.Now().Sub(start) 41 | if elapsed >= threshold { 42 | t.Fatalf("test ran long: limit %s, elapsed %s", threshold, elapsed) 43 | } 44 | if elapsed < test.estimate { 45 | t.Fatalf("test ran short : limit %s, elapsed %s", test.estimate, elapsed) 46 | } 47 | bot.Stop() 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /foozler/doc.go: -------------------------------------------------------------------------------- 1 | // Package foozler provides functionality for debugging a Cisco switch running 2 | // l2t. In particular, this some tooling to get debug output (the "fooz") into 3 | // another application (the "ler" action). 4 | package foozler 5 | -------------------------------------------------------------------------------- /foozler/output.go: -------------------------------------------------------------------------------- 1 | package foozler 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "golang.org/x/crypto/ssh" 7 | "io" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | // Debugee represents a Cisco switch that runs l2t. In this case - as a 13 | // a debug target. Upon connection, the target switch will be configured 14 | // to produce l2t debug output. 15 | type Debugee struct { 16 | session *ssh.Session 17 | stdin io.WriteCloser 18 | enable chan bool 19 | out chan string 20 | onDeath chan error 21 | stop chan chan struct{} 22 | } 23 | 24 | // Enable enables output from the switch. 25 | func (o *Debugee) Enable() { 26 | o.enable <- true 27 | } 28 | 29 | // Disable disables output from the switch. 30 | func (o *Debugee) Disable() { 31 | o.enable <- false 32 | } 33 | 34 | // Wait returns a channel that receives nil, or an error when the SSH 35 | // session ends. 36 | func (o *Debugee) Wait() <-chan error { 37 | return o.onDeath 38 | } 39 | 40 | // Output returns a channel that receives debug output from a switch 41 | // when output is enabled. 42 | func (o *Debugee) Output() <-chan string { 43 | return o.out 44 | } 45 | 46 | // Close closes the SSH session. 47 | func (o *Debugee) Close() { 48 | rejoin := make(chan struct{}) 49 | o.stop <- rejoin 50 | <-rejoin 51 | } 52 | 53 | // Execute executes a command on the switch. 54 | func (o *Debugee) Execute(command string) error { 55 | _, err := io.WriteString(o.stdin, fmt.Sprintf("%s\r\n", command)) 56 | if err != nil { 57 | return err 58 | } 59 | 60 | return nil 61 | } 62 | 63 | // ConnectTo connects to a Cisco switch via SSH to facilitate debugging. 64 | func ConnectTo(address string, port int, clientConfig *ssh.ClientConfig) (*Debugee, error) { 65 | client, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", address, port), clientConfig) 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | session, err := client.NewSession() 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | sshIn, err := session.StdinPipe() 76 | if err != nil { 77 | return nil, err 78 | } 79 | 80 | sshOut, err := session.StdoutPipe() 81 | if err != nil { 82 | return nil, err 83 | } 84 | 85 | sshErr, err := session.StderrPipe() 86 | if err != nil { 87 | return nil, err 88 | } 89 | 90 | onSessionDeath := make(chan error, 2) 91 | rawOutput := make(chan []byte) 92 | go func() { 93 | scanner := bufio.NewScanner(io.MultiReader(sshOut, sshErr)) 94 | 95 | for scanner.Scan() { 96 | rawOutput <- scanner.Bytes() 97 | } 98 | 99 | err := scanner.Err() 100 | if err != nil { 101 | onSessionDeath <- fmt.Errorf("stderr/stdout scanner exited - %s", err.Error()) 102 | } 103 | }() 104 | 105 | d := &Debugee{ 106 | session: session, 107 | stdin: sshIn, 108 | enable: make(chan bool), 109 | out: make(chan string, 1), 110 | onDeath: onSessionDeath, 111 | stop: make(chan chan struct{}), 112 | } 113 | 114 | go func() { 115 | discardIntialLines := 9 116 | discardTimeout := time.NewTimer(5 * time.Second) 117 | keepAliveTicker := time.NewTicker(10 * time.Minute) 118 | 119 | isEnabled := false 120 | for { 121 | select { 122 | case <-keepAliveTicker.C: 123 | d.Execute("show clock") 124 | case isEnabled = <-d.enable: 125 | case <-discardTimeout.C: 126 | discardIntialLines = 0 127 | case raw := <-rawOutput: 128 | if discardIntialLines > 0 { 129 | discardIntialLines-- 130 | continue 131 | } 132 | if isEnabled { 133 | d.out <- removeTimestamp(string(raw)) 134 | } 135 | case rejoin := <-d.stop: 136 | client.Close() 137 | keepAliveTicker.Stop() 138 | rejoin <- struct{}{} 139 | return 140 | } 141 | } 142 | }() 143 | 144 | err = session.Shell() 145 | if err != nil { 146 | return nil, err 147 | } 148 | 149 | err = d.Execute("no debug all") 150 | if err != nil { 151 | return nil, err 152 | } 153 | 154 | err = d.Execute("debug l2trace") 155 | if err != nil { 156 | return nil, err 157 | } 158 | 159 | err = d.Execute("terminal monitor") 160 | if err != nil { 161 | return nil, err 162 | } 163 | 164 | go func() { 165 | onSessionDeath <- session.Wait() 166 | }() 167 | 168 | return d, nil 169 | } 170 | 171 | func removeTimestamp(s string) string { 172 | if strings.Count(s, ":") < 3 { 173 | return s 174 | } 175 | 176 | firstSpace := strings.Index(s, " ") 177 | if firstSpace < 0 || firstSpace == len(s)-1 { 178 | return s 179 | } 180 | 181 | secondSpace := strings.Index(s, " ") 182 | if secondSpace < 0 || secondSpace == len(s)-1 || secondSpace - firstSpace > 2 { 183 | return s 184 | } 185 | 186 | end := strings.Index(s, ": ") 187 | if end - secondSpace < 10 || end - secondSpace > 20 { 188 | return s 189 | } 190 | 191 | if end + 2 > len(s) { 192 | return s[end+1:] 193 | } 194 | 195 | return s[end+2:] 196 | } 197 | -------------------------------------------------------------------------------- /foozler/output_test.go: -------------------------------------------------------------------------------- 1 | package foozler 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestWhatever(t *testing.T) { 8 | s := removeTimestamp("Aug 21 15:45:09.385: trace_request->src_mac : ffff.ffff.ffff") 9 | 10 | exp := "trace_request->src_mac : ffff.ffff.ffff" 11 | if s != exp { 12 | t.Fatalf("expected '%s', got '%s'", exp, s) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/chrismarget/cisco-l2t 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/cheggaaa/pb/v3 v3.0.1 // indirect 7 | github.com/stephen-fox/sshutil v0.0.1 // indirect 8 | golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/VividCortex/ewma v1.1.1 h1:MnEK4VOv6n0RSY4vtRe3h11qjxL3+t0B8yOL8iMXdcM= 2 | github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA= 3 | github.com/cheggaaa/pb/v3 v3.0.1 h1:m0BngUk2LuSRYdx4fujDKNRXNDpbNCfptPfVT2m6OJY= 4 | github.com/cheggaaa/pb/v3 v3.0.1/go.mod h1:SqqeMF/pMOIu3xgGoxtPYhMNQP258xE4x/XRTYua+KU= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= 7 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 8 | github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= 9 | github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 10 | github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= 11 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 12 | github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= 13 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 14 | github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y= 15 | github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 16 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 17 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 18 | github.com/pkg/sftp v1.8.3 h1:9jSe2SxTM8/3bXZjtqnkgTBW+lA8db0knZJyns7gpBA= 19 | github.com/pkg/sftp v1.8.3/go.mod h1:NxmoDg/QLVWluQDUYG7XBZTLUpKeFa8e3aMf1BfjyHk= 20 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 21 | github.com/stephen-fox/sshutil v0.0.1 h1:GIHJ3v8JaHJyU13XQpTs1SSV3241VdDwS01/9SLCA5Q= 22 | github.com/stephen-fox/sshutil v0.0.1/go.mod h1:2FU9C9w35aujfQPwpMT85p3WHutFaEQFAN3JRKmbUSs= 23 | github.com/stephen-fox/userutil v1.0.0 h1:6+4m8M3q7UAY16aoITQhuXbZaHC8w3fiviQ286aOJLI= 24 | github.com/stephen-fox/userutil v1.0.0/go.mod h1:7TFao7wHR7gZSsflVw3FS2raXLMsb+woUeG5NQPtsAA= 25 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 26 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 27 | golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 28 | golang.org/x/crypto v0.0.0-20190122013713-64072686203f/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 29 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 30 | golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586 h1:7KByu05hhLed2MO29w7p1XfZvZ13m8mub3shuVftRs0= 31 | golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 32 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 33 | golang.org/x/sys v0.0.0-20181218192612-074acd46bca6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 34 | golang.org/x/sys v0.0.0-20190122071731-054c452bb702/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 35 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 36 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 37 | golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= 38 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 39 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 40 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 41 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 42 | -------------------------------------------------------------------------------- /message/doc.go: -------------------------------------------------------------------------------- 1 | // Package message provides functionality for building and parsing Cisco 2 | // Layer 2 Traceroute messages. 3 | package message 4 | -------------------------------------------------------------------------------- /message/message.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "fmt" 7 | "github.com/chrismarget/cisco-l2t/attribute" 8 | "math" 9 | "strconv" 10 | "strings" 11 | ) 12 | 13 | type ( 14 | MsgType uint8 15 | MsgVer uint8 16 | MsgLen uint16 17 | AttrCount uint8 18 | ) 19 | 20 | const ( 21 | Version1 = MsgVer(1) 22 | defaultMsgType = RequestSrc 23 | defaultMsgVer = Version1 24 | 25 | RequestDst = MsgType(1) 26 | RequestSrc = MsgType(2) 27 | ReplyDst = MsgType(3) 28 | ReplySrc = MsgType(4) 29 | 30 | testMacString = "ffff.ffff.ffff" 31 | testVlan = 1 32 | ) 33 | 34 | var ( 35 | headerLenByVersion = map[MsgVer]MsgLen{ 36 | Version1: 5, 37 | } 38 | 39 | MsgTypeToString = map[MsgType]string{ 40 | RequestDst: "L2T_REQUEST_DST", 41 | RequestSrc: "L2T_REQUEST_SRC", 42 | ReplyDst: "L2T_REPLY_DST", 43 | ReplySrc: "L2T_REPLY_SRC", 44 | } 45 | 46 | msgTypeAttributeOrder = map[MsgType][]attribute.AttrType{ 47 | RequestDst: { 48 | attribute.DstMacType, 49 | attribute.SrcMacType, 50 | attribute.VlanType, 51 | attribute.SrcIPv4Type, 52 | attribute.NbrDevIDType, 53 | }, 54 | RequestSrc: { 55 | attribute.DstMacType, 56 | attribute.SrcMacType, 57 | attribute.VlanType, 58 | attribute.SrcIPv4Type, 59 | attribute.NbrDevIDType, 60 | }, 61 | ReplyDst: { 62 | attribute.DevNameType, 63 | attribute.DevTypeType, 64 | attribute.DevIPv4Type, 65 | attribute.ReplyStatusType, 66 | attribute.SrcIPv4Type, 67 | attribute.NbrDevIDType, 68 | attribute.InPortNameType, 69 | attribute.InPortDuplexType, 70 | attribute.InPortSpeedType, 71 | attribute.OutPortNameType, 72 | attribute.OutPortDuplexType, 73 | attribute.OutPortSpeedType, 74 | }, 75 | } 76 | 77 | msgTypeRequiredAttrs = map[MsgType][]attribute.AttrType{ 78 | RequestDst: { 79 | attribute.DstMacType, 80 | attribute.SrcMacType, 81 | attribute.VlanType, 82 | attribute.SrcIPv4Type, 83 | }, 84 | RequestSrc: { 85 | attribute.DstMacType, 86 | attribute.SrcMacType, 87 | attribute.VlanType, 88 | attribute.SrcIPv4Type, 89 | }, 90 | } 91 | ) 92 | 93 | // Msg represents an L2T message 94 | type Msg interface { 95 | // Type returns the message type. This is the first byte on the wire. 96 | Type() MsgType 97 | 98 | // Ver returns the message protocol version. This is the 99 | // second byte on the wire. 100 | Ver() MsgVer 101 | 102 | // Len returns the message overall length: header plus sum of attribute 103 | // lengths. This is the third/fourth bytes on the wire 104 | Len() MsgLen 105 | 106 | // GetAttr retrieves an attribute from the message by type. Returns 107 | // if the attribute is not present in the message. 108 | GetAttr(attribute.AttrType) attribute.Attribute 109 | 110 | // SetAttr adds an attribute to the message 111 | SetAttr(attribute.Attribute) 112 | 113 | // AttrCount returns the count of attributes in the message. This is 114 | // the fifth byte on the wire. 115 | AttrCount() AttrCount 116 | 117 | // Attributes returns a slice of attributes belonging to the message. 118 | Attributes() map[attribute.AttrType]attribute.Attribute 119 | 120 | // Validate checks the message for problems. 121 | Validate() error 122 | 123 | // NeedsSrcIp indicates whether this message requires an 124 | // attribute.SrcIPv4Type to be added before it can be sent. 125 | NeedsSrcIp() bool 126 | 127 | // AddAttr(attribute.Attribute) attrCount 128 | // DelAttr(attrCount) error 129 | 130 | //// SrcIpForTarget allows the caller to specify a function which picks 131 | //// the Type 14 (L2_ATTR_SRC_IP) payload (our IP address) when sending 132 | //// a message. Default behavior loads this value using egress interface 133 | //// address if the Type 14 attribute is omitted. There's probably no 134 | //// reason to call this function. 135 | //SrcIpForTarget(*net.IP) (*net.IP, error) 136 | 137 | // Marshal returns the message formatted for transmission onto the wire. 138 | // Extra attributes beyond those already built into the message may be 139 | // included when calling Marshal(). 140 | // 141 | // Support for these last-minute attributes stems from the requirement to 142 | // include our local IP address in the L2T payload (attribute 14). When 143 | // Marshal()ing a message to several different targets we might need to 144 | // source traffic from different local IP interfaces, so this lets us tack 145 | // the source attribute on at the last moment. 146 | Marshal([]attribute.Attribute) []byte 147 | 148 | // String returns the message header as a multiline string. 149 | String() string 150 | } 151 | 152 | type defaultMsg struct { 153 | msgType MsgType 154 | msgVer MsgVer 155 | attrs map[attribute.AttrType]attribute.Attribute 156 | srcIpIncluded bool 157 | //srcIpFunc func(*net.IP) (*net.IP, error) 158 | } 159 | 160 | func (o *defaultMsg) Type() MsgType { 161 | return o.msgType 162 | } 163 | 164 | func (o *defaultMsg) Ver() MsgVer { 165 | return o.msgVer 166 | } 167 | 168 | func (o *defaultMsg) Len() MsgLen { 169 | l := headerLenByVersion[o.msgVer] 170 | for _, a := range o.attrs { 171 | l += MsgLen(a.Len()) 172 | } 173 | return l 174 | } 175 | 176 | func (o *defaultMsg) GetAttr(at attribute.AttrType) attribute.Attribute { 177 | for _, a := range o.Attributes() { 178 | if at == a.Type() { 179 | return a 180 | } 181 | } 182 | return nil 183 | } 184 | 185 | func (o *defaultMsg) SetAttr(new attribute.Attribute) { 186 | o.attrs[new.Type()] = new 187 | } 188 | 189 | func (o *defaultMsg) AttrCount() AttrCount { 190 | return AttrCount(len(o.attrs)) 191 | } 192 | 193 | func (o *defaultMsg) Attributes() map[attribute.AttrType]attribute.Attribute { 194 | return o.attrs 195 | } 196 | 197 | func (o *defaultMsg) Validate() error { 198 | // undersize check 199 | if o.Len() < headerLenByVersion[Version1] { 200 | return fmt.Errorf("undersize message has %d bytes (min %d)", o.Len(), headerLenByVersion[Version1]) 201 | } 202 | 203 | // oversize check 204 | if o.Len() > math.MaxUint16 { 205 | return fmt.Errorf("oversize message has %d bytes (max %d)", o.Len(), math.MaxUint16) 206 | } 207 | 208 | // Look for duplicates, add up the length 209 | observedLen := headerLenByVersion[o.msgVer] 210 | foundAttrs := make(map[attribute.AttrType]bool) 211 | for _, a := range o.attrs { 212 | observedLen += MsgLen(a.Len()) 213 | t := a.Type() 214 | if _, ok := foundAttrs[a.Type()]; ok { 215 | return fmt.Errorf("attribute type %d (%s) repeats in message", t, attribute.AttrTypeString[t]) 216 | } 217 | foundAttrs[a.Type()] = true 218 | } 219 | 220 | // length sanity check 221 | queriedLen := o.Len() 222 | if observedLen != queriedLen { 223 | return fmt.Errorf("wire format byte length should be %d, got %d", observedLen, queriedLen) 224 | } 225 | 226 | // attribute count sanity check 227 | observedAttrCount := AttrCount(len(o.attrs)) 228 | queriedAttrCount := o.AttrCount() 229 | if observedAttrCount != queriedAttrCount { 230 | return fmt.Errorf("found %d attributes, object claims to have %d", observedAttrCount, queriedAttrCount) 231 | } 232 | 233 | return nil 234 | } 235 | 236 | func (o *defaultMsg) NeedsSrcIp() bool { 237 | return !o.srcIpIncluded 238 | } 239 | 240 | //func (o *defaultMsg) SrcIpForTarget(t *net.IP) (*net.IP, error) { 241 | // return o.srcIpFunc(t) 242 | //} 243 | 244 | func (o *defaultMsg) Marshal(extraAttrs []attribute.Attribute) []byte { 245 | 246 | // extract attributes from message 247 | var unorderedAttrs []attribute.Attribute 248 | for _, a := range o.attrs { 249 | unorderedAttrs = append(unorderedAttrs, a) 250 | } 251 | 252 | // append extra attributes and sort 253 | orderedAttrs := orderAttributes(append(unorderedAttrs, extraAttrs...), o.msgType) 254 | 255 | attributeLen := 0 256 | for _, a := range orderedAttrs { 257 | attributeLen += int(a.Len()) 258 | } 259 | 260 | // build up the 5 byte header 261 | marshaledLen := uint16(headerLenByVersion[Version1]) 262 | marshaledLen += uint16(attributeLen) 263 | lenBytes := make([]byte, 2) 264 | binary.BigEndian.PutUint16(lenBytes, marshaledLen) 265 | var outBytes bytes.Buffer 266 | outBytes.Write([]byte{ 267 | byte(o.Type()), 268 | byte(o.Ver()), 269 | }) 270 | outBytes.Write(lenBytes) 271 | outBytes.Write([]byte{ 272 | byte(len(orderedAttrs)), 273 | }) 274 | 275 | for _, a := range orderedAttrs { 276 | aBytes := attribute.MarshalAttribute(a) 277 | outBytes.Write(aBytes) 278 | } 279 | 280 | return outBytes.Bytes() 281 | } 282 | 283 | func (o *defaultMsg) String() string { 284 | msgTypeName := MsgTypeToString[o.Type()] 285 | if msgTypeName == "" { 286 | msgTypeName = "unknown" 287 | } 288 | sb := strings.Builder{} 289 | sb.WriteString(msgTypeName) 290 | sb.WriteString(" (") 291 | sb.WriteString(strconv.Itoa(int(o.Type()))) 292 | sb.WriteString(") with ") 293 | sb.WriteString(strconv.Itoa(int(o.AttrCount()))) 294 | sb.WriteString(" attributes (") 295 | sb.WriteString(strconv.Itoa(int(o.Len()))) 296 | sb.WriteString(" bytes)") 297 | return sb.String() 298 | } 299 | 300 | // MsgBuilder represents an L2T message builder 301 | type MsgBuilder interface { 302 | // SetType sets the message type. Only 4 types are known to exist, 303 | // of those, only the queries are likely relevant to this method 304 | // because I don't think we'll be sending replies... 305 | // 306 | // Default value is 1 (L2T_REQUEST_DST) 307 | SetType(MsgType) MsgBuilder 308 | 309 | // SetVer sets the message version. Only one version is known to 310 | // exist, so Version1 is the default. 311 | SetVer(MsgVer) MsgBuilder 312 | 313 | // SetAttr adds attributes to the message's []attribute.Attribute. 314 | // Attribute order matters on the wire, but not within this slice. 315 | SetAttr(attribute.Attribute) MsgBuilder 316 | 317 | //// SetSrcIpFunc sets the function that will be called to calculate 318 | //// the attribute SrcIPv4Type (14) payload if one is required but 319 | //// not otherwise specified. 320 | //SetSrcIpFunc(func(*net.IP) (*net.IP, error)) MsgBuilder 321 | 322 | // Build returns a message.Msg object with the specified type, 323 | // version and attributes. 324 | Build() Msg 325 | } 326 | 327 | type defaultMsgBuilder struct { 328 | msgType MsgType 329 | msgVer MsgVer 330 | attrs map[attribute.AttrType]attribute.Attribute 331 | // srcIpFunc func(*net.IP) (*net.IP, error) 332 | } 333 | 334 | func NewMsgBuilder() MsgBuilder { 335 | return &defaultMsgBuilder{ 336 | msgType: defaultMsgType, 337 | msgVer: defaultMsgVer, 338 | //srcIpFunc: defaultSrcIpFunc, 339 | attrs: make(map[attribute.AttrType]attribute.Attribute), 340 | } 341 | } 342 | 343 | func (o *defaultMsgBuilder) SetType(t MsgType) MsgBuilder { 344 | o.msgType = t 345 | return o 346 | } 347 | 348 | func (o *defaultMsgBuilder) SetVer(v MsgVer) MsgBuilder { 349 | o.msgVer = v 350 | return o 351 | } 352 | 353 | func (o *defaultMsgBuilder) SetAttr(a attribute.Attribute) MsgBuilder { 354 | o.attrs[a.Type()] = a 355 | return o 356 | } 357 | 358 | func (o *defaultMsgBuilder) Build() Msg { 359 | srcIpIncluded := false 360 | if _, exists := o.attrs[attribute.SrcIPv4Type]; exists { 361 | srcIpIncluded = true 362 | } 363 | m := &defaultMsg{ 364 | msgType: o.msgType, 365 | msgVer: o.msgVer, 366 | attrs: o.attrs, 367 | srcIpIncluded: srcIpIncluded, 368 | //srcIpFunc: o.srcIpFunc, 369 | } 370 | return m 371 | } 372 | 373 | // attrTypeLocationInSlice returns the index of the first instance 374 | // of and attribute.AttrType within a slice, or -1 if not found 375 | func attrTypeLocationInSlice(s []attribute.AttrType, a attribute.AttrType) int { 376 | for k, v := range s { 377 | if v == a { 378 | return k 379 | } 380 | } 381 | return -1 382 | } 383 | 384 | // orderAttributes sorts the supplied []Attribute according to 385 | // the order prescribed by msgTypeAttributeOrder. 386 | func orderAttributes(msgAttributes []attribute.Attribute, msgType MsgType) []attribute.Attribute { 387 | 388 | // make a []AttrType that represents the input []Attribute 389 | var inTypes []attribute.AttrType 390 | for _, a := range msgAttributes { 391 | inTypes = append(inTypes, a.Type()) 392 | } 393 | 394 | // loop over the correctly ordered []AttrType. Any attributes of the 395 | // appropriate type get appended to msgAttributes (they'll appear twice) 396 | for _, aType := range msgTypeAttributeOrder[msgType] { 397 | loc := attrTypeLocationInSlice(inTypes, aType) 398 | if loc >= 0 { 399 | msgAttributes = append(msgAttributes, msgAttributes[loc]) 400 | } 401 | } 402 | 403 | // loop over all of the inTypes. Any that don't appear in the correctly 404 | // ordered []AttrType get appended to msgAttributes. Now everything 405 | // appears in the list twice. 406 | for _, t := range inTypes { 407 | loc := attrTypeLocationInSlice(msgTypeAttributeOrder[msgType], t) 408 | if loc < 0 { 409 | loc = attribute.LocationOfAttributeByType(msgAttributes, t) 410 | msgAttributes = append(msgAttributes, msgAttributes[loc]) 411 | } 412 | } 413 | 414 | // At this point the msgAttributes slice is 2x its original length. 415 | // It begins with original data, then has required elements in order, 416 | // finishes with unordered elements. Cut it in half. 417 | targetLen := len(msgAttributes) >> 1 418 | msgAttributes = msgAttributes[targetLen:] 419 | 420 | return msgAttributes 421 | } 422 | 423 | // UnmarshalMessage takes a byte slice, returns a message after having 424 | func UnmarshalMessage(b []byte) (Msg, error) { 425 | msg, err := UnmarshalMessageUnsafe(b) 426 | if err != nil { 427 | return nil, err 428 | } 429 | 430 | if int(msg.Len()) != len(b) { 431 | return msg, fmt.Errorf("message header claims size of %d, got %d bytes", 432 | msg.Len(), len(b)) 433 | } 434 | 435 | // validate the message header 436 | err = msg.Validate() 437 | if err != nil { 438 | return nil, err 439 | } 440 | 441 | // validate each attribute 442 | for _, att := range msg.Attributes() { 443 | err := att.Validate() 444 | if err != nil { 445 | return msg, err 446 | } 447 | } 448 | 449 | return msg, nil 450 | } 451 | 452 | func UnmarshalMessageUnsafe(b []byte) (Msg, error) { 453 | if len(b) < int(headerLenByVersion[Version1]) { 454 | return nil, fmt.Errorf("cannot unmarshal message got only %d bytes", len(b)) 455 | } 456 | 457 | t := MsgType(b[0]) 458 | v := MsgVer(b[1]) 459 | l := MsgLen(binary.BigEndian.Uint16(b[2:4])) 460 | c := AttrCount(b[4]) 461 | 462 | attrs := make(map[attribute.AttrType]attribute.Attribute) 463 | 464 | p := int(headerLenByVersion[Version1]) 465 | for p < int(l) { 466 | remaining := int(l) - p 467 | if remaining < attribute.MinAttrLen { 468 | return nil, fmt.Errorf("at byte %d, not enough data remaining (%d btytes)to extract another attribute", p, attribute.MinAttrLen) 469 | } 470 | 471 | nextAttrLen := int(b[p+1]) 472 | if remaining < nextAttrLen { 473 | return nil, fmt.Errorf("at byte %d, not enough data remaining to extract a %d byte attribute", p, nextAttrLen) 474 | } 475 | 476 | a, err := attribute.UnmarshalAttribute(b[p : p+nextAttrLen]) 477 | if err != nil { 478 | return nil, err 479 | } 480 | 481 | attrs[a.Type()] = a 482 | p += nextAttrLen 483 | } 484 | 485 | if int(c) != len(attrs) { 486 | return nil, fmt.Errorf("header claimed %d attributes, got %d", c, len(attrs)) 487 | } 488 | 489 | return &defaultMsg{ 490 | msgType: t, 491 | msgVer: v, 492 | attrs: attrs, 493 | }, nil 494 | } 495 | 496 | // ListMissingAttributes takes a L2T message type and a map of attributes. 497 | // It returns a list of attribute types that are required for this sort of 498 | // message, but are missing from the supplied map. 499 | func ListMissingAttributes(t MsgType, a map[attribute.AttrType]attribute.Attribute) []attribute.AttrType { 500 | var missing []attribute.AttrType 501 | if required, ok := msgTypeRequiredAttrs[t]; ok { 502 | for _, r := range required { 503 | if _, ok = a[r]; !ok { 504 | missing = append(missing, r) 505 | } 506 | } 507 | } 508 | return missing 509 | } 510 | 511 | // TestMsg returns a pre-built message useful for probing for a switch 512 | func TestMsg() (Msg, error) { 513 | var a attribute.Attribute 514 | var err error 515 | 516 | builder := NewMsgBuilder() 517 | a, err = attribute.NewAttrBuilder().SetType(attribute.SrcMacType).SetString(testMacString).Build() 518 | if err != nil { 519 | return nil, err 520 | } 521 | builder.SetAttr(a) 522 | 523 | a, err = attribute.NewAttrBuilder().SetType(attribute.DstMacType).SetString(testMacString).Build() 524 | if err != nil { 525 | return nil, err 526 | } 527 | builder.SetAttr(a) 528 | 529 | a, err = attribute.NewAttrBuilder().SetType(attribute.VlanType).SetInt(uint32(testVlan)).Build() 530 | if err != nil { 531 | return nil, err 532 | } 533 | builder.SetAttr(a) 534 | 535 | return builder.Build(), nil 536 | } 537 | -------------------------------------------------------------------------------- /message/message_test.go: -------------------------------------------------------------------------------- 1 | package message 2 | 3 | import ( 4 | "bytes" 5 | "github.com/chrismarget/cisco-l2t/attribute" 6 | "log" 7 | "math/rand" 8 | "strconv" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | func TestNewMsgBuilder_Minimal(t *testing.T) { 14 | msg := NewMsgBuilder().Build() 15 | if msg.Len() != 5 { 16 | t.Fatal("Default message should be 5 bytes") 17 | } 18 | if msg.Type() != RequestSrc { 19 | t.Fatalf("Default message type should be %s", MsgTypeToString[RequestSrc]) 20 | } 21 | if msg.AttrCount() != 0 { 22 | t.Fatal("Attribute count foa a default message should be zero") 23 | } 24 | if len(msg.Attributes()) != 0 { 25 | t.Fatal("Default message should have no attributes") 26 | } 27 | err := msg.Validate() 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | } 32 | 33 | func TestNewMsgBuilder(t *testing.T) { 34 | const expectedLen = 109 // Header 5 plus attributes: 8+8+4+9+9+6+9+9+6+6+3+3+6+6+3+9 35 | 36 | var testType []attribute.AttrType 37 | var testString []string 38 | 39 | testType = append(testType, attribute.SrcMacType) 40 | testString = append(testString, "01:02:03:04:05:06") 41 | 42 | testType = append(testType, attribute.DstMacType) 43 | testString = append(testString, "02-03-04-05-06-07") 44 | 45 | testType = append(testType, attribute.VlanType) 46 | testString = append(testString, "257") 47 | 48 | testType = append(testType, attribute.DevNameType) 49 | testString = append(testString, "hello1") 50 | 51 | testType = append(testType, attribute.DevTypeType) 52 | testString = append(testString, "hello2") 53 | 54 | testType = append(testType, attribute.DevIPv4Type) 55 | testString = append(testString, "1.2.3.4") 56 | 57 | testType = append(testType, attribute.InPortNameType) 58 | testString = append(testString, "hello3") 59 | 60 | testType = append(testType, attribute.OutPortNameType) 61 | testString = append(testString, "hello4") 62 | 63 | testType = append(testType, attribute.InPortSpeedType) 64 | testString = append(testString, "10gbps") 65 | 66 | testType = append(testType, attribute.OutPortSpeedType) 67 | testString = append(testString, "100gb/s") 68 | 69 | testType = append(testType, attribute.InPortDuplexType) 70 | testString = append(testString, "auto") 71 | 72 | testType = append(testType, attribute.OutPortDuplexType) 73 | testString = append(testString, "half") 74 | 75 | testType = append(testType, attribute.NbrIPv4Type) 76 | testString = append(testString, "10.11.12.13") 77 | 78 | testType = append(testType, attribute.SrcIPv4Type) 79 | testString = append(testString, "20.21.22.23") 80 | 81 | testType = append(testType, attribute.ReplyStatusType) 82 | testString = append(testString, "Destination Mac address not found") 83 | 84 | testType = append(testType, attribute.NbrDevIDType) 85 | testString = append(testString, "hello5") 86 | 87 | var atts []attribute.Attribute 88 | for i := range testType { 89 | a, err := attribute.NewAttrBuilder().SetType(testType[i]).SetString(testString[i]).Build() 90 | if err != nil { 91 | t.Fatal(err) 92 | } 93 | atts = append(atts, a) 94 | } 95 | 96 | builder := NewMsgBuilder() 97 | builder = builder.SetType(RequestDst) 98 | for _, a := range atts { 99 | builder = builder.SetAttr(a) 100 | } 101 | 102 | msg := builder.Build() 103 | 104 | err := msg.Validate() 105 | if err != nil { 106 | t.Fatal(err) 107 | } 108 | 109 | if msg.Len() != expectedLen { 110 | t.Fatalf("Expected %d, got %d", expectedLen, msg.Len()) 111 | } 112 | } 113 | 114 | func TestLocationOfAttributeByType(t *testing.T) { 115 | testBytes := [][]byte{ 116 | {0, 0, 0, 0, 0, 0}, 117 | {1, 1, 1, 1, 1, 1}, 118 | {2, 2}, 119 | {33, 0}, 120 | {34, 0}, 121 | } 122 | 123 | testTypes := []attribute.AttrType{ 124 | attribute.SrcMacType, 125 | attribute.DstMacType, 126 | attribute.VlanType, 127 | attribute.DevNameType, 128 | attribute.DevTypeType, 129 | } 130 | 131 | // build up a slice of attributes 132 | var testAttrs []attribute.Attribute 133 | for i := 0; i < len(testBytes); i++ { 134 | a, err := attribute.NewAttrBuilder().SetType(testTypes[i]).SetBytes(testBytes[i]).Build() 135 | if err != nil { 136 | t.Fatal(err) 137 | } 138 | testAttrs = append(testAttrs, a) 139 | } 140 | 141 | // throw those attributes in there again to be 142 | // sure we get only the first occurrence 143 | for i := 0; i < len(testBytes); i++ { 144 | a, err := attribute.NewAttrBuilder().SetType(testTypes[i]).SetBytes(testBytes[i]).Build() 145 | if err != nil { 146 | t.Fatal(err) 147 | } 148 | testAttrs = append(testAttrs, a) 149 | } 150 | 151 | expectedLocationByType := map[int]attribute.AttrType{ 152 | 0: attribute.SrcMacType, 153 | 1: attribute.DstMacType, 154 | 2: attribute.VlanType, 155 | 3: attribute.DevNameType, 156 | 4: attribute.DevTypeType, 157 | } 158 | 159 | for expected, attrType := range expectedLocationByType { 160 | result := attribute.LocationOfAttributeByType(testAttrs, attrType) 161 | if result != expected { 162 | t.Fatalf("expected %d, got %d", expected, result) 163 | } 164 | } 165 | } 166 | 167 | func TestOrderAttributes_ExactMatch(t *testing.T) { 168 | //attrs := make(map[attribute.AttrType]attribute.Attribute) 169 | var attrs []attribute.Attribute 170 | var a attribute.Attribute 171 | var err error 172 | a, err = attribute.NewAttrBuilder().SetType(16).SetString("foo").Build() 173 | if err != nil { 174 | t.Fatal(err) 175 | } 176 | attrs = append(attrs, a) 177 | a, err = attribute.NewAttrBuilder().SetType(3).SetInt(5).Build() 178 | if err != nil { 179 | t.Fatal(err) 180 | } 181 | attrs = append(attrs, a) 182 | a, err = attribute.NewAttrBuilder().SetType(2).SetBytes([]byte{2, 2, 2, 2, 2, 2}).Build() 183 | if err != nil { 184 | t.Fatal(err) 185 | } 186 | attrs = append(attrs, a) 187 | a, err = attribute.NewAttrBuilder().SetType(1).SetBytes([]byte{1, 1, 1, 1, 1, 1}).Build() 188 | if err != nil { 189 | t.Fatal(err) 190 | } 191 | attrs = append(attrs, a) 192 | a, err = attribute.NewAttrBuilder().SetType(14).SetBytes([]byte{1, 2, 3, 4}).Build() 193 | if err != nil { 194 | t.Fatal(err) 195 | } 196 | attrs = append(attrs, a) 197 | 198 | resultAttrs := orderAttributes(attrs, RequestDst) 199 | 200 | var resultTypes []attribute.AttrType 201 | for _, a := range resultAttrs { 202 | resultTypes = append(resultTypes, a.Type()) 203 | } 204 | 205 | expected := []attribute.AttrType{2, 1, 3, 14, 16} 206 | 207 | if len(resultTypes) != len(expected) { 208 | t.Fatalf("results have unexpected length: got %d, expected %d", len(resultTypes), len(expected)) 209 | } 210 | 211 | for i := range expected { 212 | if expected[i] != resultTypes[i] { 213 | t.Fatalf("position %d expected %d got %d", i, expected[i], resultTypes[i]) 214 | } 215 | } 216 | } 217 | 218 | func TestOrderAttributes_WithExtras(t *testing.T) { 219 | //attrs := make(map[attribute.AttrType]attribute.Attribute) 220 | var attrs []attribute.Attribute 221 | var a attribute.Attribute 222 | var err error 223 | a, err = attribute.NewAttrBuilder().SetType(16).SetString("foo").Build() 224 | if err != nil { 225 | t.Fatal(err) 226 | } 227 | attrs = append(attrs, a) 228 | a, err = attribute.NewAttrBuilder().SetType(5).SetString("bar").Build() 229 | if err != nil { 230 | t.Fatal(err) 231 | } 232 | attrs = append(attrs, a) 233 | a, err = attribute.NewAttrBuilder().SetType(3).SetInt(5).Build() 234 | if err != nil { 235 | t.Fatal(err) 236 | } 237 | attrs = append(attrs, a) 238 | a, err = attribute.NewAttrBuilder().SetType(2).SetBytes([]byte{2, 2, 2, 2, 2, 2}).Build() 239 | if err != nil { 240 | t.Fatal(err) 241 | } 242 | attrs = append(attrs, a) 243 | a, err = attribute.NewAttrBuilder().SetType(1).SetBytes([]byte{1, 1, 1, 1, 1, 1}).Build() 244 | if err != nil { 245 | t.Fatal(err) 246 | } 247 | attrs = append(attrs, a) 248 | a, err = attribute.NewAttrBuilder().SetType(10).SetBytes([]byte{0, 0, 0, 1}).Build() 249 | if err != nil { 250 | t.Fatal(err) 251 | } 252 | attrs = append(attrs, a) 253 | a, err = attribute.NewAttrBuilder().SetType(14).SetBytes([]byte{1, 2, 3, 4}).Build() 254 | if err != nil { 255 | t.Fatal(err) 256 | } 257 | attrs = append(attrs, a) 258 | 259 | resultAttrs := orderAttributes(attrs, RequestDst) 260 | 261 | var resultTypes []attribute.AttrType 262 | for _, a := range resultAttrs { 263 | resultTypes = append(resultTypes, a.Type()) 264 | } 265 | 266 | // type 0 here are placeholders for attribute types 267 | // with no order expectation 268 | expected := []attribute.AttrType{2, 1, 3, 14, 16, 0, 0} 269 | 270 | if len(resultTypes) != len(expected) { 271 | t.Fatalf("results have unexpected length: got %d, expected %d", len(resultTypes), len(expected)) 272 | } 273 | 274 | for i := range expected { 275 | if expected[i] != resultTypes[i] { 276 | if expected[i] != 0 { 277 | t.Fatalf("position %d expected %d got %d", i, expected[i], resultTypes[i]) 278 | } 279 | } 280 | } 281 | } 282 | 283 | func TestOrderAttributes_ShortlistAndExtras(t *testing.T) { 284 | //attrs := make(map[attribute.AttrType]attribute.Attribute) 285 | var attrs []attribute.Attribute 286 | var a attribute.Attribute 287 | var err error 288 | a, err = attribute.NewAttrBuilder().SetType(5).SetString("bar").Build() 289 | if err != nil { 290 | t.Fatal(err) 291 | } 292 | attrs = append(attrs, a) 293 | a, err = attribute.NewAttrBuilder().SetType(3).SetInt(5).Build() 294 | if err != nil { 295 | t.Fatal(err) 296 | } 297 | attrs = append(attrs, a) 298 | a, err = attribute.NewAttrBuilder().SetType(2).SetBytes([]byte{2, 2, 2, 2, 2, 2}).Build() 299 | if err != nil { 300 | t.Fatal(err) 301 | } 302 | attrs = append(attrs, a) 303 | a, err = attribute.NewAttrBuilder().SetType(10).SetBytes([]byte{0, 0, 0, 1}).Build() 304 | if err != nil { 305 | t.Fatal(err) 306 | } 307 | attrs = append(attrs, a) 308 | a, err = attribute.NewAttrBuilder().SetType(14).SetBytes([]byte{1, 2, 3, 4}).Build() 309 | if err != nil { 310 | t.Fatal(err) 311 | } 312 | attrs = append(attrs, a) 313 | 314 | resultAttrs := orderAttributes(attrs, RequestDst) 315 | 316 | var resultTypes []attribute.AttrType 317 | for _, a := range resultAttrs { 318 | resultTypes = append(resultTypes, a.Type()) 319 | } 320 | 321 | expected := []attribute.AttrType{2, 3, 14, 5, 10} 322 | 323 | if len(resultTypes) != len(expected) { 324 | t.Fatalf("results have unexpected length: got %d, expected %d", len(resultAttrs), len(expected)) 325 | } 326 | 327 | for i := range expected { 328 | if expected[i] != resultTypes[i] { 329 | t.Fatalf("position %d expected %d got %d", i, expected[i], resultAttrs[i]) 330 | } 331 | } 332 | } 333 | 334 | func TestOrderAttributes_Shortlist(t *testing.T) { 335 | //attrs := make(map[attribute.AttrType]attribute.Attribute) 336 | var attrs []attribute.Attribute 337 | var a attribute.Attribute 338 | var err error 339 | 340 | a, err = attribute.NewAttrBuilder().SetType(16).SetString("foo").Build() 341 | if err != nil { 342 | t.Fatal(err) 343 | } 344 | attrs = append(attrs, a) 345 | a, err = attribute.NewAttrBuilder().SetType(3).SetInt(5).Build() 346 | if err != nil { 347 | t.Fatal(err) 348 | } 349 | attrs = append(attrs, a) 350 | a, err = attribute.NewAttrBuilder().SetType(1).SetBytes([]byte{1, 1, 1, 1, 1, 1}).Build() 351 | if err != nil { 352 | t.Fatal(err) 353 | } 354 | attrs = append(attrs, a) 355 | 356 | resultAttrs := orderAttributes(attrs, RequestDst) 357 | 358 | var resultTypes []attribute.AttrType 359 | for _, a := range resultAttrs { 360 | resultTypes = append(resultTypes, a.Type()) 361 | } 362 | 363 | expected := []attribute.AttrType{1, 3, 16} 364 | 365 | if len(resultTypes) != len(expected) { 366 | t.Fatalf("results have unexpected length: got %d, expected %d", len(resultTypes), len(expected)) 367 | } 368 | 369 | for i := range expected { 370 | if expected[i] != resultTypes[i] { 371 | t.Fatalf("position %d expected %d got %d", i, expected[i], resultTypes[i]) 372 | } 373 | } 374 | } 375 | 376 | func TestAttrTypeLocationInSlice(t *testing.T) { 377 | testData := []attribute.AttrType{1, 3, 5, 7, 9, 11, 13, 2, 4, 6, 8, 10, 12} 378 | expected := []int{-1, 1, 5, 11, -1} 379 | var result []int 380 | result = append(result, attrTypeLocationInSlice(testData, 16)) 381 | result = append(result, attrTypeLocationInSlice(testData, 3)) 382 | result = append(result, attrTypeLocationInSlice(testData, 11)) 383 | result = append(result, attrTypeLocationInSlice(testData, 10)) 384 | result = append(result, attrTypeLocationInSlice(testData, 15)) 385 | 386 | if len(result) != len(expected) { 387 | t.Fatalf("expected %d results, got %d", len(expected), len(result)) 388 | } 389 | 390 | for i := range expected { 391 | if expected[i] != result[i] { 392 | t.Fatalf("result %d expected %d got %d", i, expected[i], result[i]) 393 | } 394 | } 395 | 396 | } 397 | 398 | func TestMarshalMsg_Minimal(t *testing.T) { 399 | msg := NewMsgBuilder().Build() 400 | 401 | err := msg.Validate() 402 | if err != nil { 403 | t.Fatal(err) 404 | } 405 | 406 | expected := []byte{2, 1, 0, 5, 0} 407 | result := msg.Marshal(nil) 408 | if len(result) != len(expected) { 409 | t.Fatalf("expected 5 bytes") 410 | } 411 | 412 | if bytes.Compare(result, expected) != 0 { 413 | t.Fatalf("minimal marshaled message bad data") 414 | } 415 | } 416 | 417 | func TestMarshalMsg_ReqDstStandard(t *testing.T) { 418 | var a attribute.Attribute 419 | var err error 420 | 421 | builder := NewMsgBuilder() 422 | builder.SetType(RequestDst) 423 | builder.SetVer(Version1) 424 | 425 | // Attribute should be {2, 8, 1, 2, 3, 4, 5, 6} 426 | a, err = attribute.NewAttrBuilder().SetType(attribute.DstMacType).SetString("0102.0304.0506").Build() 427 | if err != nil { 428 | t.Fatal(err) 429 | } 430 | builder.SetAttr(a) 431 | 432 | // Attribute should be {3, 4, 12, 34} 433 | a, err = attribute.NewAttrBuilder().SetType(attribute.VlanType).SetInt(3106).Build() 434 | if err != nil { 435 | t.Fatal(err) 436 | } 437 | builder.SetAttr(a) 438 | 439 | // Attribute should be {16, 6, 102, 111, 111, 0} 440 | a, err = attribute.NewAttrBuilder().SetType(attribute.NbrDevIDType).SetString("foo").Build() 441 | if err != nil { 442 | t.Fatal(err) 443 | } 444 | builder.SetAttr(a) 445 | 446 | // Attribute should be {14, 6, 1, 2, 3, 4} 447 | a, err = attribute.NewAttrBuilder().SetType(attribute.SrcIPv4Type).SetString("1.2.3.4").Build() 448 | if err != nil { 449 | t.Fatal(err) 450 | } 451 | builder.SetAttr(a) 452 | 453 | // Attribute should be {1, 8, 255, 254, 253, 5, 6, 7} 454 | a, err = attribute.NewAttrBuilder().SetType(attribute.SrcMacType).SetString("ff-fe-fd-05-06-07").Build() 455 | if err != nil { 456 | t.Fatal(err) 457 | } 458 | builder.SetAttr(a) 459 | 460 | msg := builder.Build() 461 | if err != nil { 462 | t.Fatal(err) 463 | } 464 | 465 | err = msg.Validate() 466 | if err != nil { 467 | t.Fatal(err) 468 | } 469 | 470 | // RequestDst header should be {2, 1, 0, 37, 5} 471 | // DstMacType attribute should be {2, 8, 1, 2, 3, 4, 5, 6} 472 | // SrcMacType attribute should be {1, 8, 255, 254, 253, 5, 6, 7} 473 | // VlanType attribute should be {3, 4, 12, 34} 474 | // SrcIPv4Type attribute should be {14, 6, 1, 2, 3, 4} 475 | // NbrDevIDType attribute should be {16, 6, 102, 111, 111, 0} 476 | 477 | expected := []byte{ 478 | 1, 1, 0, 37, 5, 479 | 2, 8, 1, 2, 3, 4, 5, 6, 480 | 1, 8, 255, 254, 253, 5, 6, 7, 481 | 3, 4, 12, 34, 482 | 14, 6, 1, 2, 3, 4, 483 | 16, 6, 102, 111, 111, 0, 484 | } 485 | result := msg.Marshal(nil) 486 | if len(result) != len(expected) { 487 | t.Fatalf("got %d bytes, expected %d", len(result), len(expected)) 488 | } 489 | 490 | if bytes.Compare(result, expected) != 0 { 491 | t.Fatalf("RequestDst standard message bad data") 492 | } 493 | } 494 | 495 | func TestMarshalMsg_ReqDstOversize(t *testing.T) { 496 | var a attribute.Attribute 497 | var err error 498 | 499 | builder := NewMsgBuilder() 500 | builder.SetType(RequestDst) 501 | builder.SetVer(Version1) 502 | 503 | // Attribute should be {2, 8, 1, 2, 3, 4, 5, 6} 504 | a, err = attribute.NewAttrBuilder().SetType(attribute.DstMacType).SetString("0102.0304.0506").Build() 505 | if err != nil { 506 | t.Fatal(err) 507 | } 508 | builder.SetAttr(a) 509 | 510 | // Attribute should be {3, 4, 12, 34} 511 | a, err = attribute.NewAttrBuilder().SetType(attribute.VlanType).SetInt(3106).Build() 512 | if err != nil { 513 | t.Fatal(err) 514 | } 515 | builder.SetAttr(a) 516 | 517 | // Attribute should be {16, 6, 102, 111, 111, 0} 518 | a, err = attribute.NewAttrBuilder().SetType(attribute.NbrDevIDType).SetString("foo").Build() 519 | if err != nil { 520 | t.Fatal(err) 521 | } 522 | builder.SetAttr(a) 523 | 524 | // Superflous Attribute should be {11, 3, 0} 525 | a, err = attribute.NewAttrBuilder().SetType(attribute.InPortDuplexType).SetInt(0).Build() 526 | if err != nil { 527 | t.Fatal(err) 528 | } 529 | builder.SetAttr(a) 530 | 531 | // Attribute should be {14, 6, 1, 2, 3, 4} 532 | a, err = attribute.NewAttrBuilder().SetType(attribute.SrcIPv4Type).SetString("1.2.3.4").Build() 533 | if err != nil { 534 | t.Fatal(err) 535 | } 536 | builder.SetAttr(a) 537 | 538 | // Attribute should be {1, 8, 255, 254, 253, 5, 6, 7} 539 | a, err = attribute.NewAttrBuilder().SetType(attribute.SrcMacType).SetString("ff-fe-fd-05-06-07").Build() 540 | if err != nil { 541 | t.Fatal(err) 542 | } 543 | builder.SetAttr(a) 544 | 545 | msg := builder.Build() 546 | if err != nil { 547 | t.Fatal(err) 548 | } 549 | 550 | err = msg.Validate() 551 | if err != nil { 552 | t.Fatal(err) 553 | } 554 | 555 | // RequestDst header should be {2, 1, 0, 40, 6} 556 | // DstMacType attribute should be {2, 8, 1, 2, 3, 4, 5, 6} 557 | // SrcMacType attribute should be {1, 8, 255, 254, 253, 5, 6, 7} 558 | // VlanType attribute should be {3, 4, 12, 34} 559 | // SrcIPv4Type attribute should be {14, 6, 1, 2, 3, 4} 560 | // NbrDevIDType attribute should be {16, 6, 102, 111, 111, 0} 561 | // Superfluous 562 | // InPortDuplexType attribute should be {11, 3, 0} 563 | 564 | expected := []byte{ 565 | 1, 1, 0, 40, 6, 566 | 2, 8, 1, 2, 3, 4, 5, 6, 567 | 1, 8, 255, 254, 253, 5, 6, 7, 568 | 3, 4, 12, 34, 569 | 14, 6, 1, 2, 3, 4, 570 | 16, 6, 102, 111, 111, 0, 571 | 11, 3, 0, 572 | } 573 | result := msg.Marshal(nil) 574 | if len(result) != len(expected) { 575 | t.Fatalf("got %d bytes, expected %d", len(expected), len(result)) 576 | } 577 | 578 | if bytes.Compare(result, expected) != 0 { 579 | t.Fatalf("RequestDst oversize message bad data") 580 | } 581 | } 582 | 583 | func TestMarshalMsg_ReqDstUndersize(t *testing.T) { 584 | var a attribute.Attribute 585 | var err error 586 | 587 | builder := NewMsgBuilder() 588 | builder.SetType(RequestDst) 589 | builder.SetVer(Version1) 590 | 591 | // Attribute should be {2, 8, 1, 2, 3, 4, 5, 6} 592 | a, err = attribute.NewAttrBuilder().SetType(attribute.DstMacType).SetString("0102.0304.0506").Build() 593 | if err != nil { 594 | t.Fatal(err) 595 | } 596 | builder.SetAttr(a) 597 | 598 | // Attribute should be {3, 4, 12, 34} 599 | a, err = attribute.NewAttrBuilder().SetType(attribute.VlanType).SetInt(3106).Build() 600 | if err != nil { 601 | t.Fatal(err) 602 | } 603 | builder.SetAttr(a) 604 | 605 | // Attribute should be {16, 6, 102, 111, 111, 0} 606 | a, err = attribute.NewAttrBuilder().SetType(attribute.NbrDevIDType).SetString("foo").Build() 607 | if err != nil { 608 | t.Fatal(err) 609 | } 610 | builder.SetAttr(a) 611 | 612 | // Attribute should be {1, 8, 255, 254, 253, 5, 6, 7} 613 | a, err = attribute.NewAttrBuilder().SetType(attribute.SrcMacType).SetString("ff-fe-fd-05-06-07").Build() 614 | if err != nil { 615 | t.Fatal(err) 616 | } 617 | builder.SetAttr(a) 618 | 619 | msg := builder.Build() 620 | if err != nil { 621 | t.Fatal(err) 622 | } 623 | 624 | err = msg.Validate() 625 | if err != nil { 626 | t.Fatal(err) 627 | } 628 | 629 | // RequestDst header should be {2, 1, 0, 31, 4} 630 | // DstMacType attribute should be {2, 8, 1, 2, 3, 4, 5, 6} 631 | // SrcMacType attribute should be {1, 8, 255, 254, 253, 5, 6, 7} 632 | // VlanType attribute should be {3, 4, 12, 34} 633 | // NbrDevIDType attribute should be {16, 6, 102, 111, 111, 0} 634 | 635 | expected := []byte{ 636 | 1, 1, 0, 31, 4, 637 | 2, 8, 1, 2, 3, 4, 5, 6, 638 | 1, 8, 255, 254, 253, 5, 6, 7, 639 | 3, 4, 12, 34, 640 | 16, 6, 102, 111, 111, 0, 641 | } 642 | result := msg.Marshal(nil) 643 | if len(result) != len(expected) { 644 | t.Fatalf("got %d bytes, expected %d", len(expected), len(result)) 645 | } 646 | 647 | if bytes.Compare(result, expected) != 0 { 648 | t.Fatalf("RequestDst undersize message bad data") 649 | } 650 | } 651 | 652 | func TestUnmarshalMessage(t *testing.T) { 653 | data := []byte{ 654 | 3, 1, 0, 86, 12, 655 | 4, 10, 67, 97, 116, 50, 57, 54, 48, 0, 656 | 5, 19, 87, 83, 45, 67, 50, 57, 54, 48, 71, 45, 50, 52, 84, 67, 45, 76, 0, 657 | 6, 6, 192, 168, 1, 254, 658 | 15, 3, 1, 659 | 13, 6, 0, 0, 0, 0, 660 | 16, 3, 0, 661 | 7, 8, 71, 105, 48, 47, 52, 0, 662 | 11, 3, 0, 663 | 9, 6, 0, 0, 0, 0, 664 | 8, 8, 71, 105, 48, 47, 52, 0, 665 | 12, 3, 0, 666 | 10, 6, 0, 0, 0, 0, 667 | } 668 | 669 | msg, err := UnmarshalMessage(data) 670 | if err != nil { 671 | t.Fatal(err) 672 | } 673 | 674 | _ = msg 675 | // TODO inspect the message 676 | } 677 | 678 | func TestString(t *testing.T) { 679 | data := []byte{ 680 | 3, 1, 0, 86, 12, 681 | 4, 10, 67, 97, 116, 50, 57, 54, 48, 0, 682 | 5, 19, 87, 83, 45, 67, 50, 57, 54, 48, 71, 45, 50, 52, 84, 67, 45, 76, 0, 683 | 6, 6, 192, 168, 1, 254, 684 | 15, 3, 1, 685 | 13, 6, 0, 0, 0, 0, 686 | 16, 3, 0, 687 | 7, 8, 71, 105, 48, 47, 52, 0, 688 | 11, 3, 0, 689 | 9, 6, 0, 0, 0, 0, 690 | 8, 8, 71, 105, 48, 47, 52, 0, 691 | 12, 3, 0, 692 | 10, 6, 0, 0, 0, 0, 693 | } 694 | 695 | msg, err := UnmarshalMessage(data) 696 | if err != nil { 697 | t.Fatal(err) 698 | } 699 | 700 | expected := "L2T_REPLY_DST (3) with 12 attributes (86 bytes)" 701 | result := msg.String() 702 | if expected != result { 703 | t.Fatalf("expected '%s', got '%s'", expected, result) 704 | } 705 | } 706 | 707 | func TestSetAttr(t *testing.T) { 708 | msgData := []byte{ 709 | 2, 1, 0, 25, 3, // header t2, v1, l25, ac3 710 | 2, 8, 255, 255, 255, 255, 255, 255, // src_mac ff:ff:ff:ff:ff:ff 711 | 1, 8, 255, 255, 255, 255, 255, 255, // dst_mac ff:ff:ff:ff:ff:ff 712 | 3, 4, 0, 1, // vlan 1 713 | } 714 | 715 | msg, err := UnmarshalMessage(msgData) 716 | if err != nil { 717 | t.Fatal(err) 718 | } 719 | 720 | if msg.AttrCount() != 3 { 721 | t.Fatalf("expected attrcout 3, got %d", msg.AttrCount()) 722 | } 723 | 724 | if msg.NeedsSrcIp() != true { 725 | t.Fatalf("message should have needed src ip") 726 | } 727 | 728 | srcIpAttr, err := attribute.NewAttrBuilder(). 729 | SetType(attribute.SrcIPv4Type). 730 | SetString("1.1.1.1"). 731 | Build() 732 | if err != nil { 733 | t.Fatal(err) 734 | } 735 | 736 | msg.SetAttr(srcIpAttr) 737 | 738 | if msg.AttrCount() != 4 { 739 | t.Fatalf("expected attrcout 4, got %d", msg.AttrCount()) 740 | } 741 | 742 | _ = msg 743 | } 744 | 745 | func TestSetAttrAgain(t *testing.T) { 746 | msgData := []byte{ 747 | 2, 1, 0, 25, 3, // header t2, v1, l25, ac3 748 | 2, 8, 255, 255, 255, 255, 255, 255, // src_mac ff:ff:ff:ff:ff:ff 749 | 1, 8, 255, 255, 255, 255, 255, 255, // dst_mac ff:ff:ff:ff:ff:ff 750 | 3, 4, 0, 1, // vlan 1 751 | } 752 | 753 | msg, err := UnmarshalMessage(msgData) 754 | if err != nil { 755 | t.Fatal(err) 756 | } 757 | 758 | if msg.AttrCount() != 3 { 759 | t.Fatalf("expected attrcout 3, got %d", msg.AttrCount()) 760 | } 761 | 762 | if msg.NeedsSrcIp() != true { 763 | t.Fatalf("message should have needed src ip") 764 | } 765 | 766 | srcIpAttr, err := attribute.NewAttrBuilder(). 767 | SetType(attribute.SrcIPv4Type). 768 | SetString("1.1.1.1"). 769 | Build() 770 | if err != nil { 771 | t.Fatal(err) 772 | } 773 | msg.SetAttr(srcIpAttr) 774 | 775 | rand.Seed(time.Now().UTC().UnixNano()) 776 | 777 | v1 := (rand.Int() % 4093) + 1 778 | vlan1Attr, err := attribute.NewAttrBuilder(). 779 | SetType(attribute.VlanType). 780 | SetInt(uint32(v1)). 781 | Build() 782 | if err != nil { 783 | log.Fatal(err) 784 | } 785 | 786 | v2 := (rand.Int() % 4093) + 1 787 | vlan2Attr, err := attribute.NewAttrBuilder(). 788 | SetType(attribute.VlanType). 789 | SetInt(uint32(v2)). 790 | Build() 791 | if err != nil { 792 | log.Fatal(err) 793 | } 794 | 795 | msg.SetAttr(vlan1Attr) 796 | for _, a := range msg.Attributes() { 797 | if a.Type() == attribute.VlanType { 798 | if a.String() != strconv.Itoa(v1) { 799 | t.Fatalf("v1 got vlan %s, expected %d", a.String(), v1) 800 | } 801 | } 802 | } 803 | msg.SetAttr(vlan2Attr) 804 | for _, a := range msg.Attributes() { 805 | if a.Type() == attribute.VlanType { 806 | if a.String() != strconv.Itoa(v2) { 807 | t.Fatalf("v2 got vlan %s, expected %d", a.String(), v2) 808 | } 809 | } 810 | } 811 | 812 | if msg.AttrCount() != 4 { 813 | t.Fatalf("expected attrcout 4, got %d", msg.AttrCount()) 814 | } 815 | } 816 | -------------------------------------------------------------------------------- /target/builder.go: -------------------------------------------------------------------------------- 1 | package target 2 | 3 | import ( 4 | "github.com/chrismarget/cisco-l2t/attribute" 5 | "github.com/chrismarget/cisco-l2t/communicate" 6 | "github.com/chrismarget/cisco-l2t/message" 7 | "net" 8 | "strings" 9 | "sync" 10 | "time" 11 | ) 12 | 13 | type Builder interface { 14 | AddIp(net.IP) Builder 15 | Build() (Target, error) 16 | } 17 | 18 | func TargetBuilder() Builder { 19 | return &defaultTargetBuilder{} 20 | } 21 | 22 | type targetInfo struct { 23 | destination *net.UDPAddr 24 | theirSource net.IP 25 | localAddr net.IP 26 | rtt []time.Duration 27 | bestRtt time.Duration 28 | } 29 | 30 | type defaultTargetBuilder struct { 31 | addresses []net.IP 32 | } 33 | 34 | func (o *defaultTargetBuilder) AddIp(ip net.IP) Builder { 35 | if addressIsNew(ip, o.addresses) { 36 | o.addresses = append(o.addresses, ip) 37 | } 38 | return o 39 | } 40 | 41 | func (o *defaultTargetBuilder) Build() (Target, error) { 42 | var name string 43 | var platform string 44 | var mgmtIp net.IP 45 | var info []targetInfo 46 | 47 | // Loop over o.addresses, noting that it may grow as the loop progresses 48 | var i int 49 | for i < len(o.addresses) { 50 | destination := &net.UDPAddr{ 51 | IP: o.addresses[len(info)], 52 | Port: communicate.CiscoL2TPort, 53 | } 54 | result := checkTarget(destination) 55 | 56 | // Save "name" and "result" so they're not 57 | // overwritten by a future failed query. 58 | if result.name != "" { 59 | name = result.name 60 | } 61 | if result.platform != "" { 62 | platform = result.platform 63 | } 64 | if result.mgmtIp.String() != "" { 65 | mgmtIp = result.mgmtIp 66 | } 67 | 68 | var rttSamples []time.Duration 69 | if result.sourceIp != nil { 70 | rttSamples = append(rttSamples, result.latency) 71 | } 72 | // Add a targetInfo structure to the slice for every address we probe. 73 | info = append(info, targetInfo{ 74 | localAddr: result.localIp, 75 | destination: destination, 76 | theirSource: result.sourceIp, 77 | rtt: rttSamples, 78 | bestRtt: result.latency, // only sample is best sample 79 | }) 80 | 81 | // Reply came from an unknown source address? Add it to the list. 82 | if result.sourceIp != nil && addressIsNew(result.sourceIp, o.addresses) { 83 | o.addresses = append(o.addresses, result.sourceIp) 84 | } 85 | 86 | // Reply cited an unknown management address? Add it to the list. 87 | if result.mgmtIp != nil && addressIsNew(result.mgmtIp, o.addresses) { 88 | o.addresses = append(o.addresses, result.mgmtIp) 89 | } 90 | 91 | // Last iteration of this loop is the one where 'i' has grown to match 92 | // the length of the address list. 93 | i++ 94 | } 95 | 96 | // look through the targetInfo structures we've collected 97 | var fastestTarget int 98 | var reachable bool 99 | for i, ti := range info { 100 | // ignore targetInfo if the target didn't talk to us 101 | if ti.theirSource == nil { 102 | continue 103 | } else { 104 | reachable = true 105 | } 106 | 107 | if ti.bestRtt < info[fastestTarget].bestRtt { 108 | fastestTarget = i 109 | } 110 | } 111 | 112 | return &defaultTarget{ 113 | reachable: reachable, 114 | info: info, 115 | best: fastestTarget, 116 | name: name, 117 | platform: platform, 118 | mgmtIp: mgmtIp, 119 | }, nil 120 | } 121 | 122 | func TestTargetBuilder() Builder { 123 | return &testTargetBuilder{} 124 | } 125 | 126 | type testTargetBuilder struct { 127 | addresses []net.IP 128 | } 129 | 130 | func (o *testTargetBuilder) AddIp(ip net.IP) Builder { 131 | if addressIsNew(ip, o.addresses) { 132 | o.addresses = append(o.addresses, ip) 133 | } 134 | return o 135 | 136 | } 137 | func (o *testTargetBuilder) Build() (Target, error) { 138 | name := "TestTarget" 139 | platform := "TestPlatform" 140 | mgmtIp := net.ParseIP("192.168.255.1") 141 | var ti []targetInfo 142 | if len(o.addresses) == 0 { 143 | o.addresses = append(o.addresses, net.ParseIP("127.0.0.1")) 144 | } 145 | 146 | for i, a := range o.addresses { 147 | outIp, _ := communicate.GetOutgoingIpForDestination(a) 148 | rtt := []time.Duration{(time.Duration(i) + 1) * time.Millisecond} 149 | ti = append(ti, targetInfo{ 150 | destination: &net.UDPAddr{ 151 | IP: a, 152 | Port: communicate.CiscoL2TPort, 153 | }, 154 | theirSource: a, 155 | localAddr: outIp, 156 | rtt: rtt, 157 | bestRtt: 0, 158 | }) 159 | } 160 | 161 | return &defaultTarget{ 162 | reachable: true, 163 | info: ti, 164 | best: 0, 165 | name: name, 166 | platform: platform, 167 | mgmtIp: mgmtIp, 168 | rttLock: sync.Mutex{}, 169 | }, nil 170 | 171 | } 172 | 173 | // addressIsNew returns a boolean indicating whether 174 | // the net.IP is found in the []net.IP 175 | func addressIsNew(a net.IP, known []net.IP) bool { 176 | for _, k := range known { 177 | if a.String() == k.String() { 178 | return false 179 | } 180 | } 181 | return true 182 | } 183 | 184 | type testPacketResult struct { 185 | destination *net.UDPAddr 186 | err error 187 | latency time.Duration 188 | sourceIp net.IP 189 | platform string 190 | name string 191 | mgmtIp net.IP 192 | localIp net.IP 193 | } 194 | 195 | func (r *testPacketResult) String() string { 196 | var s strings.Builder 197 | s.WriteString("result:\n ") 198 | s.WriteString(r.destination.String()) 199 | s.WriteString("\n ") 200 | s.WriteString(r.sourceIp.String()) 201 | s.WriteString("\n") 202 | return s.String() 203 | } 204 | 205 | // checkTarget sends test L2T messages to the specified IP address. It 206 | // returns a testPacketResult that represents the result of the check. 207 | func checkTarget(destination *net.UDPAddr) testPacketResult { 208 | // Build up the test message. Doing so requires that we know our IP address 209 | // which, on a multihomed system requires that we look up the route to the 210 | // target. So, we need to know about the target before we can form the 211 | // message. 212 | ourIp, err := communicate.GetOutgoingIpForDestination(destination.IP) 213 | if err != nil { 214 | return testPacketResult{ 215 | destination: destination, 216 | err: err, 217 | } 218 | } 219 | ourIpAttr, err := attribute.NewAttrBuilder(). 220 | SetType(attribute.SrcIPv4Type). 221 | SetString(ourIp.String()). 222 | Build() 223 | if err != nil { 224 | return testPacketResult{ 225 | err: err, 226 | } 227 | } 228 | 229 | testMsg, err := message.TestMsg() 230 | if err != nil { 231 | return testPacketResult{err: err} 232 | } 233 | 234 | err = testMsg.Validate() 235 | if err != nil { 236 | return testPacketResult{err: err} 237 | } 238 | 239 | payload := testMsg.Marshal([]attribute.Attribute{ourIpAttr}) 240 | 241 | // We're going to send the message via two different sockets: A "connected" 242 | // (dial) socket and a "non-connected" (listen) socket. The former can 243 | // telegraph ICMP unreachable (go away!) messages to us, while the latter 244 | // can detect 3rd party replies (necessary because of course the Cisco L2T 245 | // service generates replies from an alien (NAT unfriendly!) address. 246 | stopDialSocket := make(chan struct{}) // abort channel 247 | outViaDial := communicate.SendThis{ // Communicate() output structure 248 | Payload: payload, 249 | Destination: destination, 250 | ExpectReplyFrom: destination.IP, 251 | RttGuess: communicate.InitialRTTGuess * 2, 252 | } 253 | stopListenSocket := make(chan struct{}) // abort channel 254 | outViaListen := communicate.SendThis{ // Communicate() output structure 255 | Payload: payload, 256 | Destination: destination, 257 | ExpectReplyFrom: nil, 258 | RttGuess: communicate.InitialRTTGuess * 2, 259 | } 260 | 261 | dialResult := make(chan communicate.SendResult) 262 | go func() { 263 | // This guy can't hear 3rd party (alien) replies. Start him first 264 | // because he's not deaf to (noisy) ICMP unreachables. 265 | dialResult <- communicate.Communicate(outViaDial, stopDialSocket) 266 | }() 267 | 268 | listenResult := make(chan communicate.SendResult) 269 | go func() { 270 | // This guy can't hear ICMP unreachables, so keep the noise down 271 | // by starting him a bit after the "dial" based listener. 272 | time.Sleep(communicate.InitialRTTGuess) 273 | listenResult <- communicate.Communicate(outViaListen, stopListenSocket) 274 | }() 275 | 276 | // grab a SendResult from either channel (socket) 277 | var in communicate.SendResult 278 | select { 279 | case in = <-dialResult: 280 | case in = <-listenResult: 281 | } 282 | close(stopDialSocket) 283 | close(stopListenSocket) 284 | 285 | // return an error (maybe) 286 | if in.Err != nil { 287 | if result, ok := in.Err.(net.Error); ok && result.Timeout() { 288 | // we timed out. Return an empty testPacketResult 289 | return testPacketResult{} 290 | } else { 291 | // some other type of error 292 | return testPacketResult{err: in.Err} 293 | } 294 | } 295 | 296 | replyMsg, err := message.UnmarshalMessage(in.ReplyData) 297 | if err != nil { 298 | return testPacketResult{err: err} 299 | } 300 | 301 | err = replyMsg.Validate() 302 | if err != nil { 303 | return testPacketResult{err: err} 304 | } 305 | 306 | // Pull the name, platform, and IP address from the message attributes. 307 | var name string 308 | var platform string 309 | var mgmtIp net.IP 310 | for t, a := range replyMsg.Attributes() { 311 | if t == attribute.DevNameType { 312 | name = a.String() 313 | } 314 | if t == attribute.DevTypeType { 315 | platform = a.String() 316 | } 317 | if t == attribute.DevIPv4Type { 318 | mgmtIp = net.ParseIP(a.String()) 319 | } 320 | } 321 | 322 | return testPacketResult{ 323 | localIp: ourIp, 324 | err: in.Err, 325 | latency: in.Rtt, 326 | sourceIp: in.ReplyFrom, 327 | platform: platform, 328 | name: name, 329 | mgmtIp: mgmtIp, 330 | } 331 | } 332 | 333 | func initialLatency() []time.Duration { 334 | var l []time.Duration 335 | for len(l) < 5 { 336 | l = append(l, communicate.InitialRTTGuess) 337 | } 338 | return l 339 | } 340 | -------------------------------------------------------------------------------- /target/builder_test.go: -------------------------------------------------------------------------------- 1 | package target 2 | 3 | import ( 4 | "github.com/chrismarget/cisco-l2t/communicate" 5 | "log" 6 | "net" 7 | "testing" 8 | ) 9 | 10 | func TestCheckTarget(t *testing.T) { 11 | destination := &net.UDPAddr{ 12 | IP: net.ParseIP("192.168.96.2"), 13 | Port: communicate.CiscoL2TPort, 14 | Zone: "", 15 | } 16 | result := checkTarget(destination) 17 | if result.err != nil { 18 | t.Fatal(result.err) 19 | } 20 | if result.latency <= 0 { 21 | t.Fatalf("observed latency %d units of duration", result.latency) 22 | } 23 | log.Println("testIp:",destination.IP.String()) 24 | log.Println("responseIp:",result.sourceIp) 25 | log.Println("latency:",result.latency) 26 | } 27 | 28 | func TestTestTargetBuilder(t *testing.T) { 29 | log.Println("----------------------------------") 30 | tta, err := TestTargetBuilder(). 31 | Build() 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | log.Println(tta.String()) 36 | log.Println("----------------------------------") 37 | 38 | ttb, err := TestTargetBuilder(). 39 | AddIp(net.ParseIP("2.2.2.2")). 40 | Build() 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | log.Println(ttb.String()) 45 | log.Println("----------------------------------") 46 | 47 | ttc, err := TestTargetBuilder(). 48 | AddIp(net.ParseIP("3.3.3.3")). 49 | AddIp(net.ParseIP("4.4.4.4")). 50 | Build() 51 | if err != nil { 52 | t.Fatal(err) 53 | } 54 | log.Println(ttc.String()) 55 | log.Println("----------------------------------") 56 | 57 | } -------------------------------------------------------------------------------- /target/doc.go: -------------------------------------------------------------------------------- 1 | // Package target provides functionality for communicating with Cisco Layer 2 2 | // Traceroute listeners in a more reliable manner. It builds on the 3 | // communicate library, and attempts to handle weirdness specific to the Cisco 4 | // L2T protocol. 5 | package target 6 | -------------------------------------------------------------------------------- /target/errors.go: -------------------------------------------------------------------------------- 1 | package target 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "strings" 7 | ) 8 | 9 | type UnreachableTargetError struct { 10 | AddressesTried []net.IP 11 | } 12 | 13 | func (o UnreachableTargetError) Error() string { 14 | var at []string 15 | for _, i := range o.AddressesTried { 16 | at = append(at, i.String()) 17 | } 18 | return fmt.Sprintf("cannot reach target using any of these addresses: %v", strings.Join(at, ", ")) 19 | } 20 | -------------------------------------------------------------------------------- /target/queries.go: -------------------------------------------------------------------------------- 1 | package target 2 | 3 | import ( 4 | "fmt" 5 | "github.com/chrismarget/cisco-l2t/attribute" 6 | "github.com/chrismarget/cisco-l2t/message" 7 | "net" 8 | ) 9 | 10 | const ( 11 | vlanMin = 1 12 | vlanMax = 4094 13 | ) 14 | 15 | // HasIp returns a boolean indicating whether the target is known 16 | // to have the given IP address. 17 | func (o defaultTarget) HasIp(in *net.IP) bool { 18 | for _, i := range o.info { 19 | if in.Equal(i.destination.IP) { 20 | return true 21 | } 22 | } 23 | return false 24 | } 25 | 26 | // HasVlan queries the target about a VLAN is configured. 27 | func (o *defaultTarget) HasVlan(vlan int) (bool, error) { 28 | var att attribute.Attribute 29 | var err error 30 | 31 | builder := message.NewMsgBuilder() 32 | builder.SetType(message.RequestSrc) 33 | att, err = attribute.NewAttrBuilder().SetType(attribute.SrcMacType).SetString("ffff.ffff.ffff").Build() 34 | if err != nil { 35 | return false, err 36 | } 37 | builder.SetAttr(att) 38 | 39 | att, err = attribute.NewAttrBuilder().SetType(attribute.DstMacType).SetString("ffff.ffff.ffff").Build() 40 | if err != nil { 41 | return false, err 42 | } 43 | builder.SetAttr(att) 44 | 45 | att, err = attribute.NewAttrBuilder().SetType(attribute.VlanType).SetInt(uint32(vlan)).Build() 46 | if err != nil { 47 | return false, err 48 | } 49 | builder.SetAttr(att) 50 | 51 | msg := builder.Build() 52 | err = msg.Validate() 53 | if err != nil { 54 | return false, err 55 | } 56 | 57 | response, err := o.Send(msg) 58 | if err != nil { 59 | return false, err 60 | } 61 | 62 | // Parse response. If we got the "good" error, then the VLAN exists. 63 | for _, a := range response.Attributes() { 64 | if a.Type() == attribute.ReplyStatusType && 65 | a.String() == attribute.ReplyStatusSrcNotFound { 66 | return true, nil 67 | } 68 | } 69 | return false, nil 70 | } 71 | 72 | func (o *defaultTarget) GetIps() []net.IP { 73 | var out []net.IP 74 | INFO: 75 | for _, info := range o.info { 76 | ip := info.destination.IP 77 | for _, found := range out { 78 | if found.Equal(ip) { 79 | continue INFO 80 | } 81 | } 82 | out = append(out, ip) 83 | } 84 | return out 85 | } 86 | 87 | func (o *defaultTarget) MacInVlan(mac net.HardwareAddr, vlan int) (bool, error) { 88 | if vlan < 1 || vlan > 4094 { 89 | return false, fmt.Errorf("vlan %d out of range", vlan) 90 | } 91 | 92 | var att attribute.Attribute 93 | var err error 94 | 95 | builder := message.NewMsgBuilder() 96 | builder.SetType(message.RequestSrc) 97 | att, err = attribute.NewAttrBuilder(). 98 | SetType(attribute.SrcMacType). 99 | SetString("ffff.ffff.ffff"). 100 | Build() 101 | if err != nil { 102 | return false, err 103 | } 104 | builder.SetAttr(att) 105 | 106 | att, err = attribute.NewAttrBuilder(). 107 | SetType(attribute.DstMacType). 108 | SetString(mac.String()). 109 | Build() 110 | if err != nil { 111 | return false, err 112 | } 113 | builder.SetAttr(att) 114 | 115 | att, err = attribute.NewAttrBuilder(). 116 | SetType(attribute.VlanType). 117 | SetInt(uint32(vlan)). 118 | Build() 119 | if err != nil { 120 | return false, err 121 | } 122 | builder.SetAttr(att) 123 | 124 | att, err = attribute.NewAttrBuilder(). 125 | SetType(attribute.SrcIPv4Type). 126 | SetString(o.info[o.best].localAddr.String()). 127 | Build() 128 | if err != nil { 129 | return false, err 130 | } 131 | builder.SetAttr(att) 132 | 133 | msg := builder.Build() 134 | err = msg.Validate() 135 | if err != nil { 136 | return false, err 137 | } 138 | 139 | response, err := o.Send(msg) 140 | if err != nil { 141 | return false, err 142 | } 143 | 144 | if response.Attributes()[attribute.ReplyStatusType].Type() == 2 { 145 | return true, nil 146 | } 147 | 148 | return false, nil 149 | } 150 | -------------------------------------------------------------------------------- /target/target.go: -------------------------------------------------------------------------------- 1 | package target 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/chrismarget/cisco-l2t/attribute" 7 | "github.com/chrismarget/cisco-l2t/communicate" 8 | "github.com/chrismarget/cisco-l2t/message" 9 | "net" 10 | "sync" 11 | "time" 12 | ) 13 | 14 | const ( 15 | maxLatencySamples = 10 16 | ) 17 | 18 | type Target interface { 19 | GetIps() []net.IP 20 | GetLocalIp() net.IP 21 | HasIp(*net.IP) bool 22 | HasVlan(int) (bool, error) 23 | MacInVlan(net.HardwareAddr, int) (bool, error) 24 | Reachable() bool 25 | Send(message.Msg) (message.Msg, error) 26 | SendBulkUnsafe([]message.Msg, chan struct{}) []BulkSendResult 27 | SendUnsafe(message.Msg) communicate.SendResult 28 | String() string 29 | } 30 | 31 | type defaultTarget struct { 32 | reachable bool 33 | info []targetInfo 34 | best int 35 | name string 36 | platform string 37 | mgmtIp net.IP 38 | rttLock sync.Mutex 39 | } 40 | 41 | func (o *defaultTarget) GetLocalIp() net.IP { 42 | return o.info[o.best].localAddr 43 | } 44 | 45 | func (o *defaultTarget) Reachable() bool { 46 | return o.reachable 47 | } 48 | 49 | type BulkSendResult struct { 50 | Index int 51 | Retries int 52 | Msg message.Msg 53 | Err error 54 | } 55 | 56 | func (o *defaultTarget) SendBulkUnsafe(out []message.Msg, progressChan chan struct{}) []BulkSendResult { 57 | resultChan := make(chan BulkSendResult, len(out)) 58 | finalResultChan := make(chan []BulkSendResult) 59 | 60 | // Credit to stephen-fox for good ideas about using a buffered channel 61 | // to size a concurrency pool. Thank you Steve! 62 | // 63 | // Wait until all messages (len(out)) have been sent 64 | wg := &sync.WaitGroup{} 65 | wg.Add(len(out)) 66 | 67 | // Initialize the worker pool (channel) 68 | maxWorkers := 100 69 | workers := 5 70 | workerPool := make(chan struct{}, maxWorkers) 71 | for i := 0; i < workers; i++ { 72 | workerPool <- struct{}{} //add worker credits to the workerPool 73 | } 74 | 75 | // collect results from all of the child routines; 76 | // tweak workerPool as necessary 77 | go func() { 78 | msgsSinceLastRetry := 0 79 | var results []BulkSendResult 80 | for r := range resultChan { // loop until resultChan closes 81 | results = append(results, r) // collect the reply 82 | wg.Done() 83 | 84 | updatePBar := true 85 | switch r.Err.(type) { 86 | case (net.Error): 87 | if r.Err.(net.Error).Temporary() { 88 | updatePBar = false 89 | msgsSinceLastRetry = 0 90 | } 91 | if r.Err.(net.Error).Timeout() { 92 | updatePBar = true 93 | msgsSinceLastRetry = 0 94 | } 95 | default: 96 | updatePBar = true 97 | if r.Retries == 0 { 98 | msgsSinceLastRetry++ 99 | } else { 100 | msgsSinceLastRetry = 0 101 | } 102 | } 103 | 104 | if updatePBar { 105 | if progressChan != nil { 106 | progressChan <- struct{}{} 107 | } 108 | } 109 | 110 | switch msgsSinceLastRetry { 111 | case 0: 112 | if workers > 1 { 113 | <-workerPool 114 | workers-- 115 | } 116 | case 5: 117 | if workers < maxWorkers { 118 | workerPool <- struct{}{} 119 | workers++ 120 | msgsSinceLastRetry = 1 121 | } 122 | } 123 | } 124 | finalResultChan <- results 125 | }() 126 | 127 | // main loop instantiates a worker (pool permitting) to send each message 128 | for index, outMsg := range out { 129 | <-workerPool // Block until possible to get a worker credit 130 | go func(i int, m message.Msg) { // Start a worker routine 131 | reply := o.SendUnsafe(m) 132 | 133 | var inMsg message.Msg 134 | replyErr := reply.Err 135 | if replyErr == nil { 136 | inMsg, replyErr = message.UnmarshalMessageUnsafe(reply.ReplyData) 137 | } 138 | 139 | resultChan <- BulkSendResult{ 140 | Index: i, 141 | Retries: reply.Attempts - 1, 142 | Msg: inMsg, 143 | Err: replyErr, 144 | } 145 | workerPool <- struct{}{} // Worker done, return credit to the pool 146 | }(index, outMsg) 147 | 148 | } 149 | 150 | wg.Wait() 151 | close(resultChan) 152 | 153 | interimResults := <-finalResultChan // hope these are all good 154 | 155 | var goodResults []BulkSendResult 156 | var retry []message.Msg 157 | 158 | for _, ir := range interimResults { 159 | if x, ok := ir.Err.(net.Error); ok && x.Temporary() { 160 | retry = append(retry, out[ir.Index]) 161 | } else { 162 | goodResults = append(goodResults, ir) 163 | } 164 | } 165 | 166 | var retryResult []BulkSendResult 167 | if len(retry) != 0 { 168 | retryResult = o.SendBulkUnsafe(retry, progressChan) 169 | } 170 | 171 | return append(goodResults, retryResult...) 172 | } 173 | 174 | func (o *defaultTarget) Send(out message.Msg) (message.Msg, error) { 175 | if out.NeedsSrcIp() { 176 | srcIpAttr, err := attribute.NewAttrBuilder(). 177 | SetType(attribute.SrcIPv4Type). 178 | SetString(o.info[o.best].localAddr.String()). 179 | Build() 180 | if err != nil { 181 | return nil, err 182 | } 183 | out.SetAttr(srcIpAttr) 184 | } 185 | 186 | in := o.SendUnsafe(out) 187 | if in.Err != nil { 188 | return nil, in.Err 189 | } 190 | 191 | inMsg, err := message.UnmarshalMessageUnsafe(in.ReplyData) 192 | if err != nil { 193 | return nil, err 194 | } 195 | 196 | err = inMsg.Validate() 197 | if err != nil { 198 | return inMsg, err 199 | } 200 | 201 | return inMsg, nil 202 | } 203 | 204 | func (o *defaultTarget) SendUnsafe(msg message.Msg) communicate.SendResult { 205 | payload := msg.Marshal([]attribute.Attribute{}) 206 | 207 | out := communicate.SendThis{ 208 | Payload: payload, 209 | Destination: o.info[o.best].destination, 210 | ExpectReplyFrom: o.info[o.best].theirSource, 211 | RttGuess: o.estimateLatency(), 212 | } 213 | 214 | in := communicate.Communicate(out, nil) 215 | 216 | if in.Err == nil { 217 | o.updateLatency(o.best, in.Rtt) 218 | } 219 | 220 | return in 221 | } 222 | 223 | func (o *defaultTarget) String() string { 224 | var out bytes.Buffer 225 | 226 | out.WriteString("Target info:\n Hostname: ") 227 | switch o.name { 228 | case "": 229 | out.WriteString("") 230 | default: 231 | out.WriteString(o.name) 232 | } 233 | 234 | out.WriteString("\n Platform: ") 235 | switch o.platform { 236 | case "": 237 | out.WriteString("") 238 | default: 239 | out.WriteString(o.platform) 240 | } 241 | 242 | out.WriteString("\n Claimed IP: ") 243 | switch o.mgmtIp.String() { 244 | case "": 245 | out.WriteString("") 246 | default: 247 | out.WriteString(o.mgmtIp.String()) 248 | } 249 | 250 | out.WriteString("\n Known IP Addresses:") 251 | for _, i := range o.info { 252 | var avgRttStr string 253 | switch len(i.rtt){ 254 | case 0: avgRttStr = "" 255 | default: avgRttStr = fmt.Sprintf("%s", averageRtt(i.rtt)) 256 | } 257 | out.WriteString(fmt.Sprintf("\n %15s responds from %-15s %s", 258 | i.destination.IP.String(), 259 | i.theirSource, 260 | avgRttStr)) 261 | } 262 | 263 | out.WriteString("\n Target address: ") 264 | out.WriteString(o.info[o.best].destination.IP.String()) 265 | 266 | out.WriteString("\n Listen address: ") 267 | out.WriteString(o.info[o.best].theirSource.String()) 268 | 269 | out.WriteString("\n Local address: ") 270 | out.WriteString(o.info[o.best].localAddr.String()) 271 | 272 | return out.String() 273 | } 274 | 275 | func averageRtt (in []time.Duration) time.Duration{ 276 | if len(in) == 0 { 277 | return 0 278 | } 279 | var total time.Duration 280 | for _, i := range in { 281 | total = total + i 282 | } 283 | // nonsense math below rounds to Microseconds 284 | return total / time.Duration(len(in)) / time.Microsecond * time.Microsecond 285 | } 286 | 287 | // estimateLatency tries to estimate the response time for this target 288 | // using the contents of the objects latency slice. 289 | func (o *defaultTarget) estimateLatency() time.Duration { 290 | o.rttLock.Lock() 291 | observed := o.info[o.best].rtt 292 | o.rttLock.Unlock() 293 | 294 | if len(observed) == 0 { 295 | return communicate.InitialRTTGuess 296 | } 297 | 298 | // trim the latency samples 299 | lo := len(observed) 300 | if lo > maxLatencySamples { 301 | observed = observed[lo-maxLatencySamples : lo] 302 | } 303 | 304 | // half-assed latency estimator does a rolling average then pads 25% 305 | var result int64 306 | for i, l := range observed { 307 | switch i { 308 | case 0: 309 | result = int64(l) 310 | default: 311 | result = (result + int64(l)) / 2 312 | } 313 | } 314 | return time.Duration(float32(result) * float32(1.25)) 315 | } 316 | 317 | // updateLatency adds the passed time.Duration as the most recent 318 | // latency sample to the specified targetInfo index. 319 | func (o *defaultTarget) updateLatency(index int, t time.Duration) { 320 | o.rttLock.Lock() 321 | l := len(o.info[index].rtt) 322 | if l < maxLatencySamples { 323 | o.info[index].rtt = append(o.info[index].rtt, t) 324 | } else { 325 | o.info[index].rtt = append(o.info[index].rtt, t)[l+1-maxLatencySamples : l+1] 326 | } 327 | o.rttLock.Unlock() 328 | } 329 | 330 | type SendMessageConfig struct { 331 | M message.Msg 332 | Inbox chan MessageResponse 333 | } 334 | 335 | type MessageResponse struct { 336 | Response message.Msg 337 | Err error 338 | } 339 | -------------------------------------------------------------------------------- /target/target_test.go: -------------------------------------------------------------------------------- 1 | package target 2 | 3 | import ( 4 | "github.com/chrismarget/cisco-l2t/attribute" 5 | "github.com/chrismarget/cisco-l2t/message" 6 | "log" 7 | "net" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | func TestNewTargetBuilder(t *testing.T) { 13 | tb, err := TargetBuilder(). 14 | AddIp(net.ParseIP("192.168.8.254")). 15 | Build() 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | 20 | println(tb.String()) 21 | } 22 | 23 | func TestEstimateLatency(t *testing.T) { 24 | latency := []time.Duration{ 25 | 1 * time.Millisecond, 26 | 2 * time.Millisecond, 27 | 3 * time.Millisecond, 28 | 4 * time.Millisecond, 29 | 5 * time.Millisecond, 30 | 6 * time.Millisecond, 31 | 7 * time.Millisecond, 32 | 8 * time.Millisecond, 33 | 9 * time.Millisecond, 34 | 10 * time.Millisecond, 35 | 11 * time.Millisecond, 36 | } 37 | o := defaultTarget{ 38 | info: []targetInfo{ 39 | { 40 | rtt: latency, 41 | }, 42 | }, 43 | best: 0, 44 | } 45 | result := o.estimateLatency() 46 | if result.Round(100*time.Microsecond) == 12500*time.Microsecond { 47 | log.Printf("latency estimate okay: %s", result) 48 | } else { 49 | t.Fatalf("expected %d usec, got %d usec", 11500, result.Round(time.Microsecond)) 50 | } 51 | 52 | latency = []time.Duration{} 53 | o = defaultTarget{ 54 | info: []targetInfo{ 55 | { 56 | rtt: latency, 57 | }, 58 | }, 59 | best: 0, 60 | } 61 | result = o.estimateLatency() 62 | if result == 250*time.Millisecond { 63 | log.Printf("wild-ass guess latency okay: %s", result) 64 | } else { 65 | t.Fatalf("expected 100ms, got %s", result) 66 | } 67 | } 68 | 69 | func TestUpdateLatency(t *testing.T) { 70 | values := []time.Duration{ 71 | 4 * time.Millisecond, 72 | 6 * time.Millisecond, 73 | 2 * time.Millisecond, 74 | 10 * time.Millisecond, 75 | } 76 | 77 | target := defaultTarget{ 78 | info: []targetInfo{ 79 | {rtt: nil}, 80 | }, 81 | best: 0, 82 | } 83 | 84 | for vi, val := range values { 85 | target.updateLatency(0, val) 86 | for i := 0; i <= vi; i++ { 87 | if values[i] != target.info[0].rtt[i] { 88 | t.Fatalf("target latency info not updating correctly") 89 | } 90 | } 91 | } 92 | log.Println("samples: ", len(target.info[0].rtt)) 93 | } 94 | 95 | func TestSendBulkUnsafe(t *testing.T) { 96 | var bulkSendThis []message.Msg 97 | for i := 1; i <= 4094; i++ { 98 | aSrcMac, err := attribute.NewAttrBuilder(). 99 | SetType(attribute.SrcMacType). 100 | SetString("ffff.ffff.ffff"). 101 | Build() 102 | if err != nil { 103 | t.Fatal(err) 104 | } 105 | aDstMac, err := attribute.NewAttrBuilder(). 106 | SetType(attribute.DstMacType). 107 | SetString("ffff.ffff.ffff"). 108 | Build() 109 | if err != nil { 110 | t.Fatal(err) 111 | } 112 | aVlan, err := attribute.NewAttrBuilder(). 113 | SetType(attribute.VlanType). 114 | SetInt(uint32(i)). 115 | Build() 116 | if err != nil { 117 | t.Fatal(err) 118 | } 119 | aSrcIp, err := attribute.NewAttrBuilder(). 120 | SetType(attribute.SrcIPv4Type). 121 | SetString("1.1.1.1"). 122 | Build() 123 | if err != nil { 124 | t.Fatal(err) 125 | } 126 | msg := message.NewMsgBuilder(). 127 | SetAttr(aSrcMac). 128 | SetAttr(aDstMac). 129 | SetAttr(aVlan). 130 | SetAttr(aSrcIp). 131 | Build() 132 | bulkSendThis = append(bulkSendThis, msg) 133 | } 134 | 135 | testTarget, err := TargetBuilder(). 136 | AddIp(net.ParseIP("192.168.96.150")). 137 | Build() 138 | _ = testTarget 139 | if err != nil { 140 | t.Fatal(err) 141 | } 142 | 143 | time.Sleep(3 * time.Second) 144 | 145 | result := testTarget.SendBulkUnsafe(bulkSendThis, nil) 146 | log.Println(len(result)) 147 | } 148 | 149 | func TestAverageRtt(t *testing.T) { 150 | var a []time.Duration 151 | e := []time.Duration{ 152 | 0, 153 | 500*time.Microsecond, 154 | 1000*time.Microsecond, 155 | 1500*time.Microsecond, 156 | 2000*time.Microsecond, 157 | } 158 | for len(a) < 5 { 159 | a = append(a, time.Duration(len(a))*time.Millisecond) 160 | r := averageRtt(a) 161 | if r != e[len(a)-1] { 162 | log.Println(a) 163 | t.Fatalf("bad average time, expected %s, got %s",e[len(a)-1], r) 164 | } else { 165 | log.Println("okay") 166 | } 167 | } 168 | } 169 | 170 | func TestAverageRttEmpty(t *testing.T) { 171 | a := averageRtt([]time.Duration{}) 172 | if a != 0 { 173 | t.Fatalf("average of no RTT samples should have returned zero durations") 174 | } 175 | } 176 | --------------------------------------------------------------------------------