Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1228dbddb9 | ||
|
|
c00b00d623 | ||
|
|
7bbc9f1b82 | ||
|
|
15a92f9c92 | ||
|
|
9386c01845 | ||
|
|
5232a9af6b | ||
|
|
c393b23b31 | ||
|
|
955fbca44a | ||
|
|
18c26e9fbb | ||
|
|
b54efbde69 | ||
|
|
be9d64b2cc | ||
|
|
c6f356a167 | ||
|
|
eae45ebc94 | ||
|
|
ea50b3d5cc | ||
|
|
3297b93edb | ||
|
|
6d0e867ad0 | ||
|
|
1cea9c1c87 | ||
|
|
3320a9e7d4 | ||
|
|
9b90a938f5 | ||
|
|
c6d0149ba6 | ||
|
|
598efe5d1a | ||
|
|
3679065258 | ||
|
|
4649f1158a | ||
|
|
de17a44f35 | ||
|
|
52ce4df18e | ||
|
|
f1ed5bf838 | ||
|
|
256a801d16 | ||
|
|
c416cf8da5 | ||
|
|
75e812a061 | ||
|
|
4cc7b01940 | ||
|
|
a2ff7e5981 | ||
|
|
cda98a6375 | ||
|
|
3b7a754b0a | ||
|
|
c01c6bcb91 | ||
|
|
e47360f8dc | ||
|
|
6035b54277 | ||
|
|
a238130613 | ||
|
|
998be42a52 | ||
|
|
14944da396 | ||
|
|
a3974c2aeb | ||
|
|
13ec517f94 | ||
|
|
43419815f4 | ||
|
|
b5256f7b71 | ||
|
|
0caa9dd652 | ||
|
|
5e1e8ec543 | ||
|
|
c391092ea6 | ||
|
|
b3a29bec1b | ||
|
|
b055c9a4a2 | ||
|
|
3daceac668 | ||
|
|
4611e8825a | ||
|
|
00e4729533 | ||
|
|
517a2e0e0a | ||
|
|
bb8c7c599c | ||
|
|
0d0fc39236 | ||
|
|
302a5a4efc | ||
|
|
3b29e8e6f2 | ||
|
|
a1dff70ba2 | ||
|
|
12671be260 | ||
|
|
9a4adccec6 | ||
|
|
f90390974a | ||
|
|
f598ae297b | ||
|
|
4b0fe502f0 | ||
|
|
e4295a2da9 | ||
|
|
216a0fe4fc | ||
|
|
80770d39e5 | ||
|
|
beb15fa32c | ||
|
|
0adf7626d0 | ||
|
|
9c17fe259c | ||
|
|
ecab4b4716 | ||
|
|
7660ad761a | ||
|
|
c8ff2de3b3 | ||
|
|
24a2e43547 | ||
|
|
a9d212c888 | ||
|
|
c1b1276aaf | ||
|
|
370212609c | ||
|
|
281937cf82 | ||
|
|
d11b6e8075 | ||
|
|
7709c6d0bd | ||
|
|
ef3e478506 | ||
|
|
93eef26d0b | ||
|
|
14d78cf08c | ||
|
|
e96a63f550 | ||
|
|
7a5b69f850 | ||
|
|
a71d2a1682 | ||
|
|
257b1a5353 | ||
|
|
b45ac75735 | ||
|
|
c7c669fd11 | ||
|
|
ba266ef8d3 | ||
|
|
f434fad3be | ||
|
|
bab758ebf9 | ||
|
|
800f4663b6 | ||
|
|
b1b62afa90 | ||
|
|
6b90ef7078 | ||
|
|
2ed4b45c24 | ||
|
|
47ba9748b4 | ||
|
|
077ad4208f | ||
|
|
c19edec364 | ||
|
|
0c2aa494ae | ||
|
|
73e0a6bb0a | ||
|
|
abf1db0045 | ||
|
|
b40b78ece0 | ||
|
|
3ade796e8d | ||
|
|
7c2e808924 | ||
|
|
7d2227c5a4 | ||
|
|
8a517d5c7c | ||
|
|
48658f011f | ||
|
|
4ebb13fa94 | ||
|
|
ff9a6b91a4 | ||
|
|
86fc37a968 | ||
|
|
37ae169323 | ||
|
|
1ce0df4e94 | ||
|
|
db5ba61915 | ||
|
|
db120fb9be | ||
|
|
ea6e171a7f | ||
|
|
246cb76e50 | ||
|
|
ecf4a82b99 | ||
|
|
357f67255e | ||
|
|
7015ab8717 | ||
|
|
67195bc9f3 | ||
|
|
ce18509aa7 | ||
|
|
cbaa92dcb2 | ||
|
|
f09d87c112 | ||
|
|
7a8bba4d95 | ||
|
|
e6e414a017 | ||
|
|
81df06b3f4 | ||
|
|
5401037743 | ||
|
|
9d9768ee6f | ||
|
|
3043217b2c | ||
|
|
afcb84d42c | ||
|
|
d5aad8f8d2 | ||
|
|
c46febd7df | ||
|
|
f880da4827 | ||
|
|
85c9cf43f7 | ||
|
|
ce90d2389b | ||
|
|
d8cfbc4e08 | ||
|
|
88fad251aa | ||
|
|
1a1d8b7c74 | ||
|
|
a4ef96690e | ||
|
|
f28ad350bd | ||
|
|
0afc3db383 | ||
|
|
ca1cce3232 | ||
|
|
f9228eeb46 | ||
|
|
0c077b8bb1 | ||
|
|
0e9991f7d4 | ||
|
|
41482aff01 | ||
|
|
791fc9d6b1 | ||
|
|
776ccac00a | ||
|
|
1539009362 | ||
|
|
9542b77a34 | ||
|
|
b709cdeab5 | ||
|
|
e2f936fde3 | ||
|
|
a4ca389f2e | ||
|
|
c973808b20 | ||
|
|
e12dbb4605 | ||
|
|
c341399498 | ||
|
|
d1ba4ab7f4 | ||
|
|
85d4b1f137 | ||
|
|
b9792f8f96 | ||
|
|
e0cb9cf24d | ||
|
|
4e41d4d451 | ||
|
|
059cc65b55 | ||
|
|
d434b50dfc | ||
|
|
96f69583e7 | ||
|
|
ce25f58639 | ||
|
|
367b73e6e3 | ||
|
|
98a7d3fc99 | ||
|
|
6fd0f121a0 | ||
|
|
68d6c262d1 | ||
|
|
1d9ef76080 | ||
|
|
b51b1adfcd | ||
|
|
ab902b45aa | ||
|
|
ed14561afd | ||
|
|
86d2db5cc2 | ||
|
|
700851cdd5 | ||
|
|
c3e1f5a6c6 | ||
|
|
9bf18b1761 | ||
|
|
1374e4c4de | ||
|
|
0ef57a102f | ||
|
|
1c7954a43d | ||
|
|
be1dc8ef96 | ||
|
|
18fc6ccc03 | ||
|
|
7ec46983aa | ||
|
|
eddcf93942 | ||
|
|
42807f7b8a | ||
|
|
0b521272ae | ||
|
|
ecd93bbfe4 | ||
|
|
a6f07fbaac | ||
|
|
f87876c1c3 | ||
|
|
ec3de1a072 | ||
|
|
c1d2c159f0 | ||
|
|
9f86e2ba43 | ||
|
|
d0be36dc9b | ||
|
|
af604a1477 | ||
|
|
c981cff450 | ||
|
|
ab1033c26c | ||
|
|
01706df7c8 | ||
|
|
827a7c1cf1 | ||
|
|
d14d9933b4 | ||
|
|
af946893f0 | ||
|
|
245ae5e987 | ||
|
|
29f3d711c6 | ||
|
|
3909936cc8 | ||
|
|
0602c91c54 | ||
|
|
a7bdb02a2b | ||
|
|
2cfaf5a119 | ||
|
|
20a48f05b3 | ||
|
|
4e70fb7f9d | ||
|
|
1f438dbf71 | ||
|
|
619d0e2bdd | ||
|
|
ec420c66d4 | ||
|
|
ac1cf633c7 | ||
|
|
6f25d2170d | ||
|
|
295a2ced32 | ||
|
|
cbb2ad018d | ||
|
|
7e15eabe65 | ||
|
|
eb9b5b535f | ||
|
|
a13351a9eb | ||
|
|
74fdf6beec | ||
|
|
cd7c88e5a5 | ||
|
|
a22b88ed89 | ||
|
|
5eb99fe5ef | ||
|
|
85e465cfa4 | ||
|
|
e101dde62e | ||
|
|
1980ffea30 | ||
|
|
0935e0a13d | ||
|
|
b2b68119fc | ||
|
|
4420057d4f | ||
|
|
bd2b8df7c5 | ||
|
|
068bbe0837 | ||
|
|
7e0e1fbaad | ||
|
|
9723476392 | ||
|
|
9dee2f2230 | ||
|
|
175f38414e | ||
|
|
8bbf8231d2 | ||
|
|
2fbbb802bd | ||
|
|
cb65a80dce | ||
|
|
3a9bcbcf6b | ||
|
|
e38018ab1e | ||
|
|
044c6474b2 | ||
|
|
5f42a4d95e | ||
|
|
6d3fd6da07 | ||
|
|
34e7996f2f | ||
|
|
f2d7a28258 | ||
|
|
ba6f8da1e4 | ||
|
|
0a5e79891d | ||
|
|
901bf63cb8 | ||
|
|
350b8efb0b | ||
|
|
2f69b4d190 | ||
|
|
57a15a4e96 | ||
|
|
13042002d7 | ||
|
|
8ac745b4e5 | ||
|
|
8a4f038ec4 | ||
|
|
890522f6f4 | ||
|
|
c8aa032051 | ||
|
|
9830c146d0 | ||
|
|
00c9164fdb | ||
|
|
a31e7118b4 | ||
|
|
2058ce3f3a | ||
|
|
61e3bf4a20 | ||
|
|
ea5bae711b | ||
|
|
44a2c5fdff | ||
|
|
3b43960779 | ||
|
|
89f27ec99a | ||
|
|
82cf4358ce | ||
|
|
0f9b117744 | ||
|
|
c420c21219 | ||
|
|
3bae4b0d93 | ||
|
|
1432bd7a81 | ||
|
|
bdcf930e25 | ||
|
|
2e42d74c59 | ||
|
|
bbf26fc3e8 | ||
|
|
6dcc42cdeb | ||
|
|
385b0aa07f | ||
|
|
f2b1ee3398 | ||
|
|
2a2c60201f | ||
|
|
53749156a7 | ||
|
|
ee12b34334 | ||
|
|
b5a4aefe8b | ||
|
|
c2d5cab1b8 | ||
|
|
f25af8f7d6 | ||
|
|
33648b083c | ||
|
|
eb79711e0a | ||
|
|
03ce6052cc | ||
|
|
7992bf4177 | ||
|
|
ddb390d5ec | ||
|
|
1500b1dc00 | ||
|
|
9263d3e82c | ||
|
|
02b7fd53ed | ||
|
|
3cd199e852 | ||
|
|
77bc22baf5 | ||
|
|
f57988761c | ||
|
|
7987d09438 | ||
|
|
262352f8bb | ||
|
|
e8c56a8e8e | ||
|
|
312691376a | ||
|
|
a9611c782b | ||
|
|
bcc70b36fd | ||
|
|
9ee3b1bfbd | ||
|
|
e80a83b30b | ||
|
|
d80e72dfcd | ||
|
|
d01c26e618 | ||
|
|
cab2e11ca9 | ||
|
|
b48994b1b3 | ||
|
|
fb48acddb2 | ||
|
|
7d627f7af4 | ||
|
|
f458491d88 | ||
|
|
de802f9d76 | ||
|
|
804e6ba586 | ||
|
|
af3fc82b77 | ||
|
|
3bb7036cfd | ||
|
|
d802eb485b | ||
|
|
b4dd9c2490 | ||
|
|
04ab198e95 | ||
|
|
c345700963 | ||
|
|
b21afcb78f | ||
|
|
7f65530b26 | ||
|
|
e230eeb486 | ||
|
|
b1b03f3a97 | ||
|
|
36ef6d8da2 | ||
|
|
28bb0dd3b0 | ||
|
|
79f8a8f909 | ||
|
|
bfe2513540 | ||
|
|
2536aef737 | ||
|
|
0661ccdea9 | ||
|
|
29f7015e58 | ||
|
|
7dc1b50d3b | ||
|
|
682133a56a | ||
|
|
76db732f1b | ||
|
|
64aa2b5cc3 | ||
|
|
1eed5fd271 | ||
|
|
dd7ffa4501 | ||
|
|
5e79da0c9c | ||
|
|
65fe753a58 | ||
|
|
17c835e672 | ||
|
|
fa5bcd0457 | ||
|
|
499d6312b7 | ||
|
|
20d5903696 | ||
|
|
f039221ec9 | ||
|
|
dc9f8762ca | ||
|
|
e5c69801a7 | ||
|
|
ab458368de | ||
|
|
8eb680fcc9 | ||
|
|
71b05972f4 | ||
|
|
7deeb8dba5 | ||
|
|
daad190188 | ||
|
|
7cc0052451 | ||
|
|
18f8e29af8 | ||
|
|
d78f6614cc | ||
|
|
53ac43202a | ||
|
|
bfb050c9cc | ||
|
|
97622b961c | ||
|
|
b7e7c11a57 | ||
|
|
59e11e4e8a | ||
|
|
39b0357517 | ||
|
|
878a12002c | ||
|
|
d3d2863d52 | ||
|
|
f3f23b43b5 | ||
|
|
006262ed8f | ||
|
|
519aef86fa | ||
|
|
8bd77c0d47 | ||
|
|
7a5c910cd5 | ||
|
|
4e34f2001c | ||
|
|
54b7e2c874 | ||
|
|
19f89f7122 | ||
|
|
9f25d72a4a | ||
|
|
3e99b71e0f | ||
|
|
fb3b690c5f | ||
|
|
18fe553d23 | ||
|
|
e7ff6a2191 | ||
|
|
ad15e5a234 | ||
|
|
094c7aa438 | ||
|
|
472ed5a7d3 | ||
|
|
f284300870 | ||
|
|
ab5f4410b6 | ||
|
|
a400a3dd09 | ||
|
|
c4855e8123 | ||
|
|
3abb657777 | ||
|
|
4dd63d8f72 | ||
|
|
6c0bdec2c9 | ||
|
|
d6dd1f269a | ||
|
|
2bd23f3709 | ||
|
|
79dc464b83 | ||
|
|
3dbd5b11d8 | ||
|
|
549fd5fe29 | ||
|
|
0ab5c65185 | ||
|
|
5f4753f881 | ||
|
|
e03a45f8c5 | ||
|
|
67c2d53e59 | ||
|
|
7b01dd9794 | ||
|
|
04e50ff02c | ||
|
|
e57ccdc858 | ||
|
|
a73a65b48a | ||
|
|
781c805e10 | ||
|
|
7c4af88911 | ||
|
|
0aca173e96 | ||
|
|
60db57a41c | ||
|
|
df68f7dcb9 | ||
|
|
e95e3cc29d | ||
|
|
67e9310cc1 | ||
|
|
94984df047 | ||
|
|
861bf953bf | ||
|
|
06eaac8690 | ||
|
|
6b9e8816d1 | ||
|
|
cf9f715a28 | ||
|
|
08e043fea7 | ||
|
|
54ba1009d3 | ||
|
|
4a6a46fd7b | ||
|
|
0eb7414f72 | ||
|
|
c02691a70c | ||
|
|
8742957f75 | ||
|
|
ace372cfc6 | ||
|
|
27c5f5f49f | ||
|
|
ec5a1a3977 | ||
|
|
abc653c87d | ||
|
|
30a128c00b | ||
|
|
77c7963bde | ||
|
|
b80c1343c3 | ||
|
|
e5c5fbcea3 | ||
|
|
fc7d04ce73 | ||
|
|
35f57b3ed2 | ||
|
|
ed9ade05ea | ||
|
|
f485b729bc | ||
|
|
64f7953423 | ||
|
|
fc1d837501 | ||
|
|
6a238818da | ||
|
|
9800c43bfa | ||
|
|
b3cda39b0b | ||
|
|
7e1b7a2fcd | ||
|
|
ccae8d42fd | ||
|
|
38f78286c7 | ||
|
|
c3a53a6ec5 | ||
|
|
2bbe6a2ad8 | ||
|
|
f64f63f675 | ||
|
|
f0fb44f6d4 | ||
|
|
fa972615e6 | ||
|
|
f7cb2ef25d | ||
|
|
949f8bb2e7 | ||
|
|
5eaff802d4 | ||
|
|
b37d428726 | ||
|
|
a6336149f8 | ||
|
|
12f5ec40b5 | ||
|
|
6a672cf8bc | ||
|
|
036b7d9f66 | ||
|
|
ca6bce3820 | ||
|
|
9b4f2297b0 | ||
|
|
355c4bc58e | ||
|
|
a31b2876d0 | ||
|
|
b697b6e57b | ||
|
|
4792fe6391 | ||
|
|
7408f72de2 | ||
|
|
203a3cfdd1 | ||
|
|
3a029686e3 | ||
|
|
9fa7b3956d | ||
|
|
b888ace508 | ||
|
|
2e54efe34d | ||
|
|
94d7f33a03 | ||
|
|
11598295cf | ||
|
|
383c3769aa | ||
|
|
caccba18e9 | ||
|
|
95ce00e551 | ||
|
|
85dad58cfe | ||
|
|
acdc70f72a | ||
|
|
bc9db72212 | ||
|
|
7905f81d7e | ||
|
|
2ef9cc6a1c | ||
|
|
2357b69a5f | ||
|
|
98b47f866b | ||
|
|
d2dfb0d15d | ||
|
|
63339c026d | ||
|
|
40922b88f7 | ||
|
|
674c617c16 | ||
|
|
2884b6deca | ||
|
|
720862dafb | ||
|
|
5120b4b468 | ||
|
|
c5f5d4543e | ||
|
|
baf92da9f2 | ||
|
|
526c9c3d73 | ||
|
|
119d87b795 | ||
|
|
17b030cca1 | ||
|
|
81c9b3f327 | ||
|
|
50198a2159 | ||
|
|
20c8b1eaf6 | ||
|
|
faf94b033d | ||
|
|
342b3d3d08 | ||
|
|
26d43f193e | ||
|
|
f04ae91c82 | ||
|
|
bb5515113f | ||
|
|
c09d84efc6 | ||
|
|
2a99a2a597 | ||
|
|
08f6dcf08d | ||
|
|
284d78c6b2 | ||
|
|
b8b5fb9eae | ||
|
|
697ee62d9a | ||
|
|
20ac0edda8 | ||
|
|
a1bbb06e2f | ||
|
|
a87cc5c3b1 | ||
|
|
5ff740cddc | ||
|
|
db9229f485 | ||
|
|
ace6c01b00 | ||
|
|
2f4d460c04 | ||
|
|
d5401ae3cb | ||
|
|
3482a7f1f1 | ||
|
|
23762e0639 | ||
|
|
50cc15af3b | ||
|
|
a0f0367e60 | ||
|
|
3f08ddefe9 | ||
|
|
debe656f9f | ||
|
|
4350b6402f | ||
|
|
5d2892f6fc | ||
|
|
91f27ddd66 | ||
|
|
6a674d0d67 | ||
|
|
0d165487b8 | ||
|
|
bacc5836ea | ||
|
|
230931a2c9 | ||
|
|
61d289f414 | ||
|
|
44255862a9 | ||
|
|
ed9577150b | ||
|
|
80319e7087 | ||
|
|
379048d8fc | ||
|
|
9ac655d774 | ||
|
|
9b17470129 | ||
|
|
5c114fc6fe | ||
|
|
efb12f41b2 | ||
|
|
813edcfe0a | ||
|
|
dea1405d71 | ||
|
|
e7f91b7529 | ||
|
|
5e8a64508e | ||
|
|
21ebb7a585 | ||
|
|
6edd430538 | ||
|
|
34606e0f9f | ||
|
|
5f62f9edc7 | ||
|
|
0d2ff55c07 | ||
|
|
207ee8667b | ||
|
|
5984ed921e | ||
|
|
aac5fb8c24 | ||
|
|
8cfc8b3132 | ||
|
|
0b5ebd7b1d | ||
|
|
35141a4aef | ||
|
|
e702234fdc | ||
|
|
05dbe23563 | ||
|
|
fd07f59d16 | ||
|
|
36de840c91 | ||
|
|
b36ffe1c9d | ||
|
|
1f1451e666 | ||
|
|
1000839309 | ||
|
|
ed4ed5fdcd | ||
|
|
eaf1f7cc13 | ||
|
|
ddfc8c2fa4 | ||
|
|
91afa7f7f4 | ||
|
|
da49e71eb8 | ||
|
|
5b8ce8ebae | ||
|
|
827bcd898e | ||
|
|
54a4add40f | ||
|
|
0093eb1220 | ||
|
|
b56ff7678c | ||
|
|
431197042d | ||
|
|
295876e1d1 | ||
|
|
e8f975e44f | ||
|
|
bc026ab49a | ||
|
|
f56457271d | ||
|
|
bc5250c899 | ||
|
|
971511e1c5 | ||
|
|
51317dca70 | ||
|
|
91dda08165 | ||
|
|
67bfa5bc90 | ||
|
|
0841f4a27d | ||
|
|
c6edf8421f | ||
|
|
97fb90c37a | ||
|
|
1d00bdd2a4 | ||
|
|
0ff01871f3 | ||
|
|
e00a675e6e | ||
|
|
d5e63b875b | ||
|
|
a31ccef99b | ||
|
|
375fd9140b | ||
|
|
fbb2c213a9 | ||
|
|
d323de3c21 | ||
|
|
18872cc8ae | ||
|
|
33085e3798 | ||
|
|
e42c811797 | ||
|
|
6b321e4d02 | ||
|
|
e6b17c701d | ||
|
|
60bda7ca25 | ||
|
|
b37d2ec6d1 | ||
|
|
751ccd02f6 | ||
|
|
431d04bd40 | ||
|
|
e756f07cad | ||
|
|
0ef548b9c9 | ||
|
|
b9e025b50e | ||
|
|
7743f0e05f | ||
|
|
a1581ed5b5 | ||
|
|
7a83a71a02 | ||
|
|
a6c30088ab | ||
|
|
fdbe950483 | ||
|
|
2f86326405 | ||
|
|
8aaf4cfaf0 | ||
|
|
8a62d13cee | ||
|
|
5a45471c68 | ||
|
|
6dc21bf433 | ||
|
|
1da24c68ca | ||
|
|
3d3b7a5672 | ||
|
|
6c4ea7a957 | ||
|
|
f221a6d953 | ||
|
|
de95859e0d | ||
|
|
bcf71c0fee | ||
|
|
8fe0228d4d | ||
|
|
37ea8a28ad | ||
|
|
a689317696 | ||
|
|
14a5c12404 | ||
|
|
35b2968b60 | ||
|
|
dac932f553 | ||
|
|
7e0ae6112c | ||
|
|
9ad1ccf19f | ||
|
|
5b7599e1b0 | ||
|
|
19c44b83d2 | ||
|
|
41691957d2 | ||
|
|
e3182624fd | ||
|
|
b8b3e6f9cd | ||
|
|
520c7286c4 | ||
|
|
d547829c5f | ||
|
|
49459cfc9b | ||
|
|
827ba446ab | ||
|
|
e5316221bb | ||
|
|
357174e351 | ||
|
|
5fc2f3236e | ||
|
|
a3484cce43 | ||
|
|
cdd107b06b | ||
|
|
6619b584bc | ||
|
|
d798b0cabb | ||
|
|
29bfd77855 | ||
|
|
c24dc6609d | ||
|
|
b5e7e9fc5c | ||
|
|
58dec7334b | ||
|
|
0b7af28c86 | ||
|
|
725f99ea4c | ||
|
|
526259ef8e | ||
|
|
4b43cac093 | ||
|
|
3795c14931 | ||
|
|
3e5cdd8bf2 | ||
|
|
35058a1dde | ||
|
|
0159e26631 | ||
|
|
11d93b1883 | ||
|
|
f86f392641 | ||
|
|
19cb1ae8e1 | ||
|
|
cd8befe43e | ||
|
|
db46204ff9 | ||
|
|
d2290cc82c | ||
|
|
f8e06f9dbc | ||
|
|
e68ca761a9 | ||
|
|
fc6f3993bd | ||
|
|
f8b074f696 | ||
|
|
66f14099ed | ||
|
|
f3dc681f61 | ||
|
|
62f62e1fd5 | ||
|
|
f3360e61e1 | ||
|
|
5208dcd67e | ||
|
|
6f7d4a7a01 | ||
|
|
958a3bbd99 | ||
|
|
d5c85a3d53 | ||
|
|
5e7b527af2 | ||
|
|
ddbd33a8e3 | ||
|
|
a7dc368191 | ||
|
|
5b1d0c55a0 | ||
|
|
58515ca36e | ||
|
|
2d753ebf1a | ||
|
|
50cd716220 | ||
|
|
47c97e8191 | ||
|
|
d704ba175f | ||
|
|
c1036c5e4f | ||
|
|
356d06915b | ||
|
|
2b4fd738da | ||
|
|
f9de6df426 | ||
|
|
8ee409d9bd | ||
|
|
0cf3de4541 | ||
|
|
3a79be7426 | ||
|
|
30589bb580 | ||
|
|
e07b18bb18 | ||
|
|
63e3f441d8 | ||
|
|
b31eb3db82 | ||
|
|
99ae38c9a6 | ||
|
|
011f96c6a7 | ||
|
|
c7b4136d31 | ||
|
|
da2f98fa8f | ||
|
|
7ac532a1d0 | ||
|
|
b98ab70517 | ||
|
|
b1d453a0c2 | ||
|
|
a51f63267a | ||
|
|
0bdc84d42a | ||
|
|
7f74ae7912 | ||
|
|
abf13c7026 | ||
|
|
7d7c5ce63e | ||
|
|
547c9e7f5a | ||
|
|
3e4d8a3ffe | ||
|
|
74c3b0d079 | ||
|
|
846aa98a9c | ||
|
|
17b7789531 | ||
|
|
b86d9bf1ab | ||
|
|
6e06ce2af1 | ||
|
|
dc275d7009 | ||
|
|
a67b9f566a | ||
|
|
9344289262 | ||
|
|
6713282a43 | ||
|
|
f834626f00 | ||
|
|
98b80c59d9 | ||
|
|
1beaf79e47 | ||
|
|
0a475bd4cc | ||
|
|
0820c484d6 | ||
|
|
dbf39403d3 | ||
|
|
14dde9ec32 | ||
|
|
8e656c1fd7 | ||
|
|
d55c68c248 | ||
|
|
3c266dce8c | ||
|
|
8ec3ab5e28 | ||
|
|
f7be34f5bb | ||
|
|
58a30f7282 | ||
|
|
2b5c2874ab | ||
|
|
64b47ca3c7 | ||
|
|
bed11f4214 | ||
|
|
bbf7646751 | ||
|
|
6dfefb95e8 | ||
|
|
44c2861aba | ||
|
|
3039046a6d | ||
|
|
e39974eb07 | ||
|
|
76895c76a1 | ||
|
|
a7e0f03647 | ||
|
|
98e8ef4e11 | ||
|
|
5635ba1fd0 | ||
|
|
2ffe9b53f7 | ||
|
|
dd0eee6418 | ||
|
|
405c2889f0 | ||
|
|
40cf392504 | ||
|
|
42baa03985 | ||
|
|
016f20b2d4 | ||
|
|
83ef93f3fb | ||
|
|
d5faf31d43 | ||
|
|
12324e3550 | ||
|
|
ff67499562 | ||
|
|
2ba5abc2bb | ||
|
|
f7dfe9dd23 | ||
|
|
3d4a8aaad1 | ||
|
|
ac145052d6 | ||
|
|
4ded1f6193 | ||
|
|
7a41337d17 | ||
|
|
2d006dffaf | ||
|
|
940a0a67a5 | ||
|
|
69c935a6e8 | ||
|
|
be304365a0 | ||
|
|
676127893f | ||
|
|
df62d512cd | ||
|
|
fc00767cf8 | ||
|
|
2163b39268 | ||
|
|
aa61cbd574 | ||
|
|
2a89d53d7d | ||
|
|
f7ce6a1fdb | ||
|
|
20b35f3139 | ||
|
|
46d454730f | ||
|
|
c2ad4c5633 | ||
|
|
6e0795253e | ||
|
|
9956d422f2 | ||
|
|
57e78e2bec | ||
|
|
5aa58e6043 | ||
|
|
64e3cf04cc | ||
|
|
4f9185e3fe | ||
|
|
4dbccea5df | ||
|
|
a412268154 | ||
|
|
5860492a38 | ||
|
|
d79ac3c12c | ||
|
|
85c309eee0 | ||
|
|
f782fb569f | ||
|
|
c50968a0a4 | ||
|
|
99b482b363 | ||
|
|
ce5dd847e9 | ||
|
|
5c0383059e | ||
|
|
6f672f9bc1 | ||
|
|
e74b6cbcaf | ||
|
|
32a82f2d0e | ||
|
|
633df0cdaf | ||
|
|
32a12578f3 | ||
|
|
11b655154c | ||
|
|
19ceb424c3 | ||
|
|
e21a204eed | ||
|
|
145a527626 | ||
|
|
b33bbdb3f8 | ||
|
|
704e6a8676 | ||
|
|
4c6a8f29c0 | ||
|
|
5dfe1d28ff | ||
|
|
5744ed8994 | ||
|
|
c71d9b126f | ||
|
|
5b437d2199 | ||
|
|
0125e67b35 | ||
|
|
1472e35011 | ||
|
|
58ea422ea2 | ||
|
|
3097565664 | ||
|
|
81134b408d | ||
|
|
26fdd5da03 | ||
|
|
6a5df80bda | ||
|
|
1e2344341d | ||
|
|
b712ba1e04 | ||
|
|
9ae6435317 | ||
|
|
81e8ba223f | ||
|
|
b13baccbd2 | ||
|
|
82f2f7b2b9 | ||
|
|
f48a4373d1 | ||
|
|
ab9d1777a1 | ||
|
|
6a34995796 | ||
|
|
df25f84011 | ||
|
|
cead9579be | ||
|
|
a23c0237c3 | ||
|
|
5c3efbdca0 | ||
|
|
176ee73efe | ||
|
|
54833195ed | ||
|
|
8960a3a898 | ||
|
|
8184d29d4a | ||
|
|
fef91b5339 | ||
|
|
24de98e188 | ||
|
|
6291a89665 | ||
|
|
a45e5ac152 | ||
|
|
6b5f35e574 | ||
|
|
244a679f90 | ||
|
|
1a9909e3e2 | ||
|
|
6655974ef3 | ||
|
|
cdf4fb605a | ||
|
|
bca31c7335 | ||
|
|
7652b44c21 | ||
|
|
6fce798164 | ||
|
|
7801dcd849 | ||
|
|
2eed101a93 | ||
|
|
3d25620d13 | ||
|
|
6e89b036e2 | ||
|
|
8b5992e43f | ||
|
|
548b4aea40 | ||
|
|
42b64f826b | ||
|
|
95379b0c5a | ||
|
|
f5918a3c14 | ||
|
|
1df752cd33 | ||
|
|
a1f5d90323 | ||
|
|
5289a380f2 | ||
|
|
914c88454d | ||
|
|
46363f4129 | ||
|
|
bd19b86259 | ||
|
|
db2e7e4ba1 | ||
|
|
760f419e8f | ||
|
|
58ae0cd113 | ||
|
|
4809c113b3 | ||
|
|
6d0db90f43 | ||
|
|
75fb8f6ebf | ||
|
|
26739463a4 | ||
|
|
130cf6eb2b | ||
|
|
5054805ac6 | ||
|
|
ca94cfffe7 | ||
|
|
396df6bcda | ||
|
|
58dafbfa9d | ||
|
|
8516ab7c69 | ||
|
|
2284bc2120 | ||
|
|
31a490bfe6 | ||
|
|
0b5f5fd74e | ||
|
|
9a7abe68fd | ||
|
|
2ea27ec08f | ||
|
|
322441d5c8 | ||
|
|
7c7c302920 | ||
|
|
69986e77dd | ||
|
|
8182022d52 | ||
|
|
b075e93319 | ||
|
|
fd6d15e712 | ||
|
|
f399c3a888 | ||
|
|
79d1c404a7 | ||
|
|
1e83e7a95c | ||
|
|
086ab9d126 | ||
|
|
8a7821a15b | ||
|
|
135c34a45a | ||
|
|
ca6e54890c | ||
|
|
dac219b64a | ||
|
|
790a1757e0 | ||
|
|
aafa0f6783 | ||
|
|
df5426dd8b | ||
|
|
3803d79835 | ||
|
|
ceff3bed28 | ||
|
|
eebdbe226b | ||
|
|
8ab639ddaa | ||
|
|
812579a139 | ||
|
|
08f31886d6 | ||
|
|
f080e5aa5c | ||
|
|
d61c719e45 | ||
|
|
4d49bf3ab9 | ||
|
|
f34f837106 | ||
|
|
1326994a6d | ||
|
|
99ab0d3abc | ||
|
|
e61f9afb72 | ||
|
|
29c64b1f40 | ||
|
|
db51b772b4 | ||
|
|
0b764a2a27 | ||
|
|
a93a3e98b9 | ||
|
|
be94dcc8a3 | ||
|
|
a34b31ad12 | ||
|
|
e1d07aa264 | ||
|
|
0d79a8fc43 | ||
|
|
5d1411830b | ||
|
|
dc91ce9ec6 | ||
|
|
36a857f343 | ||
|
|
c1c5450caf | ||
|
|
0b4e8b8754 | ||
|
|
f73dcba306 | ||
|
|
147a537b82 | ||
|
|
a4844b8ee7 | ||
|
|
acde6cb0ee | ||
|
|
b4e13215c7 | ||
|
|
4ecaa0f508 | ||
|
|
86b7e4a6db | ||
|
|
aa19028583 | ||
|
|
a6e2296e0a | ||
|
|
8c5248da43 | ||
|
|
59859c4aed | ||
|
|
0e85783257 | ||
|
|
d8e8cd6e51 | ||
|
|
bbc91c1273 | ||
|
|
67f974923b | ||
|
|
4f00dab4c4 | ||
|
|
74e29c8990 | ||
|
|
7fb597a8bf | ||
|
|
c50c90ea8d | ||
|
|
74883c5da8 | ||
|
|
375af0a7b9 | ||
|
|
8c9679b19c | ||
|
|
d220542491 | ||
|
|
0a80fdcc1d | ||
|
|
5fbcb3f905 | ||
|
|
067f96023a | ||
|
|
9de7c9ed0d | ||
|
|
0de292c0d4 | ||
|
|
eaa5385069 | ||
|
|
fa2aa1f791 | ||
|
|
181ce4cc60 | ||
|
|
65578211ef | ||
|
|
78df99e7e7 | ||
|
|
90ed76853c | ||
|
|
26f4a41900 | ||
|
|
a3b4d6cdbf | ||
|
|
686500b181 | ||
|
|
84c5c52090 | ||
|
|
d5de0b41f9 | ||
|
|
bd41d114b5 | ||
|
|
94d2f84364 | ||
|
|
bf2b456af1 | ||
|
|
4961e83686 | ||
|
|
f70dc6660a | ||
|
|
2bb84ae28b | ||
|
|
360c0cb642 | ||
|
|
e319f54d3f | ||
|
|
1ce9551ba6 | ||
|
|
4c566c8587 | ||
|
|
74eef4deda | ||
|
|
2576e819a7 | ||
|
|
cdab6a71b7 | ||
|
|
45293fdcac | ||
|
|
e2fec3d007 | ||
|
|
7656752d87 | ||
|
|
d0ed225184 | ||
|
|
12ed5cc9ee | ||
|
|
f8639a57e7 | ||
|
|
c2147f80b7 | ||
|
|
f1b9cf4fc0 | ||
|
|
c1d799a06c | ||
|
|
6a5044fc70 | ||
|
|
7fa568872c | ||
|
|
f3ad81d554 | ||
|
|
fc9a1c8664 | ||
|
|
00ecff09f7 | ||
|
|
278807a199 | ||
|
|
8bce900170 | ||
|
|
cf6d6d0cd3 | ||
|
|
94af02fbf4 | ||
|
|
9e65cc20cb | ||
|
|
25c4a2961e | ||
|
|
755a949f6d | ||
|
|
42debde746 | ||
|
|
c2d5b35cd6 | ||
|
|
f4e7c85219 | ||
|
|
073836905a | ||
|
|
fd2a59417d | ||
|
|
6d7e784c25 | ||
|
|
b2a24d1cbe | ||
|
|
0a2701bbbc | ||
|
|
475daa82b3 | ||
|
|
c3b653b1b9 | ||
|
|
1a11b989e7 | ||
|
|
010792f714 | ||
|
|
536369bc12 | ||
|
|
f55655216f | ||
|
|
43ef3b8515 | ||
|
|
3180c67151 | ||
|
|
457e162d0c | ||
|
|
ed3207afe7 | ||
|
|
8e5b0ceceb | ||
|
|
f9127e90db | ||
|
|
94db0beb25 | ||
|
|
4a1bb1a33a | ||
|
|
77996158ef | ||
|
|
93984e1955 | ||
|
|
67aae051d6 | ||
|
|
6cc7749cf2 | ||
|
|
c47e57cc29 | ||
|
|
de6f5a93e8 | ||
|
|
157910ec9a | ||
|
|
b85b90ef4e | ||
|
|
1d1ebc103d | ||
|
|
eee13e7dd9 | ||
|
|
7cc53c4c68 | ||
|
|
9c22066bdb | ||
|
|
f241e15ec1 | ||
|
|
881ece3553 | ||
|
|
281bf02871 | ||
|
|
778038fdd3 | ||
|
|
e7ee72836d | ||
|
|
31c0506b85 | ||
|
|
7d6f85424c | ||
|
|
ca89cfee3d | ||
|
|
5a37f652c2 | ||
|
|
cc7b781bb3 | ||
|
|
55051f0194 | ||
|
|
78e0e79b26 | ||
|
|
b7e8938ab4 | ||
|
|
f8327c3c6c | ||
|
|
e3851e6e55 | ||
|
|
6a136eebb2 | ||
|
|
9396c046cd | ||
|
|
4958b8d37a | ||
|
|
e2389a4061 | ||
|
|
dccd199369 | ||
|
|
a87be89095 | ||
|
|
5c9a13d343 | ||
|
|
bfe07fc71a | ||
|
|
fa95b67c92 | ||
|
|
728203912e | ||
|
|
27289e7b7b | ||
|
|
669a0db528 | ||
|
|
4da999ba86 | ||
|
|
5e9bd5424a | ||
|
|
3bffd871b8 | ||
|
|
2d450ac39d | ||
|
|
6fe69590c4 | ||
|
|
4df3d7f150 | ||
|
|
51128a6705 | ||
|
|
e7676979ee | ||
|
|
da36c78942 | ||
|
|
77358140d9 | ||
|
|
1701bc3e92 | ||
|
|
ba0ffd225e | ||
|
|
a14d4d9f8e | ||
|
|
db6ee9954c | ||
|
|
792c4c8de4 | ||
|
|
820307dd4e | ||
|
|
88144ac7f8 | ||
|
|
da7ac78694 | ||
|
|
10dbd9ad4c | ||
|
|
9779e06f66 | ||
|
|
f861d22593 | ||
|
|
e3f0565517 | ||
|
|
9a91b3ce9c | ||
|
|
c88361473c | ||
|
|
a2e1a2d375 | ||
|
|
536ce992fc | ||
|
|
8d57b85a3a | ||
|
|
ce37b622eb | ||
|
|
d5e510f778 | ||
|
|
be8e414d5a | ||
|
|
65e94f624b | ||
|
|
ed4fb14f52 | ||
|
|
32cce0c636 | ||
|
|
34bb7ae84a | ||
|
|
5389e31190 | ||
|
|
f0915221d1 | ||
|
|
aa4efd63ef | ||
|
|
a12d63bb12 | ||
|
|
062a25a032 | ||
|
|
9e6d08c606 | ||
|
|
976641c3b6 | ||
|
|
2f80f12001 | ||
|
|
647b755b64 | ||
|
|
26dc83dbc2 | ||
|
|
3c107d675a | ||
|
|
a20ca49344 | ||
|
|
aa8466f688 | ||
|
|
2980586e76 | ||
|
|
dba116bae1 | ||
|
|
0b7d462adf | ||
|
|
6f8622a1a0 | ||
|
|
af049efb40 | ||
|
|
ad4e4ce6b8 | ||
|
|
4c9b0be117 | ||
|
|
a9dae46110 | ||
|
|
53628fdeff | ||
|
|
e34093c328 | ||
|
|
9131b46263 | ||
|
|
2a9c3fc64a | ||
|
|
c44c61dc83 | ||
|
|
11663dd29f | ||
|
|
90b7c3625d | ||
|
|
4e29dba5cd | ||
|
|
f1b5a00cb3 | ||
|
|
1a4b38a10c | ||
|
|
9ab2da8deb | ||
|
|
66473e6021 | ||
|
|
a9229b9197 | ||
|
|
7b9e59833e | ||
|
|
5baddb83cb | ||
|
|
4e56247ed9 | ||
|
|
ae2348875b | ||
|
|
fafaeef07d | ||
|
|
5b62b8c1e3 | ||
|
|
7c17c8aec3 | ||
|
|
64408b126e | ||
|
|
2a1f090a88 | ||
|
|
e2a2a8f18f | ||
|
|
dafa30725e | ||
|
|
24b9a56e0a | ||
|
|
a3d32068d2 | ||
|
|
e7f6682593 | ||
|
|
3f4b385b0f | ||
|
|
5fc16a8267 | ||
|
|
68afbe31b0 | ||
|
|
ba3451e9ca | ||
|
|
25c9180fdf | ||
|
|
e03a4bb176 | ||
|
|
4d594458d2 | ||
|
|
38176dcbb6 | ||
|
|
854d83f294 | ||
|
|
358ae3a936 | ||
|
|
f3131fab57 | ||
|
|
397f1398ee | ||
|
|
2dbf83889b | ||
|
|
9b2c5b8466 | ||
|
|
1f4b943454 | ||
|
|
2ca0fb93c9 | ||
|
|
47273d424f | ||
|
|
f4d6e028d4 | ||
|
|
f10e0e4add | ||
|
|
66e58937ca | ||
|
|
62c1cad676 | ||
|
|
ca523c716c | ||
|
|
e2deff09a6 | ||
|
|
19ddb65036 | ||
|
|
61c7c034e7 | ||
|
|
4c5920a50a | ||
|
|
c3c40b15f1 | ||
|
|
2fa65f0a22 | ||
|
|
af7ca99dc4 | ||
|
|
2d224d423d | ||
|
|
e7088e5daf | ||
|
|
d65409e97e | ||
|
|
70110e483c | ||
|
|
a0876becbc | ||
|
|
1afdac4d2c | ||
|
|
c6fa9cb17e | ||
|
|
aa3f35e38c | ||
|
|
db77570779 | ||
|
|
9619a27f8c | ||
|
|
c13154eea9 | ||
|
|
1132d26d47 | ||
|
|
6e5995964d | ||
|
|
da13238368 | ||
|
|
0865f4c207 | ||
|
|
77bc56751b | ||
|
|
b32c017f33 | ||
|
|
a4a31e632b | ||
|
|
23ad547db4 | ||
|
|
4d887c8a55 | ||
|
|
a982146cbe | ||
|
|
73210b6a70 | ||
|
|
e4422205e8 | ||
|
|
9df7358ded | ||
|
|
9507ea93dd | ||
|
|
52d3bcfe8b | ||
|
|
b3979c9505 | ||
|
|
273a0fba47 | ||
|
|
7e34ec61f6 | ||
|
|
acf13dc157 | ||
|
|
91395d6728 | ||
|
|
ef4a227ae9 | ||
|
|
25589562cd | ||
|
|
7063ca51aa | ||
|
|
97498d26f7 | ||
|
|
36292216fd | ||
|
|
86448a1025 | ||
|
|
b4f53edd91 | ||
|
|
3a22d2e6a1 | ||
|
|
9a4bbb55da | ||
|
|
b476bd2afe | ||
|
|
fa73240812 | ||
|
|
a3dc2d0482 | ||
|
|
41834478d7 | ||
|
|
6832ae20b0 | ||
|
|
7ae06ef00d | ||
|
|
a1ac0313e5 | ||
|
|
d9c5d2f940 | ||
|
|
361ed1b4a6 | ||
|
|
dfcc9678f3 | ||
|
|
8b061fd555 | ||
|
|
01f7201a9e | ||
|
|
acd8bdec21 | ||
|
|
ded3d6b40b | ||
|
|
f7739ef04c | ||
|
|
4f82dc97fd | ||
|
|
16471d0a28 | ||
|
|
6386c1bd18 | ||
|
|
2e93a4c18e | ||
|
|
0abd134a0d | ||
|
|
18032015b9 | ||
|
|
e26e7e29f2 | ||
|
|
39a1f2163f | ||
|
|
4c519fc761 | ||
|
|
bbacaa42b8 | ||
|
|
2f35ad1d42 | ||
|
|
6d17fe5df5 | ||
|
|
907de67a40 | ||
|
|
499d65e715 | ||
|
|
efb0ead9ad | ||
|
|
85b4a05c9a | ||
|
|
1f3d054b09 | ||
|
|
4853a1da05 | ||
|
|
db8016112e | ||
|
|
4eb6d147b5 | ||
|
|
e2f8b6f9fd | ||
|
|
f3c4a9fc6d | ||
|
|
9634526a67 | ||
|
|
b67c102fe5 | ||
|
|
1e39b2bdcf | ||
|
|
d6443809bd | ||
|
|
80e56d67db | ||
|
|
24a4d6501e | ||
|
|
5e43f0fd4c | ||
|
|
13f1239a3e | ||
|
|
9463ceb873 | ||
|
|
7aab5cc5b4 | ||
|
|
2b61bc91df | ||
|
|
57a4371e9f | ||
|
|
1d2e6b4c56 | ||
|
|
2a0bc91f86 | ||
|
|
210b926064 | ||
|
|
9a089a111f | ||
|
|
710eabeff3 | ||
|
|
603f34e944 | ||
|
|
a3dff7e1e9 | ||
|
|
25486f7b4d | ||
|
|
c22d824db8 | ||
|
|
a44a3a4158 | ||
|
|
982c9d7650 | ||
|
|
7042c1506a | ||
|
|
76943f0b17 | ||
|
|
c4f516655d | ||
|
|
aaba681db1 | ||
|
|
56a169b50f | ||
|
|
32b2ec4038 | ||
|
|
ce5052305d | ||
|
|
0e94e3e07b | ||
|
|
3d72670105 | ||
|
|
f8718dcc5d | ||
|
|
fa6777004f | ||
|
|
e3212d462d | ||
|
|
f5186e74c0 | ||
|
|
bf814739ba | ||
|
|
00a7490a1d | ||
|
|
da41604229 | ||
|
|
d5a4326b9e | ||
|
|
835d99e6fe | ||
|
|
e4458372e8 | ||
|
|
a05c65c73b | ||
|
|
14d346db8b | ||
|
|
882f14d50c | ||
|
|
75646d4786 | ||
|
|
b93070f91a | ||
|
|
cbcd5da680 | ||
|
|
cf4ce0a07d | ||
|
|
78c4b06492 | ||
|
|
501f40d9c1 | ||
|
|
08463ea403 | ||
|
|
2ced8b9469 | ||
|
|
26c1bcf866 | ||
|
|
89ad7e1ba2 | ||
|
|
e6828e1aa5 | ||
|
|
c8718d85dc | ||
|
|
56863c3d0e | ||
|
|
e50b1f1fe7 | ||
|
|
5e10b901a3 | ||
|
|
a6a74001bd | ||
|
|
0b032386c1 | ||
|
|
8eb092c754 | ||
|
|
33921c2cfe | ||
|
|
6cc566fd3e | ||
|
|
491a4369d7 | ||
|
|
6666fd14ac | ||
|
|
e621aca77e | ||
|
|
3e7c20b4ee | ||
|
|
22446ffc19 | ||
|
|
f39791616d | ||
|
|
d39aaa86ee | ||
|
|
4fdb14293b | ||
|
|
4ede33468e | ||
|
|
ebbb5d379d | ||
|
|
379b8c4e6b | ||
|
|
bde445df9b | ||
|
|
41b0c2a03f | ||
|
|
02715f88d5 | ||
|
|
706f9ee352 | ||
|
|
646cf84b4a | ||
|
|
f593c2fbf2 | ||
|
|
bd24307c2b | ||
|
|
e0c7054f9b | ||
|
|
59550e0fcb | ||
|
|
af64c8e2af | ||
|
|
03ac7a86c6 | ||
|
|
80a232f44c | ||
|
|
d1041c17d3 | ||
|
|
cf7fff3da2 | ||
|
|
373d663b5e | ||
|
|
e86d255a47 | ||
|
|
5c44f74475 | ||
|
|
d649bdddc7 | ||
|
|
5c7763b075 | ||
|
|
e3ab396428 | ||
|
|
df31454c30 | ||
|
|
c8bd47e948 | ||
|
|
098fc81825 | ||
|
|
cab4a7e1ee | ||
|
|
6596c8b1a7 | ||
|
|
5559f1df2e | ||
|
|
156597a965 | ||
|
|
d0641062c0 | ||
|
|
cf4fffa394 | ||
|
|
b30de63f18 | ||
|
|
ff2d7451e2 | ||
|
|
910b57731d | ||
|
|
b8aa1d473b | ||
|
|
ce26f88ee2 | ||
|
|
bf73029d15 | ||
|
|
4dbd657a26 | ||
|
|
1524796408 | ||
|
|
7821412bf7 | ||
|
|
b72b7a9ed5 | ||
|
|
a3a7a19870 | ||
|
|
8b0089565f | ||
|
|
251c68e85a | ||
|
|
227cb11f25 | ||
|
|
6097dc8aaa | ||
|
|
8c6c692926 | ||
|
|
7554bd3913 | ||
|
|
b81fce32f6 | ||
|
|
3ea1448c52 | ||
|
|
f31a3c1261 | ||
|
|
9e32605183 | ||
|
|
9009e0eb7f | ||
|
|
243d445d1b | ||
|
|
65cd2b0e04 | ||
|
|
33b230c114 | ||
|
|
6389af6ea2 | ||
|
|
fcea6fcbf8 | ||
|
|
8b35f5e8fe | ||
|
|
7d5dcfafc9 | ||
|
|
07ee2df822 | ||
|
|
5b79e274e8 | ||
|
|
e25d1b8365 | ||
|
|
af40ebcab4 | ||
|
|
3fd146557a | ||
|
|
d427c99161 | ||
|
|
d1a887857d | ||
|
|
3914c4c018 | ||
|
|
f6ffd604dd | ||
|
|
18a4c14670 | ||
|
|
0f67dbd8bd | ||
|
|
3bd5ae56f0 | ||
|
|
ecfc5d091d | ||
|
|
84238a0262 | ||
|
|
edf3ec9f09 | ||
|
|
2fee0f0184 | ||
|
|
a5ddb63fca | ||
|
|
a05a3c4b26 | ||
|
|
a938608cc8 | ||
|
|
013380c7e7 | ||
|
|
13c0b62f96 | ||
|
|
7276fc1d4b | ||
|
|
67a6961b4b | ||
|
|
d82578d895 | ||
|
|
c786e7eb5a | ||
|
|
91099694c9 | ||
|
|
c9a20ae10c | ||
|
|
f05225da22 | ||
|
|
badfdee5e4 | ||
|
|
ba1e75405f | ||
|
|
0f5f671054 | ||
|
|
5b9d350002 | ||
|
|
6403e7a11c | ||
|
|
312122e1be | ||
|
|
bb1da63bfc | ||
|
|
d1b2e74985 | ||
|
|
c0b2fe3e3b | ||
|
|
864461918f | ||
|
|
9127eb8acf | ||
|
|
8e629d9471 | ||
|
|
ab943d938a | ||
|
|
d6fedc1e6e | ||
|
|
3222478449 | ||
|
|
26cb65c357 | ||
|
|
1ef3bd3a8b | ||
|
|
51ecf05091 | ||
|
|
565454d3cb | ||
|
|
906ff210fe | ||
|
|
c4565f6ed3 | ||
|
|
8dd9020803 | ||
|
|
ec62029db5 | ||
|
|
55fbffef75 | ||
|
|
c84c5dbcdf | ||
|
|
6725c8355f | ||
|
|
c659e3448a | ||
|
|
959e5f0082 | ||
|
|
6488b0d11c | ||
|
|
039a2030db | ||
|
|
33d85aaee5 | ||
|
|
d4c1766532 | ||
|
|
39a6d1c87e | ||
|
|
ee2a66323e | ||
|
|
635a3e5b26 | ||
|
|
82da45a638 | ||
|
|
64ac7745fe | ||
|
|
5690902173 | ||
|
|
4162dcafd3 | ||
|
|
9253d45968 | ||
|
|
ea59c6c065 | ||
|
|
7c741d6f7e | ||
|
|
99dc70ea7a | ||
|
|
8a4b0d536e | ||
|
|
b78d4f05d0 | ||
|
|
c76f640e2e | ||
|
|
a9e3320e7c | ||
|
|
b472da18f0 | ||
|
|
c58cc449c0 | ||
|
|
af9668f130 | ||
|
|
08f3d5b8e8 | ||
|
|
0d5aad3e90 | ||
|
|
fcd79a78af | ||
|
|
9f784b3c31 | ||
|
|
657bbbffbc | ||
|
|
7780ac1078 | ||
|
|
76a12edf57 | ||
|
|
7387caa2eb | ||
|
|
576dd69b0b | ||
|
|
5e6b107d01 | ||
|
|
f444ae9224 | ||
|
|
8f5d1eec11 | ||
|
|
9a6afcd236 | ||
|
|
5f95de19ce | ||
|
|
8fb00ff206 | ||
|
|
94a8338be5 | ||
|
|
00744fb9b4 | ||
|
|
8250e05618 | ||
|
|
12c17d39cc | ||
|
|
409961fff3 | ||
|
|
159d47a934 | ||
|
|
4b11874390 | ||
|
|
3c7cff1c9c | ||
|
|
e3574f243f | ||
|
|
bdda9d03ae | ||
|
|
ae1805de76 | ||
|
|
0ce123e63a | ||
|
|
9ae45f6674 | ||
|
|
6c3aa99595 | ||
|
|
215d96fdc3 | ||
|
|
2c73fd6dde | ||
|
|
3b27413887 | ||
|
|
eb9e450cf1 | ||
|
|
72039bbe5a | ||
|
|
d6bceb139e | ||
|
|
a311623bb9 | ||
|
|
95d72a57e8 | ||
|
|
aa8d9eab53 | ||
|
|
17ee829299 | ||
|
|
5db39466e8 | ||
|
|
43b8840dd2 | ||
|
|
cc4ab80f62 | ||
|
|
0e47cee661 | ||
|
|
59e667801d | ||
|
|
d6dca80dd7 | ||
|
|
74c391f19c | ||
|
|
ae8172ec97 | ||
|
|
d6ab78ff03 | ||
|
|
7e36dc01e8 | ||
|
|
9020c84f81 | ||
|
|
d0a1514ab6 | ||
|
|
06c3a0cf2a | ||
|
|
dde5782625 | ||
|
|
a1913130e7 | ||
|
|
369bd0f4dc | ||
|
|
3b4a47a961 | ||
|
|
f29ee3ad05 | ||
|
|
19a3fc6b9e | ||
|
|
0a287217b6 | ||
|
|
043d15355c | ||
|
|
2c4cebe9d9 | ||
|
|
9ec1ce3225 | ||
|
|
cedf9bb82d | ||
|
|
1b4f83c067 | ||
|
|
c126c403e7 | ||
|
|
90b9b3c1bf | ||
|
|
1a17b41bc3 | ||
|
|
2e5e0a7730 | ||
|
|
b2f62cc635 | ||
|
|
2a9f8a3d75 | ||
|
|
0a553d6630 | ||
|
|
e51c240ea1 | ||
|
|
acd5b91eb8 | ||
|
|
f5201469a3 | ||
|
|
34aa2e32d8 | ||
|
|
bda245bfdb | ||
|
|
d1eb2917e8 | ||
|
|
9a631d0ecd | ||
|
|
c8032b6b25 | ||
|
|
68ca6ebe04 | ||
|
|
9f0cd93859 | ||
|
|
7cf0d59003 | ||
|
|
74f8c07674 | ||
|
|
1bbec56cc7 | ||
|
|
b86bae9c86 | ||
|
|
6d89867a74 | ||
|
|
ebf725d869 | ||
|
|
257774a093 | ||
|
|
6f798f723a | ||
|
|
5fbda6b094 | ||
|
|
b483be8c4b | ||
|
|
a751237164 | ||
|
|
0c87dcd656 | ||
|
|
b1bd1d4d6a | ||
|
|
e276f3e026 | ||
|
|
1e638be7ac | ||
|
|
acd2b9abeb | ||
|
|
6ddecfc764 | ||
|
|
f15ad71354 | ||
|
|
4d54af39b0 | ||
|
|
81d5a8f4f5 | ||
|
|
344119f42a | ||
|
|
84e5659090 | ||
|
|
514db8efdd | ||
|
|
0693ec55c6 | ||
|
|
56c720b078 | ||
|
|
bbeb7966b4 | ||
|
|
403b9654f1 | ||
|
|
84775e414b | ||
|
|
edb4911bf6 | ||
|
|
d913ca869c | ||
|
|
e3917121fa | ||
|
|
ede288ad38 | ||
|
|
58f14551dc | ||
|
|
97816d6c54 | ||
|
|
3732f968d0 | ||
|
|
939a095bf4 | ||
|
|
ad0d53f7fe | ||
|
|
e748ddc20d | ||
|
|
e51ecd73b3 | ||
|
|
5479d0a54d | ||
|
|
6830b27317 | ||
|
|
4399be7ad2 | ||
|
|
42d2a860eb | ||
|
|
3ec0200067 | ||
|
|
d1e4a8ec8a | ||
|
|
54b3061c50 | ||
|
|
19fe808dff | ||
|
|
b7281cc08d | ||
|
|
617c3883c7 | ||
|
|
9e20a5256b | ||
|
|
a3db33cfd9 | ||
|
|
aa8a86a0b9 | ||
|
|
7238192150 | ||
|
|
caa0e31be2 | ||
|
|
5a6cafc3da | ||
|
|
1df52350fa | ||
|
|
40323202f5 | ||
|
|
0d83f24661 | ||
|
|
8550469c9f | ||
|
|
e5679a787c | ||
|
|
e37a4314b3 | ||
|
|
7b5ee8fb65 | ||
|
|
cf5835f081 | ||
|
|
aea1a2b08c | ||
|
|
cdee7a5d6f | ||
|
|
21e07c0db9 | ||
|
|
06101d134b | ||
|
|
501108e3ae | ||
|
|
87fe10ab5d | ||
|
|
6c26a55b5a | ||
|
|
ca681bce13 | ||
|
|
ff838a14d9 | ||
|
|
c45b586a44 | ||
|
|
215312bb5f | ||
|
|
34e0e3bb24 | ||
|
|
b1f43f0d7c | ||
|
|
40efc347b6 | ||
|
|
71f19469ab | ||
|
|
9636675c2c | ||
|
|
1b57a24789 | ||
|
|
90fb201a51 | ||
|
|
657133db50 | ||
|
|
e68364cb3b | ||
|
|
1f92ae6ba8 | ||
|
|
a6bebd85ed | ||
|
|
220c80c3ed | ||
|
|
5960ecb3d1 | ||
|
|
c2778d9fa3 | ||
|
|
cfe14ff568 | ||
|
|
cf5d8874b6 | ||
|
|
d086867c0c | ||
|
|
d09133c010 | ||
|
|
6ac2b34a6e | ||
|
|
9276df62c2 | ||
|
|
d4c383b4b7 | ||
|
|
b10a4a2976 | ||
|
|
cfda9ffb8f | ||
|
|
ae5c2dab86 | ||
|
|
0452076ebc | ||
|
|
7831460eff | ||
|
|
20d5790358 | ||
|
|
a52702f76e | ||
|
|
351573c122 | ||
|
|
809b048abd | ||
|
|
c697d2a8f9 | ||
|
|
b8afefa55f | ||
|
|
84bf3c139c | ||
|
|
1bd343f9f0 | ||
|
|
fd5ac633d3 | ||
|
|
a3e836bbaf | ||
|
|
ee4daf02b5 | ||
|
|
a8c1832469 | ||
|
|
2c3baa9119 | ||
|
|
f4507e7252 | ||
|
|
32d3909023 | ||
|
|
8a935e3cb2 | ||
|
|
8b5d0e3fbd | ||
|
|
af38f07d26 | ||
|
|
b33d4497fa | ||
|
|
03b83811fa | ||
|
|
e5b1cc21a9 | ||
|
|
fdce84d9d9 | ||
|
|
8873d69b77 | ||
|
|
a1cb5b16a6 | ||
|
|
8621aa7cbf | ||
|
|
9a99a51e14 | ||
|
|
bd0aff6a0f | ||
|
|
25dd60a149 | ||
|
|
048261c921 | ||
|
|
9149366919 | ||
|
|
47d7c22073 | ||
|
|
7a106aeb5f | ||
|
|
3124ffede5 | ||
|
|
79db00a90e | ||
|
|
dd9adf6a65 | ||
|
|
0206434f62 | ||
|
|
657cbaefb9 | ||
|
|
ee61f07d9b | ||
|
|
cba1827d2c | ||
|
|
3d7c18ee70 | ||
|
|
82c1a58e81 | ||
|
|
8ac98f9f6b | ||
|
|
56e0636b14 | ||
|
|
6534cc6ae3 | ||
|
|
d25d0810af | ||
|
|
ba2f6671a6 | ||
|
|
9284366a3d | ||
|
|
7e6a478283 | ||
|
|
e15ebd9118 | ||
|
|
0ff311356d | ||
|
|
e49a29197b | ||
|
|
2b7fb699af | ||
|
|
f3ad8bd52f | ||
|
|
36ba838f54 | ||
|
|
dea5bd0639 | ||
|
|
0b3584a355 | ||
|
|
94220f22b2 | ||
|
|
ec85da914a | ||
|
|
bfebfecf6c | ||
|
|
e48ec6a35f | ||
|
|
a844797d67 | ||
|
|
72640f1828 | ||
|
|
4ce6a76d1f | ||
|
|
bf4c75b1cd | ||
|
|
313a95a467 | ||
|
|
926c85c6ba | ||
|
|
afb31d3beb | ||
|
|
c4c5ec1e92 | ||
|
|
cea85eb05b | ||
|
|
3e0d0a1993 | ||
|
|
882ed85d77 | ||
|
|
eae9928e98 | ||
|
|
779d1589c5 | ||
|
|
4c01af4e1d | ||
|
|
13944f3811 | ||
|
|
0f9f1feb87 | ||
|
|
b6889ec8c3 | ||
|
|
5aafbbeff0 | ||
|
|
7462fe91d5 | ||
|
|
79c5343ca5 | ||
|
|
d5ebd6a0c4 | ||
|
|
12de13c678 | ||
|
|
97d692df80 | ||
|
|
7941612dad | ||
|
|
c25706f6a5 | ||
|
|
799e202d6d | ||
|
|
658fbac7aa | ||
|
|
086daca3d0 | ||
|
|
f48f9ba475 | ||
|
|
29c988d42f | ||
|
|
06183bea1a | ||
|
|
a2146ca5b0 | ||
|
|
727a213ba1 | ||
|
|
38266f2918 | ||
|
|
6d4c1e49fe | ||
|
|
fa00b9faf3 | ||
|
|
685d7d2559 | ||
|
|
df918ab3c4 | ||
|
|
404ae010e4 | ||
|
|
43d42e6f15 | ||
|
|
328e63828e | ||
|
|
465b16e668 | ||
|
|
0e6569d78c | ||
|
|
8285046962 | ||
|
|
8e04b135bb | ||
|
|
2b7c785386 | ||
|
|
09f491821b | ||
|
|
c0cfacdb28 | ||
|
|
e96cde1872 | ||
|
|
12fc760e4a | ||
|
|
d2b17690f9 | ||
|
|
8d7fe2b9e7 | ||
|
|
1fa7c24a2a | ||
|
|
6b80dc035a | ||
|
|
35cfafc319 | ||
|
|
e1c07dce3f | ||
|
|
96bda1639e | ||
|
|
a424bfe715 | ||
|
|
4f6ace8b10 | ||
|
|
bccb5065db | ||
|
|
bed6a130d6 | ||
|
|
108eea8b79 | ||
|
|
678063efec | ||
|
|
99d9ad7042 | ||
|
|
b9850ac200 | ||
|
|
9fddad02ad | ||
|
|
a1007411ad | ||
|
|
9955de3e9b | ||
|
|
2eb39d962a | ||
|
|
d6a9fc57cc | ||
|
|
698d352322 | ||
|
|
1889a4a87f | ||
|
|
5cb2347f77 | ||
|
|
5ce4b25f38 | ||
|
|
267716fed5 | ||
|
|
a89b549c40 | ||
|
|
91ad3ef684 | ||
|
|
36f793c1f4 | ||
|
|
c27cd5b4ab | ||
|
|
7d881a0f0c | ||
|
|
487bd2eaf7 | ||
|
|
e9d194327a | ||
|
|
c301e6a380 | ||
|
|
a3d73be8b8 | ||
|
|
fa0b0042b2 | ||
|
|
e21f66db0a | ||
|
|
1e71d2dacb | ||
|
|
a80e5cb028 | ||
|
|
40c9adc8d3 | ||
|
|
147962d101 | ||
|
|
6536034ab6 | ||
|
|
7e7a2227a0 | ||
|
|
e5113889f4 | ||
|
|
3cdc3522be | ||
|
|
2b4eec39cf | ||
|
|
3a9af4413d | ||
|
|
f956a76455 | ||
|
|
ecbd514a83 | ||
|
|
50eaccc60c | ||
|
|
2a00b0092d | ||
|
|
d18ccac43b | ||
|
|
a617e7918c | ||
|
|
d20d2e9142 | ||
|
|
551394a831 | ||
|
|
0613a06653 | ||
|
|
ff545feae3 | ||
|
|
c7a9dfbb3f | ||
|
|
2553850ed4 | ||
|
|
e6d230042d | ||
|
|
ae8985deb5 | ||
|
|
18bc3c2b99 | ||
|
|
afd0275042 | ||
|
|
2d5e362115 | ||
|
|
7a2b5c43e7 | ||
|
|
0ab7c31bbe | ||
|
|
0cd4127087 | ||
|
|
5ebfc9027b | ||
|
|
fe6d05fccc | ||
|
|
35b3a2d6a6 | ||
|
|
6bb087cecd | ||
|
|
447a00ba7e | ||
|
|
0da9669e5c | ||
|
|
4a7d4d3555 | ||
|
|
1ac5d4fe98 | ||
|
|
bd5353024f | ||
|
|
569fd784fa | ||
|
|
fbf2208e80 | ||
|
|
bd364f787e | ||
|
|
ca133d6d9b | ||
|
|
94f18f7004 | ||
|
|
e324c15b04 | ||
|
|
5c70c7c71a | ||
|
|
020a9ea396 | ||
|
|
3f9c7a5c00 | ||
|
|
f025b6ea36 | ||
|
|
03fa95fcb5 | ||
|
|
fee2898c05 | ||
|
|
2d70197221 | ||
|
|
f9c325c793 | ||
|
|
a4b323c5df | ||
|
|
365edac5d5 | ||
|
|
89cf5939b8 | ||
|
|
93c1e755e6 | ||
|
|
b7f69c77f1 | ||
|
|
235acc16f6 | ||
|
|
011c86fdca | ||
|
|
5f4756aa80 | ||
|
|
106f1c4434 | ||
|
|
bb3dd55a10 | ||
|
|
3f274d02ce | ||
|
|
f65e02477e | ||
|
|
8a5725fa74 | ||
|
|
0df28f58df | ||
|
|
9d707b9670 | ||
|
|
ad92bbc8c8 | ||
|
|
e1ba892827 | ||
|
|
4012b3c011 | ||
|
|
897f625db9 | ||
|
|
33c03e00e4 | ||
|
|
03ede29e7e | ||
|
|
4ea451378e | ||
|
|
542f966dcd | ||
|
|
198b7db463 | ||
|
|
b22fbd3734 | ||
|
|
06716ba132 | ||
|
|
52509087a7 | ||
|
|
7e88302d01 | ||
|
|
6bf0feeffc | ||
|
|
e7f02c954c | ||
|
|
cdcd37e15a | ||
|
|
8b5df952c9 | ||
|
|
525b7c8eeb | ||
|
|
8f85a07ecc | ||
|
|
5f5c0079d3 | ||
|
|
c10f1dacad | ||
|
|
1c14660ed5 | ||
|
|
87e30d8382 | ||
|
|
edeb531940 | ||
|
|
14cc33c512 | ||
|
|
bddf79855d | ||
|
|
124d98246b | ||
|
|
127cffba6f | ||
|
|
3e8813e559 | ||
|
|
4fc0671d0d | ||
|
|
e1537000c2 | ||
|
|
b8595b9062 | ||
|
|
fa6707effa | ||
|
|
a1ffbb528c | ||
|
|
c0cf9e509c | ||
|
|
c31358259f | ||
|
|
8514172e57 | ||
|
|
3a0a8ba939 | ||
|
|
16410d6014 | ||
|
|
34e3f0ba3f | ||
|
|
62ee70f388 | ||
|
|
441e58e422 | ||
|
|
c917b261f7 | ||
|
|
d3daaf32f9 | ||
|
|
62bbaf57a2 | ||
|
|
7acb4be13f | ||
|
|
044992eea0 | ||
|
|
2a244e5699 | ||
|
|
bee5c300db | ||
|
|
e31233c473 | ||
|
|
720fdf714c | ||
|
|
6f9c0d6cde | ||
|
|
a7782359b1 | ||
|
|
e96a22bd6d | ||
|
|
a957ed253c | ||
|
|
dc8a82e913 | ||
|
|
c1f8b62e11 | ||
|
|
c91e699a27 | ||
|
|
4a8850afb3 | ||
|
|
d1c41a97b1 | ||
|
|
fe5102580d | ||
|
|
d909a86fb0 | ||
|
|
4e68bde7b5 | ||
|
|
52f31f0596 | ||
|
|
fa8b77b060 | ||
|
|
83fe4e59f6 | ||
|
|
8de1ddd255 | ||
|
|
395aea28c6 | ||
|
|
5e7d1cd0eb | ||
|
|
3344e45a9f | ||
|
|
d020999e82 | ||
|
|
d105d255ca | ||
|
|
791584d9f3 | ||
|
|
48b54ef722 | ||
|
|
e2447721eb | ||
|
|
89074715bc | ||
|
|
4968f0f3c4 | ||
|
|
aa7c7a9a9d | ||
|
|
6a1b658f59 | ||
|
|
e2805c01f4 | ||
|
|
04c10233f7 | ||
|
|
92444a75eb | ||
|
|
cb92e6b0d2 | ||
|
|
5b04efac7c | ||
|
|
485db6e48b | ||
|
|
ee28cf91e1 | ||
|
|
8d4a97fed5 | ||
|
|
46ef954057 | ||
|
|
f28fe80861 | ||
|
|
91375a1095 | ||
|
|
3586ddcd63 | ||
|
|
fca02eb489 | ||
|
|
56160e0798 | ||
|
|
0b72338400 | ||
|
|
cc1a5548af | ||
|
|
a8b8a0948f | ||
|
|
e7f73900cb | ||
|
|
927d40f47f | ||
|
|
294aafde14 | ||
|
|
dcedf241bb | ||
|
|
bf8e82ba16 | ||
|
|
79cc2d6d25 | ||
|
|
a4582fb529 | ||
|
|
30447ada52 | ||
|
|
66de361fd6 | ||
|
|
9da5511484 | ||
|
|
e02bd8bb30 | ||
|
|
e21592d74f | ||
|
|
5ead7eb020 | ||
|
|
2a5f3f5b2d | ||
|
|
d80ffa8ad2 | ||
|
|
74afbc5505 | ||
|
|
4774043295 | ||
|
|
1237bfe840 | ||
|
|
4f86a20824 | ||
|
|
c4fc2ecf09 | ||
|
|
49ad336a35 | ||
|
|
6feaa72abd | ||
|
|
71d6f9cdfb | ||
|
|
59f919bda3 | ||
|
|
84c0daa000 | ||
|
|
415b9ead52 | ||
|
|
7ccf650e48 | ||
|
|
a29831b7fa | ||
|
|
70dc8b027d | ||
|
|
4e1d388017 | ||
|
|
341d5ecbb6 | ||
|
|
2e208b4921 | ||
|
|
c983d9f39f | ||
|
|
adc5784fc9 | ||
|
|
a8c3551040 | ||
|
|
ac90086fb1 | ||
|
|
bd94c00e96 | ||
|
|
2698f3f376 | ||
|
|
3332f94a00 | ||
|
|
fd0aa3f6ae | ||
|
|
cd1cca3df2 | ||
|
|
b86ead2737 | ||
|
|
21ee0fdcf7 | ||
|
|
158fb36c8c | ||
|
|
5f385a297c | ||
|
|
8d0adb9359 | ||
|
|
79eb4c298c | ||
|
|
6f57bcb477 | ||
|
|
54dbc2f582 | ||
|
|
1a8f2117b1 | ||
|
|
1d6f1f7561 | ||
|
|
59fd53ee99 | ||
|
|
8c27d469a1 | ||
|
|
7f62ba6ebe | ||
|
|
9a1e8c07ac | ||
|
|
b7df116491 | ||
|
|
ece1fa1b1b | ||
|
|
1769cdad7c | ||
|
|
99b1e00a75 | ||
|
|
e0f4da4641 | ||
|
|
2be69634df | ||
|
|
858356de22 | ||
|
|
4f73ef73bb | ||
|
|
0cb2e80e09 | ||
|
|
08a17ec376 | ||
|
|
3ad08c0f2a | ||
|
|
6e2bb075a6 | ||
|
|
587a56cf31 | ||
|
|
0a945a04f3 | ||
|
|
10a9f58d13 | ||
|
|
332a1bf58d | ||
|
|
fb721558af | ||
|
|
0ab2263a41 | ||
|
|
3642dc5514 | ||
|
|
28781ef08c | ||
|
|
8a188e23d9 | ||
|
|
343e9b1f5a | ||
|
|
858a6dd253 | ||
|
|
adeb0d3934 | ||
|
|
0ee872def4 | ||
|
|
c93e789b6b | ||
|
|
db217cd56f | ||
|
|
2befd9d473 | ||
|
|
22aa1f8fe3 | ||
|
|
f919ef12d0 | ||
|
|
20c9bc04a7 | ||
|
|
e37f7de0ad | ||
|
|
72b30bc699 | ||
|
|
10c3f47bd1 | ||
|
|
e31f21a6d2 | ||
|
|
c87da5e873 | ||
|
|
b0633281a1 | ||
|
|
8dcdf8d314 | ||
|
|
915bf392e9 | ||
|
|
af5531cbf6 | ||
|
|
e3e5934c51 | ||
|
|
32c35da402 | ||
|
|
8dc5ae662a | ||
|
|
ec6a251b7c | ||
|
|
147c7f503c | ||
|
|
c8b95a2527 | ||
|
|
169eb19523 | ||
|
|
ed133dfe0d | ||
|
|
374b4c523c | ||
|
|
9c71f7f1e9 | ||
|
|
ba064a906e | ||
|
|
5c1297d560 | ||
|
|
323229dacf | ||
|
|
0b48ea01d7 | ||
|
|
54d34ea796 | ||
|
|
ccd7153b9d | ||
|
|
4f89ad2342 | ||
|
|
0ed1ef1600 | ||
|
|
7202f7e1d6 | ||
|
|
998130fa9e | ||
|
|
637215fcff | ||
|
|
d16395b8b9 | ||
|
|
e110a8ab9c | ||
|
|
cb03d1d763 | ||
|
|
2a1150b74e | ||
|
|
91cbd75f92 | ||
|
|
156f5be4d4 | ||
|
|
037b6df385 | ||
|
|
0a803c61ff | ||
|
|
b8d4068a26 | ||
|
|
6d2317d60e | ||
|
|
40d7b2a87d | ||
|
|
32627eccbf | ||
|
|
fc9475592d | ||
|
|
e950d37ed2 | ||
|
|
5e0d1e6036 | ||
|
|
7fc51a508a | ||
|
|
cd6ba31cfa | ||
|
|
ce2e80d861 | ||
|
|
a27516b0bf | ||
|
|
4dc2ee034c | ||
|
|
d68dd39839 | ||
|
|
b10c2e554c | ||
|
|
ac8d7a5c55 | ||
|
|
68ef0e845a | ||
|
|
c6604a6442 | ||
|
|
5920927a52 | ||
|
|
e30ab8abba | ||
|
|
cee6ea6762 | ||
|
|
84874b9676 | ||
|
|
f281eeb02a | ||
|
|
6f7e385c74 | ||
|
|
954c5cb394 | ||
|
|
b9d666d4a5 | ||
|
|
b26e2128e4 | ||
|
|
09feb0c53a | ||
|
|
cb73b73862 | ||
|
|
de27c465ca | ||
|
|
dd33c94188 | ||
|
|
901f0bf04e | ||
|
|
cfbd9dc310 | ||
|
|
530a39350b | ||
|
|
84aa629010 | ||
|
|
536d55a226 | ||
|
|
2d919d0a5f | ||
|
|
c4b07fd44e | ||
|
|
b87b942363 | ||
|
|
5fe0a7fbb2 | ||
|
|
a159ac44d9 | ||
|
|
836d012459 | ||
|
|
07d15092aa | ||
|
|
a23462e830 | ||
|
|
6fc6afa6a7 | ||
|
|
64090d0b95 | ||
|
|
dbce56ed30 | ||
|
|
ea808bd08b | ||
|
|
da2d66a6df | ||
|
|
c1d124df4c | ||
|
|
dafe2bbc83 | ||
|
|
b392a7d24d | ||
|
|
d18640fc8c | ||
|
|
781686ee4a | ||
|
|
69ac0950fe | ||
|
|
a77ffc4ca5 | ||
|
|
3d8c8012cf | ||
|
|
6c5075b9a1 | ||
|
|
de46758eba | ||
|
|
21a1cd0469 | ||
|
|
1e128c142e | ||
|
|
aafc1a56f9 | ||
|
|
5382037c4e | ||
|
|
ee6bbe7925 | ||
|
|
cfcc08a914 | ||
|
|
0cb6a6269d | ||
|
|
db0ef5cf5c | ||
|
|
174b47d1db | ||
|
|
87a2886539 | ||
|
|
04e4ef214c | ||
|
|
2783f02790 | ||
|
|
761eb787ff | ||
|
|
6fa2f2142f | ||
|
|
72b55c82ec | ||
|
|
44cdf3bca3 | ||
|
|
fe672eae63 | ||
|
|
267fddc474 | ||
|
|
2b6388359b | ||
|
|
a2dad42ef6 | ||
|
|
8a509168ee | ||
|
|
9c48c83701 | ||
|
|
6197f98c26 | ||
|
|
213208bf12 | ||
|
|
5148ed7936 | ||
|
|
add68afe0a | ||
|
|
ec59d5abee | ||
|
|
a7fccf053f | ||
|
|
b1af1f752d | ||
|
|
01bc994822 | ||
|
|
2b8a1fe177 | ||
|
|
1c4beb5ef0 | ||
|
|
b42b045c08 | ||
|
|
c904a650b6 | ||
|
|
0612f27f1f | ||
|
|
5387eb77ab | ||
|
|
0c01bb758e | ||
|
|
11931f3ea2 | ||
|
|
cc82213147 | ||
|
|
f36af211c0 | ||
|
|
b20b4a8675 | ||
|
|
dfa93436c2 | ||
|
|
d0320b43f9 | ||
|
|
73071f2c62 | ||
|
|
c9409c5342 | ||
|
|
46bdd8c67d | ||
|
|
22b8f482ea | ||
|
|
387ec3fdd5 | ||
|
|
449bf45c2b | ||
|
|
b771528942 | ||
|
|
f727e56bad | ||
|
|
c5d5dd5fdc | ||
|
|
6f68eb50aa | ||
|
|
c63d2e531a | ||
|
|
eb6f9faae7 | ||
|
|
23dd02a05a | ||
|
|
d24e75f76b | ||
|
|
b119872b1b | ||
|
|
eeaa6c9e2c | ||
|
|
0a7211988a | ||
|
|
9e79d7dcca | ||
|
|
369466d3d4 | ||
|
|
f642db0462 | ||
|
|
906be31483 | ||
|
|
0220f9aaca | ||
|
|
a48777e482 | ||
|
|
4ee32cc12d | ||
|
|
818e162fff | ||
|
|
6739e6c166 | ||
|
|
3a0b341770 | ||
|
|
7396f61222 | ||
|
|
f7f0369bff | ||
|
|
fe36da2041 | ||
|
|
610bc46970 | ||
|
|
e6b587ea8d | ||
|
|
70b595c4a2 | ||
|
|
f59facf5a6 | ||
|
|
0e55f75d86 | ||
|
|
ee7dac9e28 | ||
|
|
9f85cbc07d | ||
|
|
454bbdbc68 | ||
|
|
62242cb640 | ||
|
|
9363193fe8 | ||
|
|
29601610c0 | ||
|
|
fdd0bd3b35 | ||
|
|
4d3234aa36 | ||
|
|
8b66b8ae47 | ||
|
|
ee07d645b4 | ||
|
|
bffe9f811a | ||
|
|
b6a8c48097 | ||
|
|
b71a428369 | ||
|
|
6911dc8d38 | ||
|
|
1e7c82bb58 | ||
|
|
2f8c5d7bd9 | ||
|
|
126d4f4079 | ||
|
|
ca4b8e6f8e | ||
|
|
2a120e5565 | ||
|
|
6fa0a9d70c | ||
|
|
55d17ba3bf | ||
|
|
bedc20c7c1 | ||
|
|
96a69d2303 | ||
|
|
444b1482fa | ||
|
|
9389001a1d | ||
|
|
5a6a67f632 | ||
|
|
6c4271f15d | ||
|
|
6d7de3ddff | ||
|
|
d8e1d0798e | ||
|
|
e9963945d6 | ||
|
|
9017888b9c | ||
|
|
8e1ac2b300 | ||
|
|
25c70cfad6 | ||
|
|
cf34335ec7 | ||
|
|
4ea9cfca48 | ||
|
|
94525f0497 | ||
|
|
0227f7e727 | ||
|
|
bb4dd716c8 | ||
|
|
d53ec858a2 | ||
|
|
286c46653c | ||
|
|
7cf8234d9f | ||
|
|
fd0282ee05 | ||
|
|
5cc3036498 | ||
|
|
957c8a1954 | ||
|
|
94c7e7f646 | ||
|
|
5f16854e1f | ||
|
|
48e9c53dda | ||
|
|
0ec4417992 | ||
|
|
eecb540141 | ||
|
|
14c690d9f5 | ||
|
|
c08bfd2205 | ||
|
|
d9eea28f17 | ||
|
|
06abd62af0 | ||
|
|
f732252f22 | ||
|
|
a01d33452e | ||
|
|
6dd6d9d6f0 | ||
|
|
ccd468f80c | ||
|
|
4e7ddedf75 | ||
|
|
8261c7f782 | ||
|
|
c3c5b76007 | ||
|
|
1c1bd817e1 | ||
|
|
ab244f76f6 | ||
|
|
535a6ae199 | ||
|
|
822cab7750 | ||
|
|
2647fd4c92 | ||
|
|
bb8a745431 | ||
|
|
352e31c88b | ||
|
|
a94d2ef277 | ||
|
|
ad34a56e56 | ||
|
|
970af56765 | ||
|
|
defcfa5d26 | ||
|
|
11d241eccd | ||
|
|
dbcdace361 | ||
|
|
47727e23f1 | ||
|
|
2bd197e12c | ||
|
|
2934e89e14 | ||
|
|
dc96fc17a9 | ||
|
|
35211f65c9 | ||
|
|
977666c2c9 | ||
|
|
7737c7f34f | ||
|
|
dcac8d8316 | ||
|
|
f95b3971ab | ||
|
|
91bb28b47b | ||
|
|
4b942e8ed5 | ||
|
|
fbbc2bbf79 | ||
|
|
483c565fac | ||
|
|
210101932c | ||
|
|
0eee999d7c | ||
|
|
a027337be2 | ||
|
|
f6ac069bc5 | ||
|
|
3cd1a17c23 | ||
|
|
72112d7aef | ||
|
|
4f3dcf8dc9 | ||
|
|
c158b86dc6 | ||
|
|
79d3a2821f | ||
|
|
c7dd48d769 | ||
|
|
ace56da7b3 | ||
|
|
af21f5eaf6 | ||
|
|
0881fa0c69 | ||
|
|
db28286a4b | ||
|
|
ccd111d512 | ||
|
|
d106c56c00 | ||
|
|
f0578f5f66 | ||
|
|
d63a9785bd | ||
|
|
8f38a48a1f | ||
|
|
39f87a27e5 | ||
|
|
ca4d813b71 | ||
|
|
2199c5c273 | ||
|
|
366adfb8f9 | ||
|
|
17925a0c0b | ||
|
|
55077d5059 | ||
|
|
3e4f7358a7 | ||
|
|
9195894d2a | ||
|
|
953449c215 | ||
|
|
c5c93d3a77 | ||
|
|
039446f756 | ||
|
|
03c1f49850 | ||
|
|
43133b5851 | ||
|
|
df40c5b97e | ||
|
|
e246e6acc6 | ||
|
|
5bd8730353 | ||
|
|
c210f0e3c1 | ||
|
|
426d8edb31 | ||
|
|
86ba3be4f7 | ||
|
|
5bf6889250 | ||
|
|
0229185ffe | ||
|
|
505ced5cc8 | ||
|
|
3e6eef5ebd | ||
|
|
8f1f6741e3 | ||
|
|
52a89a318a | ||
|
|
b43a3e9ab2 | ||
|
|
4bef9d90fb | ||
|
|
e5a87d1ab1 | ||
|
|
ef2c55141b | ||
|
|
0cbbe9bac3 | ||
|
|
db198652a5 | ||
|
|
7b0f432339 | ||
|
|
5472d868aa | ||
|
|
5a3ff111ef | ||
|
|
35ca82dc0c | ||
|
|
86fdfef6f1 | ||
|
|
9c5323ee4f | ||
|
|
2daf805e95 | ||
|
|
5e5b3ba65e | ||
|
|
28a2c0d8a9 | ||
|
|
8d1b161d4f | ||
|
|
5094e84af3 | ||
|
|
a5750dda2a | ||
|
|
f9c43fecf4 | ||
|
|
93012a1cfd | ||
|
|
5ed8ad46ee | ||
|
|
5e5e932584 | ||
|
|
26b6b743f5 | ||
|
|
633d539b60 | ||
|
|
432662bc86 | ||
|
|
04ceb5620b | ||
|
|
63829a07c5 | ||
|
|
2028861a58 | ||
|
|
efd704c798 | ||
|
|
d6f4ebec50 | ||
|
|
14b99fac2a | ||
|
|
8caa58af26 | ||
|
|
c9bff8b5a7 | ||
|
|
b16d4dff5e | ||
|
|
a3164db1bf | ||
|
|
213337c210 | ||
|
|
f8ce0354ae | ||
|
|
3c813f71f6 | ||
|
|
9f75e4eba5 | ||
|
|
e5ae053b4f | ||
|
|
f723d5afd8 | ||
|
|
dbc4b68301 | ||
|
|
dfac7ea564 | ||
|
|
e656d74633 | ||
|
|
22e612a738 | ||
|
|
ba98135e03 | ||
|
|
e23c15dab2 | ||
|
|
1703250e87 | ||
|
|
d8f1a04467 | ||
|
|
12ce7d4c18 | ||
|
|
ab68a1c325 | ||
|
|
56038674b1 | ||
|
|
72e8b3dcc0 | ||
|
|
5aa912c6e9 | ||
|
|
e40f1fe9a7 | ||
|
|
be983931fc | ||
|
|
64f7e04e14 | ||
|
|
221c07067e | ||
|
|
17be43ff9e | ||
|
|
cdf7542c25 | ||
|
|
137776bcb2 | ||
|
|
e223899982 | ||
|
|
b365c3806b | ||
|
|
696bfe464c | ||
|
|
87f47a60ae | ||
|
|
9d0c9f1264 | ||
|
|
d710684f8e | ||
|
|
b48a93c483 | ||
|
|
047614b540 | ||
|
|
0fc0b9a671 | ||
|
|
26dfc42034 | ||
|
|
153731c1f3 | ||
|
|
df9e188867 | ||
|
|
cc62f92988 | ||
|
|
61a5640a95 | ||
|
|
c0d6eb4a22 | ||
|
|
5835838202 | ||
|
|
bafea4bc0e | ||
|
|
19fce2aa8c | ||
|
|
cf299f59de | ||
|
|
3bdfa9b050 | ||
|
|
42fa953053 | ||
|
|
18b7110c31 | ||
|
|
4aa5e0a97e | ||
|
|
ce12b36433 | ||
|
|
9e5fe255a5 | ||
|
|
d7c154ea41 | ||
|
|
7de2080b9e | ||
|
|
22a229c305 | ||
|
|
840a493e62 | ||
|
|
c2f55fc836 | ||
|
|
cfb8e75411 | ||
|
|
81ff18565f | ||
|
|
83c2bcd418 | ||
|
|
0b4b849583 | ||
|
|
6bc601bbfc | ||
|
|
e45ea65e45 | ||
|
|
b74348e59d | ||
|
|
ce03f27044 | ||
|
|
1de86f4312 | ||
|
|
02a7875eb8 | ||
|
|
ea4e84cdd3 | ||
|
|
29c02d2724 | ||
|
|
fcd44c37e1 | ||
|
|
ce4c79ed93 | ||
|
|
cb42ec358e | ||
|
|
1a35b5aa2b | ||
|
|
4cc0cf795d | ||
|
|
ff49bb5125 | ||
|
|
d82f60f18b | ||
|
|
d15575212a | ||
|
|
e414f6d059 | ||
|
|
f217d7badf | ||
|
|
d0d8144ef9 | ||
|
|
443b11646d | ||
|
|
252dacb278 | ||
|
|
2b50a183bb | ||
|
|
ed894d52c9 | ||
|
|
4a8fe43036 | ||
|
|
af4b7daa77 | ||
|
|
edf92ef958 | ||
|
|
dd21cfad7f | ||
|
|
d3128808b9 | ||
|
|
ae80d08935 | ||
|
|
41ae6e0b7a | ||
|
|
56859a9c30 | ||
|
|
58c8945b32 | ||
|
|
ec29c2c776 | ||
|
|
22c04bfc24 | ||
|
|
eebb674232 | ||
|
|
5749d19fee | ||
|
|
6a06d0f95f | ||
|
|
d850ea41a2 | ||
|
|
5e01021cba | ||
|
|
f3619367a2 | ||
|
|
849bd52096 | ||
|
|
a7c9fd272c | ||
|
|
3ad186d727 | ||
|
|
7135508d66 | ||
|
|
7221744d9a | ||
|
|
5178e1a66c | ||
|
|
8326768b9f | ||
|
|
89e0a6098c | ||
|
|
8b9a2142c9 |
No files matched your search
+35
-11
@@ -29,17 +29,41 @@ coverage.html
|
||||
# Emacs backup files
|
||||
*~
|
||||
|
||||
# Vim backup files
|
||||
*.swp
|
||||
|
||||
# Project specific
|
||||
weaver/weaver
|
||||
weavedns/weavedns
|
||||
sigproxy/sigproxy
|
||||
tools/bin
|
||||
tools/build
|
||||
prog/weaver/weaver
|
||||
prog/weavedns/weavedns
|
||||
prog/weavehosts/weavehosts
|
||||
prog/weaveproxy/weaveproxy
|
||||
prog/sigproxy/sigproxy
|
||||
prog/weavewait/weavewait
|
||||
prog/weavewait/weavewait_noop
|
||||
prog/weavewait/weavewait_nomcast
|
||||
prog/netcheck/netcheck
|
||||
prog/docker_tls_args/docker_tls_args
|
||||
prog/plugin/plugin
|
||||
testing/cover/cover
|
||||
testing/runner/runner
|
||||
releases
|
||||
weave.tar
|
||||
weavedns.tar
|
||||
weaveexec.tar
|
||||
weaveexec/weave
|
||||
weaveexec/sigproxy
|
||||
weaveexec/docker*.tgz
|
||||
.weaver.uptodate
|
||||
.weavedns.uptodate
|
||||
.weaveexec.uptodate
|
||||
.dockerplugin.uptodate
|
||||
weave.tar.gz
|
||||
prog/weaveexec/weave
|
||||
prog/weaveexec/weavehosts
|
||||
prog/weaveexec/weaveproxy
|
||||
prog/weaveexec/weavewait
|
||||
prog/weaveexec/weavewait_noop
|
||||
prog/weaveexec/weavewait_nomcast
|
||||
prog/weaveexec/sigproxy
|
||||
prog/weaveexec/netcheck
|
||||
prog/weaveexec/docker_tls_args
|
||||
prog/weaveexec/docker*.tgz
|
||||
Vagrantfile.local
|
||||
test/tls/tls
|
||||
test/tls/*.pem
|
||||
test/coverage
|
||||
test/coverage.*
|
||||
@@ -0,0 +1,3 @@
|
||||
[submodule "tools"]
|
||||
path = tools
|
||||
url = https://github.com/weaveworks/build-tools
|
||||
+8
-2
@@ -3,6 +3,9 @@ sudo: false
|
||||
go:
|
||||
- 1.4
|
||||
|
||||
env:
|
||||
- PATH="${PATH}:${HOME}/.local/bin"
|
||||
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
@@ -11,12 +14,15 @@ addons:
|
||||
install:
|
||||
- go clean -i net
|
||||
- go install -tags netgo std
|
||||
- go get github.com/golang/lint/golint
|
||||
- pip install --user --upgrade gsutil
|
||||
- bin/setup-ci-secrets $encrypted_5ba036e89377_key $encrypted_5ba036e89377_iv
|
||||
- make travis
|
||||
- go get ./...
|
||||
|
||||
script:
|
||||
- bin/lint .
|
||||
- make tests
|
||||
|
||||
after_success:
|
||||
- go get github.com/mattn/goveralls
|
||||
- $HOME/gopath/bin/goveralls -coverprofile=profile.cov -service=travis-ci
|
||||
- test "${TRAVIS_PULL_REQUEST}" = "false" && site/build-and-publish.sh
|
||||
+324
@@ -1,3 +1,327 @@
|
||||
## Release 1.4.4
|
||||
|
||||
Fixing a rather serious issue that slipped through our preparations
|
||||
for Docker 1.10:
|
||||
|
||||
* Restarting Docker or rebooting your machine while the Weave Net
|
||||
plugin is running causes Docker 1.10.0 to fail to start #1959/#1963
|
||||
|
||||
Also one other small fix:
|
||||
|
||||
* Avoid a hang when trying to use plugin and proxy at the same time
|
||||
#1956/#1961
|
||||
|
||||
## Release 1.4.3
|
||||
|
||||
Preparing for Docker 1.10, plus some bug-fixes.
|
||||
|
||||
* Avoid hang in Docker v1.10 on `docker volume ls` after `weave stop`,
|
||||
`weave stop-plugin` or `weave reset` #1934/#1936
|
||||
* Fix "unexpected EOF" from Docker 1.10 on `docker exec` with Weave
|
||||
proxy `--rewrite-inspect` enabled #1911/#1917
|
||||
* Avoid losing DNS entries and potentially double-allocating IP
|
||||
Addresses allocated via plugin, on router restart; also extend
|
||||
`weave ps` to show IP addresses allocated via plugin #1745/#1921
|
||||
* Stop creating lots of copies of `weavewait` program in Docker
|
||||
volumes #1757/#1935
|
||||
* Prevent container starting prematurely when proxy in
|
||||
`--no-multicast-route` mode #1942/#1943
|
||||
* Log error message from plugin rather than crashing when weave not
|
||||
running #1906/#1918
|
||||
* Warn, don't error, if unable to remove plugin network in 'weave
|
||||
stop', to avoid breaking the usual upgrade or config change process
|
||||
#1900/#1919
|
||||
* Don't crash if network conditions suggest only very small packets
|
||||
will get through #1905/#1926
|
||||
* Cope with unexpected errors during route traversal when starting
|
||||
container via proxy #1909/#1910/#1932
|
||||
|
||||
## Release 1.4.2
|
||||
|
||||
Bug-fixes and minor improvements.
|
||||
|
||||
* A race condition in weavewait that would occasionally hang
|
||||
containers at startup #1882/#1884
|
||||
* Having the plugin auto-restart prevents successful `weave launch` on
|
||||
reboot #1869
|
||||
* Work round weave router failure on CoreOS 4.3 caused by kernel bug
|
||||
#1854
|
||||
* `weave launch` would exit with error code on docker <1.9 #1851
|
||||
* Running `eval $(weave env)` multiple times would break `eval $(weave
|
||||
env --restore)` #1824/#1825
|
||||
* Don't complain in `weave stop` about "Plugin is not running" when
|
||||
plugin is not enabled #1840/#1841
|
||||
* `weave --local launch` would fail if utility program
|
||||
`docker_tls_args` could not be found #1844
|
||||
* Improved error reporting when TLS arg detection fails #1843
|
||||
* Improve error reporting when docker isn't running #1845
|
||||
* Add `--trusted-subnets` usage to `weave` script #1842
|
||||
* weave run can hang under rare combinations of options #1858
|
||||
|
||||
## Release 1.4.1
|
||||
|
||||
This is a bug-fix release to cover a few issues that came up since the
|
||||
release of 1.4.0.
|
||||
|
||||
* Weave would fail to launch when `$DOCKER_HOST` was set to a TCP socket secured with TLS #1820/#1822
|
||||
* Weave would fail to stop when run against Docker pre-1.9 #1815/#1817
|
||||
* Issue a warning instead of failing on `--with-dns` option to proxy, which was removed #1810/#1812
|
||||
* Make `weave version` show the plugin version #1797/#1813
|
||||
* Make `weave launch` show when the a container is restarting #1778/#1814
|
||||
* Make `weave launch` fail if the plugin is running, for consistency with router and proxy. #1818/#1819
|
||||
|
||||
More details in the [change log](https://github.com/weaveworks/weave/issues?q=milestone%3A1.4.1).
|
||||
|
||||
## Release 1.4.0
|
||||
|
||||
**Highlights**
|
||||
|
||||
* The Docker Network plugin can now operate without a cluster store, so it is now run by default.
|
||||
* You can now use the fast datapath over trusted links and Weave encryption over untrusted links, in the same network.
|
||||
|
||||
Plus many bug fixes and minor enhancements. More details below and in
|
||||
the [change log](https://github.com/weaveworks/weave/issues?q=milestone%3A1.4.0).
|
||||
|
||||
## Release 1.3.1
|
||||
|
||||
**Highlights**
|
||||
|
||||
* The minimum Docker version has been increased to 1.6 due to the
|
||||
[upcoming deprecation of Dockerhub access](https://blog.docker.com/2015/10/docker-hub-deprecation-1-5/)
|
||||
for old clients. From December 7th onwards previous versions of the
|
||||
`weave` script will fail to pull down images from the hub; if you
|
||||
are unable to upgrade to 1.3.1 immediately you can work around this
|
||||
by running `weave --local setup` in conjunction with a compatible
|
||||
Docker client installation
|
||||
* Docker networking plugin now works with older kernels and allows you
|
||||
to configure the MTU
|
||||
|
||||
More details in the
|
||||
[change log](https://github.com/weaveworks/weave/issues?q=milestone%3A1.3.1).
|
||||
|
||||
## Release 1.3.0
|
||||
|
||||
**Highlights**
|
||||
|
||||
This release includes a Docker Plugin, so you have the option to use
|
||||
Weave Net that way. More details in the
|
||||
[change log](https://github.com/weaveworks/weave/issues?q=milestone%3A1.3.0).
|
||||
|
||||
## Release 1.2.1
|
||||
|
||||
This release contains a number of bug fixes and minor
|
||||
enhancements. More details in the
|
||||
[change log](https://github.com/weaveworks/weave/issues?q=milestone%3A1.2.1).
|
||||
|
||||
The release is fully compatible with 1.2.0 versions, so existing
|
||||
clusters can be upgraded incrementally.
|
||||
|
||||
## Release 1.2.0
|
||||
|
||||
**Highlights**
|
||||
|
||||
This release introduces the
|
||||
[Fast Data Path](http://docs.weave.works/weave/master/head/features.html#fast-data-path),
|
||||
which allows Weave networks to operate at near wire-level speeds. This
|
||||
new feature is enabled by default.
|
||||
|
||||
Other highlights:
|
||||
|
||||
- auto-configuration of TLS for the Weave Docker API proxy, making it
|
||||
easier to run Weave on Macs and in conjunction with tools like
|
||||
Docker Swarm
|
||||
- support for restart policies on application containers and weave
|
||||
infrastructure containers
|
||||
- better compatibility with recent and future Docker versions
|
||||
|
||||
More details in the
|
||||
[change log](https://github.com/weaveworks/weave/issues?q=milestone%3A1.2.0).
|
||||
|
||||
## Release 1.1.2
|
||||
|
||||
This release contains a small number of bug fixes. More details in the
|
||||
[change log](https://github.com/weaveworks/weave/issues?q=milestone%3A1.1.2).
|
||||
|
||||
The release is fully compatible with 1.1.0 versions, so existing
|
||||
clusters can be upgraded incrementally. When upgrading from 1.0.x,
|
||||
note the compatibility information in the
|
||||
[Installation & Upgrading instructions for Weave 1.1.0](https://github.com/weaveworks/weave/releases/tag/v1.1.0).
|
||||
|
||||
## Release 1.1.1
|
||||
|
||||
This release contains a number of bug fixes and minor
|
||||
enhancements. More details in the
|
||||
[change log](https://github.com/weaveworks/weave/issues?q=milestone%3A1.1.1).
|
||||
|
||||
The release is fully compatible with 1.1.0 versions, so existing
|
||||
clusters can be upgraded incrementally. When upgrading from 1.0.x,
|
||||
note the compatibility information in the
|
||||
[Installation & Upgrading instructions for Weave 1.1.0](https://github.com/weaveworks/weave/releases/tag/v1.1.0).
|
||||
|
||||
## Release v1.1.0
|
||||
|
||||
**Highlights**:
|
||||
|
||||
- `weave launch` now launches all weave components, simplifying
|
||||
startup.
|
||||
- `weave status` has been completely revamped, with a much improved
|
||||
presentation of the information, and the option to select and output
|
||||
data in JSON.
|
||||
- weaveDNS has been rewritten and embedded in the router. The new
|
||||
implementation simplifies configuration, improves performance, and
|
||||
provides fault resilience for services.
|
||||
- the weave Docker API proxy now provides an even more seamless user
|
||||
experience, and enables easier integration of weave with other
|
||||
systems such as kubernetes.
|
||||
- many usability improvements
|
||||
- a few minor bug fixes, including a couple of security
|
||||
vulnerabilities
|
||||
|
||||
More details in the
|
||||
[change log](https://github.com/weaveworks/weave/issues?q=milestone%3A1.1.0).
|
||||
|
||||
## Release 1.0.3
|
||||
|
||||
This release contains a weaveDNS feature enhancement as well as minor fixes for
|
||||
improved stability and robustness.
|
||||
|
||||
More details in the
|
||||
[change log](https://github.com/weaveworks/weave/issues?q=milestone%3A1.0.3).
|
||||
|
||||
The release is fully compatible with other 1.0.x versions, so existing
|
||||
clusters can be upgraded incrementally.
|
||||
|
||||
## Release 1.0.2
|
||||
|
||||
This release fixes a number of bugs, including some security
|
||||
vulnerabilities in the Weave Docker API proxy, hangs and failures in
|
||||
address allocation, and sporadic failures in name resolution.
|
||||
|
||||
More details in the
|
||||
[change log](https://github.com/weaveworks/weave/issues?q=milestone%3A1.0.2).
|
||||
|
||||
The release is fully compatible with other 1.0.x versions, so existing
|
||||
clusters can be upgraded incrementally.
|
||||
|
||||
## Release 1.0.1
|
||||
|
||||
This is a bug fix release, addressing the following issue:
|
||||
|
||||
- executing `docker run` from a Docker 1.7 client against the weave
|
||||
proxy would report a `no such image` error when the requested image
|
||||
wasn't present on the Docker host, instead of downloading it.
|
||||
|
||||
## Release 1.0.0
|
||||
|
||||
**Highlights**:
|
||||
|
||||
- It is now easier than ever to start containers and for them to
|
||||
communicate, across multiple hosts. Automatic IP address allocation,
|
||||
and name resolution via weaveDNS are now enabled by default, and the
|
||||
proxy has become more fully-featured. In short, once weave has been
|
||||
launched the following is possible:
|
||||
|
||||
````
|
||||
host1$ docker run --name=pingme -dti ubuntu
|
||||
host2$ docker run -ti ubuntu
|
||||
root@d11e9287f65b:/# ping pingme
|
||||
````
|
||||
- Containers can now be
|
||||
[load-balanced](http://docs.weave.works/weave/latest_release/weavedns.html#load-balancing)
|
||||
easily.
|
||||
- IP address allocation is now available across [multiple
|
||||
subnets](http://docs.weave.works/weave/latest_release/ipam.html#range),
|
||||
and hence can be employed when running multiple, isolated
|
||||
applications.
|
||||
- The proxy now supports [TLS
|
||||
connections](http://docs.weave.works/weave/latest_release/proxy.html#tls),
|
||||
enabling its deployment when the communication between docker
|
||||
clients and the server must be secured.
|
||||
|
||||
There are many other new features, plus the usual assortment of bug
|
||||
fixes and improvements under the hood. More detail below and in the
|
||||
[change log](https://github.com/weaveworks/weave/issues?q=milestone%3A1.0.0).
|
||||
|
||||
*NB: This release changes the weave protocol version. Therefore, when
|
||||
upgrading an existing installation, all hosts need to be upgraded in
|
||||
order to for them to be able to communicate and form a network.*
|
||||
|
||||
## Release 0.11.2
|
||||
|
||||
This is a bug fix release, addressing the following issues:
|
||||
|
||||
- `weave run` did not respect DOCKER_CLIENT_ARGS. #855
|
||||
- Negative result cache did not expire if requeried within TTL. #845
|
||||
|
||||
More details in the [change
|
||||
log](https://github.com/weaveworks/weave/issues?q=milestone%3A0.11.2).
|
||||
|
||||
*NB: This release does not change the weave protocol version.
|
||||
Therefore, when upgrading an existing 0.11 installation incrementally,
|
||||
connectivity between peers will be retained.*
|
||||
|
||||
## Release 0.11.1
|
||||
|
||||
This is a bug fix release, addressing the following issues:
|
||||
|
||||
- The IP Allocator could crash in some relatively rare
|
||||
circumstances. #782/#783.
|
||||
- When the proxy failed to attach a container to the weave network,
|
||||
there was no failure indication and descriptive error anywhere, and
|
||||
the application process would still start. Now an error is reported
|
||||
to the client (i.e. typically the Docker CLI), recorded in the proxy
|
||||
logs, and the container is terminated. #788/#799.
|
||||
- `weave launch-proxy --with-ipam` failed to configure the entrypoint
|
||||
and DNS unless a (possibly blank) `WEAVE_CIDR` was
|
||||
specified. Affected containers could start the application process
|
||||
w/o the weave network interface being available, and without
|
||||
functioning name resolution for the weaveDNS domain.
|
||||
#744/#747/#751/#752
|
||||
- The `weave status` output for the IP Allocator was misleadingly
|
||||
conveying a sense of brokenness when no IP allocation requests had
|
||||
been made yet. #787/#801
|
||||
- When invoking `weave launch-proxy` twice, the second invocation
|
||||
would output a blank line and terminate with a success exit
|
||||
status. Now it reports that the proxy is already running and exits
|
||||
with a non-zero status. #767/#780
|
||||
- `weave launch-proxy` was not respecting WEAVEPROXY_DOCKER_ARGS, so
|
||||
any user-supplied custom configuration for the weaveproxy container
|
||||
was ignored. #755/#780
|
||||
- The proxy was not intercepting API calls to the unversioned (1.0)
|
||||
Docker Remote API. Hence none of the weave functionality was
|
||||
available when Docker clients were using that version of the
|
||||
API. #770/#774/#777,#809
|
||||
- The proxy would crash when certain elements of the
|
||||
`/containers/create` JSON were missing. We are not aware of any
|
||||
Docker clients doing this, but it's been fixed regardless. #778/#777
|
||||
|
||||
More details in the [change
|
||||
log](https://github.com/weaveworks/weave/issues?q=milestone%3A0.11.1).
|
||||
|
||||
*NB: This release does not change the weave protocol version.
|
||||
Therefore, when upgrading an existing 0.11 installation incrementally,
|
||||
connectivity between peers will be retained.*
|
||||
|
||||
## Release 0.11.0
|
||||
|
||||
**Highlights**:
|
||||
|
||||
- **automatic IP Address Management (IPAM)**, which allows application
|
||||
containers to be started and attached to the weave network without
|
||||
needing to supply an IP address.
|
||||
- **proxy** for automatically attaching containers started with
|
||||
ordinary `docker run`, or the Docker remote API, to the weave
|
||||
network.
|
||||
- ability to **add/remove extra DNS records**.
|
||||
- performance and scalability improvements
|
||||
- fixes for a small number of bugs discovered during testing
|
||||
|
||||
More detail in the [change log](https://github.com/weaveworks/weave/issues?q=milestone%3A0.11.0)
|
||||
|
||||
*NB: This release changes the weave protocol version. Therefore, when
|
||||
upgrading an existing installation, all hosts need to be upgraded in
|
||||
order to for them to be able to communicate and form a network.*
|
||||
|
||||
## Release 0.10.0
|
||||
|
||||
**Highlights**:
|
||||
|
||||
@@ -1,100 +1,167 @@
|
||||
PUBLISH=publish_weave publish_weavedns publish_weaveexec
|
||||
PUBLISH=publish_weave publish_weaveexec publish_plugin
|
||||
|
||||
.DEFAULT: all
|
||||
.PHONY: all update tests publish $(PUBLISH) clean prerequisites build travis
|
||||
.PHONY: all update tests lint publish $(PUBLISH) clean clean-bin prerequisites build travis run-smoketests
|
||||
|
||||
# If you can use docker without being root, you can do "make SUDO="
|
||||
SUDO=sudo
|
||||
|
||||
DOCKERHUB_USER=weaveworks
|
||||
WEAVE_VERSION=git-$(shell git rev-parse --short=12 HEAD)
|
||||
WEAVER_EXE=weaver/weaver
|
||||
WEAVEDNS_EXE=weavedns/weavedns
|
||||
SIGPROXY_EXE=sigproxy/sigproxy
|
||||
|
||||
WEAVER_EXE=prog/weaver/weaver
|
||||
WEAVEPROXY_EXE=prog/weaveproxy/weaveproxy
|
||||
SIGPROXY_EXE=prog/sigproxy/sigproxy
|
||||
WEAVEWAIT_EXE=prog/weavewait/weavewait
|
||||
WEAVEWAIT_NOOP_EXE=prog/weavewait/weavewait_noop
|
||||
WEAVEWAIT_NOMCAST_EXE=prog/weavewait/weavewait_nomcast
|
||||
NETCHECK_EXE=prog/netcheck/netcheck
|
||||
DOCKERTLSARGS_EXE=prog/docker_tls_args/docker_tls_args
|
||||
DOCKERPLUGIN_EXE=prog/plugin/plugin
|
||||
RUNNER_EXE=tools/runner/runner
|
||||
TEST_TLS_EXE=test/tls/tls
|
||||
|
||||
EXES=$(WEAVER_EXE) $(SIGPROXY_EXE) $(WEAVEPROXY_EXE) $(WEAVEWAIT_EXE) $(WEAVEWAIT_NOOP_EXE) $(WEAVEWAIT_NOMCAST_EXE) $(NETCHECK_EXE) $(DOCKERTLSARGS_EXE) $(DOCKERPLUGIN_EXE) $(TEST_TLS_EXE)
|
||||
|
||||
WEAVER_UPTODATE=.weaver.uptodate
|
||||
WEAVEEXEC_UPTODATE=.weaveexec.uptodate
|
||||
DOCKERPLUGIN_UPTODATE=.dockerplugin.uptodate
|
||||
|
||||
IMAGES_UPTODATE=$(WEAVER_UPTODATE) $(WEAVEEXEC_UPTODATE) $(DOCKERPLUGIN_UPTODATE)
|
||||
|
||||
WEAVER_IMAGE=$(DOCKERHUB_USER)/weave
|
||||
WEAVEDNS_IMAGE=$(DOCKERHUB_USER)/weavedns
|
||||
WEAVEEXEC_IMAGE=$(DOCKERHUB_USER)/weaveexec
|
||||
WEAVER_EXPORT=weave.tar
|
||||
WEAVEDNS_EXPORT=weavedns.tar
|
||||
WEAVEEXEC_EXPORT=weaveexec.tar
|
||||
DOCKERPLUGIN_IMAGE=$(DOCKERHUB_USER)/plugin
|
||||
|
||||
WEAVEEXEC_DOCKER_VERSION=1.3.1
|
||||
DOCKER_DISTRIB=weaveexec/docker-$(WEAVEEXEC_DOCKER_VERSION).tgz
|
||||
IMAGES=$(WEAVER_IMAGE) $(WEAVEEXEC_IMAGE) $(DOCKERPLUGIN_IMAGE)
|
||||
|
||||
WEAVE_EXPORT=weave.tar.gz
|
||||
|
||||
WEAVEEXEC_DOCKER_VERSION=1.6.2
|
||||
DOCKER_DISTRIB=prog/weaveexec/docker-$(WEAVEEXEC_DOCKER_VERSION).tgz
|
||||
DOCKER_DISTRIB_URL=https://get.docker.com/builds/Linux/x86_64/docker-$(WEAVEEXEC_DOCKER_VERSION).tgz
|
||||
NETGO_CHECK=@strings $@ | grep cgo_stub\\\.go >/dev/null || { \
|
||||
rm $@; \
|
||||
echo "\nYour go standard library was built without the 'netgo' build tag."; \
|
||||
echo "To fix that, run"; \
|
||||
echo " sudo go clean -i net"; \
|
||||
echo " sudo go install -tags netgo std"; \
|
||||
false; \
|
||||
}
|
||||
BUILD_FLAGS=-ldflags "-extldflags \"-static\" -X main.version $(WEAVE_VERSION)" -tags netgo
|
||||
|
||||
all: $(WEAVER_EXPORT) $(WEAVEDNS_EXPORT) $(WEAVEEXEC_EXPORT)
|
||||
PACKAGE_BASE=$(shell go list -e ./)
|
||||
|
||||
travis: $(WEAVER_EXE) $(WEAVEDNS_EXE)
|
||||
all: $(WEAVE_EXPORT) $(RUNNER_EXE) $(TEST_TLS_EXE)
|
||||
|
||||
travis: $(EXES)
|
||||
|
||||
update:
|
||||
go get -u -f -v -tags -netgo ./$(dir $(WEAVER_EXE)) ./$(dir $(WEAVEDNS_EXE))
|
||||
go get -u -f -v -tags netgo $(addprefix ./,$(dir $(EXES)))
|
||||
|
||||
$(WEAVER_EXE) $(WEAVEDNS_EXE): common/*.go
|
||||
$(WEAVER_EXE) $(WEAVEPROXY_EXE): common/*.go common/*/*.go net/*.go
|
||||
ifeq ($(COVERAGE),true)
|
||||
$(eval COVERAGE_MODULES := $(shell (go list ./$(@D); go list -f '{{join .Deps "\n"}}' ./$(@D) | grep "^$(PACKAGE_BASE)/") | paste -s -d,))
|
||||
go get -t -tags netgo ./$(@D)
|
||||
go test -c -o ./$@ $(BUILD_FLAGS) -v -covermode=atomic -coverpkg $(COVERAGE_MODULES) ./$(@D)/
|
||||
else
|
||||
go get -tags netgo ./$(@D)
|
||||
go build -ldflags "-extldflags \"-static\" -X main.version $(WEAVE_VERSION)" -tags netgo -o $@ ./$(@D)
|
||||
@strings $@ | grep cgo_stub\\\.go >/dev/null || { \
|
||||
rm $@; \
|
||||
echo "\nYour go standard library was built without the 'netgo' build tag."; \
|
||||
echo "To fix that, run"; \
|
||||
echo " sudo go clean -i net"; \
|
||||
echo " sudo go install -tags netgo std"; \
|
||||
false; \
|
||||
}
|
||||
go build $(BUILD_FLAGS) -o $@ ./$(@D)
|
||||
endif
|
||||
$(NETGO_CHECK)
|
||||
|
||||
$(WEAVER_EXE): router/*.go weaver/main.go
|
||||
$(WEAVEDNS_EXE): nameserver/*.go weavedns/main.go
|
||||
$(NETCHECK_EXE): common/*.go common/*/*.go net/*.go
|
||||
go get -tags netgo ./$(@D)
|
||||
go build $(BUILD_FLAGS) -o $@ ./$(@D)
|
||||
$(NETGO_CHECK)
|
||||
|
||||
# Sigproxy needs separate rule as it fails the netgo check in the main
|
||||
# build stanza due to not importing net package
|
||||
$(SIGPROXY_EXE): sigproxy/main.go
|
||||
go build -o $@ ./$(@D)
|
||||
$(WEAVER_EXE): router/*.go mesh/*.go ipam/*.go ipam/*/*.go nameserver/*.go prog/weaver/*.go
|
||||
$(WEAVEPROXY_EXE): proxy/*.go prog/weaveproxy/main.go
|
||||
$(NETCHECK_EXE): prog/netcheck/netcheck.go
|
||||
|
||||
$(WEAVER_EXPORT): weaver/Dockerfile $(WEAVER_EXE)
|
||||
$(SUDO) docker build -t $(WEAVER_IMAGE) weaver
|
||||
$(SUDO) docker save $(WEAVER_IMAGE):latest > $@
|
||||
# These next programs need separate rules as they fail the netgo check in
|
||||
# the main build stanza due to not importing net package
|
||||
$(SIGPROXY_EXE): prog/sigproxy/main.go
|
||||
$(DOCKERTLSARGS_EXE): prog/docker_tls_args/*.go
|
||||
$(DOCKERPLUGIN_EXE): prog/plugin/*.go plugin/net/*.go plugin/ipam/*.go plugin/skel/*.go api/*.go common/docker/*.go
|
||||
$(TEST_TLS_EXE): test/tls/*.go
|
||||
|
||||
$(WEAVEDNS_EXPORT): weavedns/Dockerfile $(WEAVEDNS_EXE)
|
||||
$(SUDO) docker build -t $(WEAVEDNS_IMAGE) weavedns
|
||||
$(SUDO) docker save $(WEAVEDNS_IMAGE):latest > $@
|
||||
$(SIGPROXY_EXE) $(DOCKERTLSARGS_EXE) $(DOCKERPLUGIN_EXE) $(TEST_TLS_EXE):
|
||||
go get -tags netgo ./$(@D)
|
||||
go build $(BUILD_FLAGS) -o $@ ./$(@D)
|
||||
|
||||
$(WEAVEEXEC_EXPORT): weaveexec/Dockerfile $(DOCKER_DISTRIB) weave $(SIGPROXY_EXE)
|
||||
cp weave weaveexec/weave
|
||||
cp sigproxy/sigproxy weaveexec/sigproxy
|
||||
cp $(DOCKER_DISTRIB) weaveexec/docker.tgz
|
||||
$(SUDO) docker build -t $(WEAVEEXEC_IMAGE) weaveexec
|
||||
$(SUDO) docker save $(WEAVEEXEC_IMAGE):latest > $@
|
||||
$(WEAVEWAIT_EXE): prog/weavewait/*.go net/*.go
|
||||
go get -tags netgo ./$(@D)
|
||||
go build $(BUILD_FLAGS) -tags "netgo iface mcast" -o $@ ./$(@D)
|
||||
|
||||
$(WEAVEWAIT_NOMCAST_EXE): prog/weavewait/*.go net/*.go
|
||||
go get -tags netgo ./$(@D)
|
||||
go build $(BUILD_FLAGS) -tags "netgo iface" -o $@ ./$(@D)
|
||||
|
||||
$(WEAVEWAIT_NOOP_EXE): prog/weavewait/*.go
|
||||
go get -tags netgo ./$(@D)
|
||||
go build $(BUILD_FLAGS) -o $@ ./$(@D)
|
||||
|
||||
$(WEAVER_UPTODATE): prog/weaver/Dockerfile $(WEAVER_EXE)
|
||||
$(SUDO) DOCKER_HOST=$(DOCKER_HOST) docker build -t $(WEAVER_IMAGE) prog/weaver
|
||||
touch $@
|
||||
|
||||
$(WEAVEEXEC_UPTODATE): prog/weaveexec/Dockerfile prog/weaveexec/symlink $(DOCKER_DISTRIB) weave $(SIGPROXY_EXE) $(WEAVEPROXY_EXE) $(WEAVEWAIT_EXE) $(WEAVEWAIT_NOOP_EXE) $(WEAVEWAIT_NOMCAST_EXE) $(NETCHECK_EXE) $(DOCKERTLSARGS_EXE)
|
||||
cp weave prog/weaveexec/weave
|
||||
cp $(SIGPROXY_EXE) prog/weaveexec/sigproxy
|
||||
cp $(WEAVEPROXY_EXE) prog/weaveexec/weaveproxy
|
||||
cp $(WEAVEWAIT_EXE) prog/weaveexec/weavewait
|
||||
cp $(WEAVEWAIT_NOOP_EXE) prog/weaveexec/weavewait_noop
|
||||
cp $(WEAVEWAIT_NOMCAST_EXE) prog/weaveexec/weavewait_nomcast
|
||||
cp $(NETCHECK_EXE) prog/weaveexec/netcheck
|
||||
cp $(DOCKERTLSARGS_EXE) prog/weaveexec/docker_tls_args
|
||||
cp $(DOCKER_DISTRIB) prog/weaveexec/docker.tgz
|
||||
$(SUDO) DOCKER_HOST=$(DOCKER_HOST) docker build -t $(WEAVEEXEC_IMAGE) prog/weaveexec
|
||||
touch $@
|
||||
|
||||
$(DOCKERPLUGIN_UPTODATE): prog/plugin/Dockerfile $(DOCKERPLUGIN_EXE) $(WEAVEEXEC_UPTODATE)
|
||||
$(SUDO) docker build -t $(DOCKERPLUGIN_IMAGE) prog/plugin
|
||||
touch $@
|
||||
|
||||
$(WEAVE_EXPORT): $(IMAGES_UPTODATE)
|
||||
$(SUDO) DOCKER_HOST=$(DOCKER_HOST) docker save $(addsuffix :latest,$(IMAGES)) | gzip > $@
|
||||
|
||||
$(DOCKER_DISTRIB):
|
||||
curl -o $(DOCKER_DISTRIB) $(DOCKER_DISTRIB_URL)
|
||||
|
||||
tests:
|
||||
echo "mode: count" > profile.cov
|
||||
fail=0 ; \
|
||||
for dir in $$(find . -type f -name '*_test.go' | xargs -n1 dirname | sort -u); do \
|
||||
output=$$(mktemp cover.XXXXXXXXXX) ; \
|
||||
if ! go test -tags netgo -covermode=count -coverprofile=$$output $$dir ; then \
|
||||
fail=1 ; \
|
||||
fi ; \
|
||||
if [ -f $$output ]; then \
|
||||
tail -n +2 <$$output >>profile.cov; \
|
||||
rm $$output; \
|
||||
fi \
|
||||
done ; \
|
||||
exit $$fail
|
||||
go tool cover -html=profile.cov -o=coverage.html
|
||||
tests: tools/.git
|
||||
tools/test
|
||||
|
||||
$(PUBLISH): publish_%:
|
||||
$(SUDO) docker tag -f $(DOCKERHUB_USER)/$* $(DOCKERHUB_USER)/$*:$(WEAVE_VERSION)
|
||||
$(SUDO) docker push $(DOCKERHUB_USER)/$*:$(WEAVE_VERSION)
|
||||
$(SUDO) docker push $(DOCKERHUB_USER)/$*:latest
|
||||
lint: tools/.git
|
||||
tools/lint -nocomment -notestpackage .
|
||||
|
||||
tools/.git:
|
||||
git submodule update --init
|
||||
|
||||
$(RUNNER_EXE): tools/.git
|
||||
make -C tools/runner
|
||||
|
||||
$(PUBLISH): publish_%: $(IMAGES_UPTODATE)
|
||||
$(SUDO) DOCKER_HOST=$(DOCKER_HOST) docker tag -f $(DOCKERHUB_USER)/$* $(DOCKERHUB_USER)/$*:$(WEAVE_VERSION)
|
||||
$(SUDO) DOCKER_HOST=$(DOCKER_HOST) docker push $(DOCKERHUB_USER)/$*:$(WEAVE_VERSION)
|
||||
ifneq ($(UPDATE_LATEST),false)
|
||||
$(SUDO) DOCKER_HOST=$(DOCKER_HOST) docker push $(DOCKERHUB_USER)/$*:latest
|
||||
endif
|
||||
|
||||
publish: $(PUBLISH)
|
||||
|
||||
clean:
|
||||
-$(SUDO) docker rmi $(WEAVER_IMAGE) $(WEAVEDNS_IMAGE) $(WEAVEEXEC_IMAGE)
|
||||
rm -f $(WEAVER_EXE) $(WEAVEDNS_EXE) $(SIGPROXY_EXE) $(WEAVER_EXPORT) $(WEAVEDNS_EXPORT) $(WEAVEEXEC_EXPORT)
|
||||
clean-bin:
|
||||
-$(SUDO) DOCKER_HOST=$(DOCKER_HOST) docker rmi $(IMAGES)
|
||||
go clean -r $(addprefix ./,$(dir $(EXES)))
|
||||
rm -f $(EXES) $(IMAGES_UPTODATE) $(WEAVE_EXPORT)
|
||||
|
||||
clean: clean-bin
|
||||
rm -rf test/tls/*.pem test/coverage.* test/coverage
|
||||
|
||||
build:
|
||||
$(SUDO) go clean -i net
|
||||
$(SUDO) go install -tags netgo std
|
||||
$(MAKE)
|
||||
|
||||
run-smoketests: all
|
||||
cd test && ./setup.sh && ./run_all.sh
|
||||
@@ -1,9 +1,20 @@
|
||||
# Weave - the Docker network
|
||||
# Weave - weaving containers into applications
|
||||
|
||||
[](https://travis-ci.org/weaveworks/weave) [](https://coveralls.io/r/weaveworks/weave)
|
||||
[](https://travis-ci.org/weaveworks/weave) [](https://circleci.com/gh/weaveworks/weave) [](https://coveralls.io/r/weaveworks/weave)
|
||||
|
||||
# About Weaveworks
|
||||
|
||||
[Weaveworks](http://weave.works) is the company that develops Weave -
|
||||
the most productive way for developers to connect, observe and control
|
||||
Docker containers. To learn about our products, including getting
|
||||
started tutorials, visit our [website](http://weave.works) and
|
||||
[documentation](http://docs.weave.works) or continue to read about
|
||||
some of the more technical aspects of Weave in this readme.
|
||||
|
||||
# Weave
|
||||
|
||||
Weave creates a virtual network that connects Docker containers
|
||||
deployed across multiple hosts.
|
||||
deployed across multiple hosts and enables their automatic discovery.
|
||||
|
||||

|
||||
|
||||
@@ -29,21 +40,30 @@ capabilities, so these can continue to be used by containers.
|
||||
|
||||
## Installation
|
||||
|
||||
Ensure you are running Linux (kernel 3.5 or later) and have Docker
|
||||
(version 1.3.1 or later) installed. Then install weave with
|
||||
Ensure you are running Linux (kernel 3.8 or later) and have Docker
|
||||
(version 1.6.0 or later) installed. Then install weave with
|
||||
|
||||
sudo wget -O /usr/local/bin/weave \
|
||||
https://github.com/weaveworks/weave/releases/download/latest_release/weave
|
||||
sudo curl -L git.io/weave -o /usr/local/bin/weave
|
||||
sudo chmod a+x /usr/local/bin/weave
|
||||
|
||||
For usage on OSX (with Docker Machine) you first need to make sure
|
||||
that a VM is running and configured, as shown in [the Docker Machine
|
||||
documentation](https://docs.docker.com/installation/mac/#from-your-shell).
|
||||
Then you can launch weave directly from the OSX host.
|
||||
|
||||
CoreOS users see [here](https://github.com/fintanr/weave-gs/blob/master/coreos-simple/user-data) for an example of installing weave using cloud-config.
|
||||
|
||||
Amazon ECS users see
|
||||
[here](https://github.com/weaveworks/guides/blob/master/aws-ecs/LATESTAMIs.md)
|
||||
for the latest Weave AMIs and
|
||||
[here](http://weave.works/guides/service-discovery-with-weave-aws-ecs.html) to get started with Weave on ECS.
|
||||
|
||||
Weave respects the environment variable `DOCKER_HOST`, so you can run
|
||||
it locally to control a weave network on a remote host.
|
||||
|
||||
## Quick Start Screencast
|
||||
|
||||
<a href="http://youtu.be/k6r7yuSr0hE" alt="Click to watch the screencast" target="_blank">
|
||||
<a href="https://youtu.be/kihQCCT1ykE" alt="Click to watch the screencast" target="_blank">
|
||||
<img src="/docs/hello-screencast.png" />
|
||||
</a>
|
||||
|
||||
@@ -53,99 +73,98 @@ Say you have docker running on two hosts, accessible to each other as
|
||||
`$HOST1` and `$HOST2`, and want to deploy an application consisting of
|
||||
two containers, one on each host.
|
||||
|
||||
On `$HOST1` run (as root)
|
||||
On $HOST1 we run:
|
||||
|
||||
host1# weave launch
|
||||
host1# C=$(weave run 10.2.1.1/24 -t -i ubuntu)
|
||||
host1$ weave launch
|
||||
host1$ eval $(weave env)
|
||||
host1$ docker run --name a1 -ti ubuntu
|
||||
|
||||
The first line starts the weave router, in a container. This needs to
|
||||
be done once on each host. The required docker image for the weave
|
||||
router container is downloaded automatically. There is also a `weave
|
||||
setup` command for downloading this and other images required for
|
||||
weave operation; this is a strictly optional step which is especially
|
||||
useful for automated installation of weave and ensures that any
|
||||
subsequent weave commands do not encounter delays due to image
|
||||
downloading.
|
||||
> NB: If the first command results in an error like
|
||||
> `http:///var/run/docker.sock/v1.19/containers/create: dial unix
|
||||
> /var/run/docker.sock: permission denied. Are you trying to connect
|
||||
> to a TLS-enabled daemon without TLS?` then you likely need to be
|
||||
> 'root' in order to connect to the Docker daemon. If so, run the
|
||||
> above and all subsequent commands in a *single* root shell (e.g. one
|
||||
> created with `sudo -s`). Do *not* prefix individual commands with
|
||||
> `sudo`, since some commands modify environment entries and hence
|
||||
> they all need to be executed from the same shell.
|
||||
|
||||
The second line runs our application container. We give it an IP
|
||||
address and network, in
|
||||
[CIDR notation](http://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing#CIDR_notation).
|
||||
`weave run` invokes `docker run -d` with all the parameters following
|
||||
the IP address and network. So we could be launching any container
|
||||
this way; here we just take a stock ubuntu container and launch a
|
||||
shell in it. There's also a `weave start` command, which invokes
|
||||
`docker start` for starting existing containers.
|
||||
The first line runs weave. The second line configures our environment
|
||||
so that containers launched via the docker command line are
|
||||
automatically attached to the weave network. Finally, we run our
|
||||
application container.
|
||||
|
||||
If our application consists of more than one container on this host we
|
||||
simply launch them with a variation on that second line.
|
||||
That's it! If our application consists of more than one container on
|
||||
this host we simply launch them with `docker run` as appropriate.
|
||||
|
||||
The IP addresses and netmasks can be anything you like, but make sure
|
||||
they don't conflict with any IP ranges in use on the hosts or IP
|
||||
addresses of external services the hosts or containers need to connect
|
||||
to. The same IP range must be used everywhere, and the individual IP
|
||||
addresses must, of course, be unique.
|
||||
Next we repeat similar steps on `$HOST2`...
|
||||
|
||||
We repeat similar steps on `$HOST2`...
|
||||
host2$ weave launch $HOST1
|
||||
host2$ eval $(weave env)
|
||||
host2$ docker run --name a2 -ti ubuntu
|
||||
|
||||
host2# weave launch $HOST1
|
||||
host2# C=$(weave run 10.2.1.2/24 -t -i ubuntu)
|
||||
|
||||
The only difference, apart from the choice of IP address for the
|
||||
application container, is that we tell our weave that it should peer
|
||||
with the weave on `$HOST1` (specified as the IP address or hostname,
|
||||
and optional `:port`, by which `$HOST2` can reach it). NB: if there is
|
||||
a firewall between `$HOST1` and `$HOST2`, you must open the weave port
|
||||
(6783 by default; this can be overriden by setting `WEAVE_PORT`) for
|
||||
TCP and UDP.
|
||||
The only difference, apart from the name of the application container,
|
||||
is that we tell our weave that it should peer with the weave on
|
||||
`$HOST1` (specified as the IP address or hostname, and optional
|
||||
`:port`, by which `$HOST2` can reach it). NB: if there is a firewall
|
||||
between `$HOST1` and `$HOST2`, you must permit traffic to the weave
|
||||
control port (TCP 6783) and data ports (UDP 6783/6784).
|
||||
|
||||
Note that we could instead have told the weave on `$HOST1` to connect to
|
||||
`$HOST2`, or told both about each other. Order does not matter here;
|
||||
weave automatically (re)connects to peers when they become
|
||||
available. Also, we can tell weave to connect to multiple peers by
|
||||
supplying multiple addresses, separated by spaces. And we can
|
||||
[add peers dynamically](http://weaveworks.github.io/weave/features.html#dynamic-topologies).
|
||||
[add peers dynamically](http://docs.weave.works/weave/latest_release/features.html#dynamic-topologies).
|
||||
|
||||
Weave must be started once per host. The relevant container images are
|
||||
pulled down on demand, but if you wish you can preload them by running
|
||||
`weave setup` - this is particularly useful for automated deployments,
|
||||
and ensures that there are no delays during later operations.
|
||||
|
||||
Now that we've got everything set up, let's see whether our containers
|
||||
can talk to each other...
|
||||
|
||||
On `$HOST1`...
|
||||
In the container started on `$HOST1`...
|
||||
|
||||
host1# docker attach $C
|
||||
root@28841bd02eff:/# ping -c 1 -q 10.2.1.2
|
||||
PING 10.2.1.2 (10.2.1.2): 48 data bytes
|
||||
--- 10.2.1.2 ping statistics ---
|
||||
1 packets transmitted, 1 packets received, 0% packet loss
|
||||
round-trip min/avg/max/stddev = 1.048/1.048/1.048/0.000 ms
|
||||
root@a1:/# ping -c 1 -q a2
|
||||
PING a2.weave.local (10.40.0.2) 56(84) bytes of data.
|
||||
--- a2.weave.local ping statistics ---
|
||||
1 packets transmitted, 1 received, 0% packet loss, time 0ms
|
||||
rtt min/avg/max/mdev = 0.341/0.341/0.341/0.000 ms
|
||||
|
||||
Similarly, on `$HOST2`...
|
||||
Similarly, in the container started on `$HOST2`...
|
||||
|
||||
host2# docker attach $C
|
||||
root@f76829496120:/# ping -c 1 -q 10.2.1.1
|
||||
PING 10.2.1.1 (10.2.1.1): 48 data bytes
|
||||
--- 10.2.1.1 ping statistics ---
|
||||
1 packets transmitted, 1 packets received, 0% packet loss
|
||||
round-trip min/avg/max/stddev = 1.034/1.034/1.034/0.000 ms
|
||||
root@a2:/# ping -c 1 -q a1
|
||||
PING a1.weave.local (10.32.0.2) 56(84) bytes of data.
|
||||
--- a1.weave.local ping statistics ---
|
||||
1 packets transmitted, 1 received, 0% packet loss, time 0ms
|
||||
rtt min/avg/max/mdev = 0.366/0.366/0.366/0.000 ms
|
||||
|
||||
So there we have it, two containers on separate hosts happily talking
|
||||
to each other.
|
||||
|
||||
## Find out more
|
||||
|
||||
* [Documentation homepage](http://weaveworks.github.io/weave/)
|
||||
* [Features](http://weaveworks.github.io/weave/features.html)
|
||||
* [Troubleshooting](http://weaveworks.github.io/weave/troubleshooting.html)
|
||||
* [Building](http://weaveworks.github.io/weave/building.html)
|
||||
* [How it works](http://weaveworks.github.io/weave/how-it-works.html)
|
||||
* [WeaveDNS README](https://github.com/weaveworks/weave/tree/master/weavedns#readme)
|
||||
* [Documentation homepage](http://docs.weave.works/weave/latest_release/)
|
||||
* [Getting Started Guides](http://weave.works/guides/)
|
||||
* [Features](http://docs.weave.works/weave/latest_release/features.html)
|
||||
* [Troubleshooting](http://docs.weave.works/weave/latest_release/troubleshooting.html)
|
||||
* [Building](http://docs.weave.works/weave/latest_release/building.html)
|
||||
* [How it works](http://docs.weave.works/weave/latest_release/how-it-works.html)
|
||||
|
||||
## Contact Us
|
||||
|
||||
Found a bug, want to suggest a feature, or have a question?
|
||||
[File an issue](https://github.com/weaveworks/weave/issues), or email
|
||||
help@weave.works. When reporting a bug, please include which version of
|
||||
weave you are running, as shown by `weave version`.
|
||||
Found a bug, want to suggest a feature, or have a question? You can
|
||||
[File an issue](https://github.com/weaveworks/weave/issues). And
|
||||
there is a [Weave Users Google
|
||||
Group](https://groups.google.com/a/weave.works/forum/#!forum/weave-users),
|
||||
which you can post to by email at weave-users@weave.works. Or you can
|
||||
contact Weaveworks directly at help@weave.works. When reporting a
|
||||
bug, please include which version of weave you are running, as shown
|
||||
by `weave version`.
|
||||
|
||||
Follow weave on Twitter:
|
||||
Follow on Twitter:
|
||||
[@weaveworks](https://twitter.com/weaveworks).
|
||||
|
||||
Read the Weave blog:
|
||||
|
||||
Vendored
+21
-23
@@ -1,12 +1,25 @@
|
||||
VAGRANTFILE_API_VERSION = "2"
|
||||
|
||||
require './vagrant-common.rb'
|
||||
|
||||
vm_ip = "172.16.0.3" # arbitrary private IP
|
||||
pkgs = "lxc-docker aufs-tools build-essential ethtool iputils-arping libpcap-dev git mercurial bc"
|
||||
|
||||
pkgs = %w(
|
||||
docker-engine
|
||||
aufs-tools
|
||||
build-essential
|
||||
ethtool
|
||||
iputils-arping
|
||||
libpcap-dev
|
||||
git
|
||||
mercurial
|
||||
bc
|
||||
)
|
||||
|
||||
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
|
||||
|
||||
config.vm.box = "phusion/ubuntu-14.04-amd64"
|
||||
config.vm.box_url = "https://oss-binaries.phusionpassenger.com/vagrant/boxes/latest/ubuntu-14.04-amd64-vbox.box"
|
||||
config.vm.box = "ubuntu/ubuntu-15.04-amd64"
|
||||
config.vm.box_url = "https://cloud-images.ubuntu.com/vagrant/vivid/current/vivid-server-cloudimg-amd64-vagrant-disk1.box"
|
||||
|
||||
config.vm.network "private_network", ip: vm_ip
|
||||
config.vm.provider :virtualbox do |vb|
|
||||
@@ -16,27 +29,12 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
|
||||
|
||||
config.vm.synced_folder ".", "/vagrant", disabled: true
|
||||
config.vm.synced_folder ".", "/home/vagrant/src/github.com/weaveworks/weave"
|
||||
config.vm.provision :shell, :inline => "ln -s /home/vagrant/src/github.com/weaveworks/weave /home/vagrant/weave"
|
||||
|
||||
config.vm.provision :shell, :inline => "sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 36A1D7869245C8950F966E92D8576A8BA88D21E9"
|
||||
config.vm.provision :shell, :inline => "echo deb https://get.docker.io/ubuntu docker main > /etc/apt/sources.list.d/docker.list"
|
||||
|
||||
pkg_cmd = "export DEBIAN_FRONTEND=noninteractive; " \
|
||||
"apt-get update -qq; " \
|
||||
"apt-get install -qq --no-install-recommends #{pkgs}"
|
||||
|
||||
install_go_toolchain = "curl -s https://storage.googleapis.com/golang/go1.4.2.linux-amd64.tar.gz | tar xz -C /usr/local"
|
||||
|
||||
config.vm.provision :shell, :inline => pkg_cmd
|
||||
config.vm.provision :shell, :inline => install_go_toolchain
|
||||
config.vm.provision :shell, :inline => "usermod -a -G docker vagrant; "
|
||||
config.vm.provision :shell, :inline => "echo export GOPATH=/home/vagrant >> /home/vagrant/.bashrc"
|
||||
config.vm.provision :shell, :inline => "echo export PATH=/usr/local/go/bin:${PATH} >> /home/vagrant/.bashrc"
|
||||
config.vm.provision :shell, :inline => "chown -R vagrant:vagrant /home/vagrant/src"
|
||||
config.vm.provision :shell, :inline => "/usr/local/go/bin/go clean -i net; /usr/local/go/bin/go install -tags netgo std"
|
||||
|
||||
config.vm.provision :shell, :inline => "echo 'DOCKER_OPTS=\"-H unix:///var/run/docker.sock -H tcp://0.0.0.0:2375\"' >> /etc/default/docker"
|
||||
config.vm.provision :shell, :inline => "service docker restart"
|
||||
install_build_deps config.vm, pkgs
|
||||
install_go_toochain config.vm
|
||||
tweak_user_env config.vm
|
||||
tweak_docker_daemon config.vm
|
||||
cleanup config.vm
|
||||
|
||||
end
|
||||
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
func (client *Client) RegisterWithDNS(ID string, fqdn string, ip string) error {
|
||||
data := url.Values{}
|
||||
data.Add("fqdn", fqdn)
|
||||
_, err := client.httpVerb("PUT", fmt.Sprintf("/name/%s/%s", ID, ip), data)
|
||||
return err
|
||||
}
|
||||
|
||||
func (client *Client) DeregisterWithDNS(ID string, ip string) error {
|
||||
_, err := client.httpVerb("DELETE", fmt.Sprintf("/name/%s/%s", ID, ip), nil)
|
||||
return err
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
)
|
||||
|
||||
func (client *Client) ipamOp(ID string, op string) (*net.IPNet, error) {
|
||||
ip, err := client.httpVerb(op, fmt.Sprintf("/ip/%s", ID), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return parseIP(ip)
|
||||
}
|
||||
|
||||
// returns an IP for the ID given, allocating a fresh one if necessary
|
||||
func (client *Client) AllocateIP(ID string) (*net.IPNet, error) {
|
||||
return client.ipamOp(ID, "POST")
|
||||
}
|
||||
|
||||
// returns an IP for the ID given, or nil if one has not been
|
||||
// allocated
|
||||
func (client *Client) LookupIP(ID string) (*net.IPNet, error) {
|
||||
return client.ipamOp(ID, "GET")
|
||||
}
|
||||
|
||||
// release an IP which is no longer needed
|
||||
func (client *Client) ReleaseIP(ID string) error {
|
||||
_, err := client.ipamOp(ID, "DELETE")
|
||||
return err
|
||||
}
|
||||
|
||||
func (client *Client) DefaultSubnet() (*net.IPNet, error) {
|
||||
cidr, err := client.httpVerb("GET", fmt.Sprintf("/ipinfo/defaultsubnet"), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, ipnet, err := net.ParseCIDR(cidr)
|
||||
return ipnet, err
|
||||
}
|
||||
|
||||
func parseIP(body string) (*net.IPNet, error) {
|
||||
ip, ipnet, err := net.ParseCIDR(string(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ipnet.IP = ip
|
||||
return ipnet, nil
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
. "github.com/weaveworks/weave/common"
|
||||
)
|
||||
|
||||
const (
|
||||
WeaveHttpPort = 6784
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
baseUrl string
|
||||
resolve func() (string, error)
|
||||
}
|
||||
|
||||
func (client *Client) httpVerb(verb string, url string, values url.Values) (string, error) {
|
||||
baseUrl := client.baseUrl
|
||||
if client.resolve != nil {
|
||||
addr, err := client.resolve()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
baseUrl = fmt.Sprintf("http://%s:%d", addr, WeaveHttpPort)
|
||||
}
|
||||
url = baseUrl + url
|
||||
Log.Debugf("weave %s to %s with %v", verb, url, values)
|
||||
var body io.Reader
|
||||
if values != nil {
|
||||
body = strings.NewReader(values.Encode())
|
||||
}
|
||||
req, err := http.NewRequest(verb, url, body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if values != nil {
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
}
|
||||
if resp, err := http.DefaultClient.Do(req); err != nil {
|
||||
return "", err
|
||||
} else {
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusNoContent {
|
||||
return string(body), nil
|
||||
} else {
|
||||
return "", errors.New(resp.Status + ": " + string(body))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func NewClient(addr string) *Client {
|
||||
return &Client{baseUrl: fmt.Sprintf("http://%s:%d", addr, WeaveHttpPort)}
|
||||
}
|
||||
|
||||
func NewClientWithResolver(resolver func() (string, error)) *Client {
|
||||
return &Client{resolve: resolver}
|
||||
}
|
||||
|
||||
func (client *Client) Connect(remote string) error {
|
||||
_, err := client.httpVerb("POST", "/connect", url.Values{"peer": {remote}})
|
||||
return err
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
#!/bin/bash
|
||||
# This scipt lints go files for common errors.
|
||||
#
|
||||
# Its runs gofmt and go vet, and optionally golint and
|
||||
# gocyclo, if they are installed.
|
||||
#
|
||||
# With no arguments, it lints the current files staged
|
||||
# for git commit. Or you can pass it explicit filenames
|
||||
# (or directories) and it will lint them.
|
||||
#
|
||||
# To use this script automatically, run:
|
||||
# ln -s ../../bin/lint .git/hooks/pre-commit
|
||||
|
||||
set -eu
|
||||
|
||||
function lint_go {
|
||||
filename="$1"
|
||||
result=0
|
||||
|
||||
if [ -n "$(gofmt -l "${filename}")" ]; then
|
||||
result=1
|
||||
echo "${filename}: run gofmt -w ${filename}!"
|
||||
fi
|
||||
|
||||
go tool vet "${filename}" || result=$?
|
||||
|
||||
# golint is completely optional. If you don't like it
|
||||
# don't have it installed.
|
||||
if type golint >/dev/null 2>&1; then
|
||||
# golint doesn't set an exit code it seems
|
||||
lintoutput=$(golint "${filename}")
|
||||
if [ "$lintoutput" != "" ]; then
|
||||
result=1
|
||||
echo "$lintoutput"
|
||||
fi
|
||||
fi
|
||||
|
||||
# gocyclo is completely optional. If you don't like it
|
||||
# don't have it installed. Also never blocks a commit,
|
||||
# it just warns.
|
||||
if type gocyclo >/dev/null 2>&1; then
|
||||
gocyclo -over 25 "${filename}" | while read line; do
|
||||
echo "${filename}": higher than 25 cyclomatic complexity - "${line}"
|
||||
done
|
||||
fi
|
||||
|
||||
return $result
|
||||
}
|
||||
|
||||
function lint {
|
||||
filename="$1"
|
||||
ext="${filename##*\.}"
|
||||
|
||||
# Don't lint this script
|
||||
if [ "${filename}" = "bin/lint" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
case "$ext" in
|
||||
go) lint_go "${filename}"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
function lint_files {
|
||||
lint_result=0
|
||||
while read filename; do
|
||||
lint "${filename}" || lint_result=1
|
||||
done
|
||||
exit $lint_result
|
||||
}
|
||||
|
||||
function list_files {
|
||||
if [ $# -gt 0 ]; then
|
||||
find "$@" -type f
|
||||
else
|
||||
git diff --cached --name-only
|
||||
fi
|
||||
}
|
||||
|
||||
list_files "$@" | lint_files
|
||||
@@ -1,20 +0,0 @@
|
||||
#! /bin/bash
|
||||
set -e
|
||||
|
||||
# For info on why/how to use this script, see
|
||||
# https://github.com/weaveworks/weave/wiki/WorkingOnWeave#working-on-docs
|
||||
|
||||
MERGE_ARGS="-X subtree=site"
|
||||
|
||||
merge_dir=$(mktemp -d -t weave-merge.XXXXXX)/weave
|
||||
git clone -b gh-pages . $merge_dir
|
||||
|
||||
pushd $merge_dir
|
||||
git pull --ff-only git@github.com:weaveworks/weave gh-pages
|
||||
git fetch origin master
|
||||
git merge $MERGE_ARGS -e -m "Merge master into gh-pages" origin/master
|
||||
|
||||
popd
|
||||
git fetch $merge_dir gh-pages:gh-pages
|
||||
|
||||
# rm -rf $merge_dir
|
||||
+14
-2
@@ -14,19 +14,30 @@ set -e
|
||||
START=${FIRST_WEAVE:-1}
|
||||
COUNT=${NUM_WEAVES:-28}
|
||||
FINISH=$((START+COUNT-1))
|
||||
PORT=6783
|
||||
|
||||
weavedir=$(dirname $0)/..
|
||||
|
||||
# Generate a random MAC value - copied from 'weave' script
|
||||
random_mac() {
|
||||
# In the first byte of the MAC, the 'multicast' bit should be
|
||||
# clear and 'locally administered' bit should be set. All other
|
||||
# bits should be random.
|
||||
od -txC -An -N6 /dev/urandom | ( read a b c d e f && printf "%02x:$b:$c:$d:$e:$f" $((0x$a & ~1 | 2)) )
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
launch)
|
||||
shift 1
|
||||
echo $(date -R) launching
|
||||
for i in $(seq $START $FINISH); do
|
||||
WEAVE_CONTAINER_NAME=weave$i $weavedir/weave test-launch "$@"
|
||||
WEAVE_CONTAINER_NAME=weave$i WEAVE_PORT=$((5000+i)) $weavedir/weave launch-router --iface '' --name $(random_mac) --nickname weave$i --port $PORT --no-dns "$@"
|
||||
done
|
||||
if [ $START -ne 1 ] ; then
|
||||
status=$(docker inspect --format='{{.State.Running}} {{.NetworkSettings.IPAddress}}' weave$((START-1)))
|
||||
PREV="${status#true }"
|
||||
fi
|
||||
echo $(date -R) connecting
|
||||
for i in $(seq $START $FINISH); do
|
||||
if [ -n "$PREV" ] ; then
|
||||
echo connecting weave$i to $PREV
|
||||
@@ -35,11 +46,12 @@ case "$1" in
|
||||
status=$(docker inspect --format='{{.State.Running}} {{.NetworkSettings.IPAddress}}' weave$i)
|
||||
PREV="${status#true }"
|
||||
done
|
||||
echo $(date -R) done
|
||||
;;
|
||||
stop)
|
||||
for i in $(seq $START $FINISH); do
|
||||
echo Stopping weave$i
|
||||
WEAVE_CONTAINER_NAME=weave$i $weavedir/weave stop
|
||||
WEAVE_CONTAINER_NAME=weave$i $weavedir/weave stop-router
|
||||
done
|
||||
;;
|
||||
esac
|
||||
@@ -1,3 +0,0 @@
|
||||
#! /bin/sh
|
||||
set -e
|
||||
git push git@github.com:weaveworks/weave gh-pages:gh-pages
|
||||
+156
-65
@@ -10,15 +10,53 @@ RELEASE_DESCRIPTION=${RELEASE_DESCRIPTION:-"Weaving Containers into Applications
|
||||
|
||||
PWD=`pwd`
|
||||
|
||||
setup() {
|
||||
## Get the new version number from the most recent tag
|
||||
if ! LATEST_TAG=$(git describe --abbrev=0 --match='v*' 2>/dev/null) ; then
|
||||
echo "Could not find an annotated 'v*' version tag." >&2
|
||||
exit 1
|
||||
infer_release_type() {
|
||||
if echo $1 | grep -qE '^v[0-9]+\.[0-9]+\.0+$' ; then
|
||||
echo MAINLINE
|
||||
elif echo $1 | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+$' ; then
|
||||
echo BRANCH
|
||||
else
|
||||
echo PRERELEASE
|
||||
fi
|
||||
}
|
||||
|
||||
setup() {
|
||||
# Ensure we have exactly one annotated tag pointing at HEAD
|
||||
HEAD_TAGS=$(git tag --points-at HEAD)
|
||||
case $(echo $HEAD_TAGS | wc -w) in
|
||||
1)
|
||||
if [ $HEAD_TAGS != "latest_release" ] ; then
|
||||
LATEST_TAG=$HEAD_TAGS
|
||||
else
|
||||
echo "Cannot determine version - latest_release points at HEAD" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
0)
|
||||
echo "Cannot determine version - no tags point at HEAD" >&2
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
echo "Cannot determine version - multiple tags point at HEAD:" >&2
|
||||
for TAG in $HEAD_TAGS ; do
|
||||
echo -e "\t$TAG" >&2
|
||||
done
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
RELEASE_TYPE=$(infer_release_type $LATEST_TAG)
|
||||
echo "== Inferred release type $RELEASE_TYPE from tag $LATEST_TAG"
|
||||
|
||||
LATEST_TAG_SHA=$(git rev-parse $LATEST_TAG)
|
||||
LATEST_TAG_COMMIT_SHA=$(git rev-list -1 $LATEST_TAG)
|
||||
LATEST_RELEASE_SHA=$(git rev-parse latest_release)
|
||||
VERSION=${LATEST_TAG#v}
|
||||
LATEST_RELEASE_COMMIT_SHA=$(git rev-list -1 latest_release)
|
||||
if [ "$RELEASE_TYPE" != 'PRERELEASE' ] ; then
|
||||
VERSION=${LATEST_TAG#v}
|
||||
else
|
||||
VERSION=${LATEST_TAG}
|
||||
fi
|
||||
# NB does not check that this tag is on master
|
||||
RELEASE_DIR=./releases/$LATEST_TAG
|
||||
}
|
||||
@@ -39,8 +77,8 @@ build() {
|
||||
cd $RELEASE_DIR
|
||||
|
||||
## Check that the top changelog entry is this version
|
||||
if ! latest_changelog=$(grep -o "## Release [0-9].*" -m1 ./CHANGELOG.md) || \
|
||||
! [ `echo "$latest_changelog" | grep -o '[0-9][^ ]*'` == "$VERSION" ]; then
|
||||
if ! latest_changelog=$(grep -oP '(?<=^## Release ).*' -m1 ./CHANGELOG.md) || \
|
||||
! [ `echo "$latest_changelog" = "$VERSION"` ]; then
|
||||
echo -e "\u2757 Latest changelog entry \"$latest_changelog\" does not match the release version $VERSION" >&2
|
||||
exit 1
|
||||
fi
|
||||
@@ -61,7 +99,7 @@ build() {
|
||||
fi
|
||||
|
||||
## Run tests with the distributables, including version check
|
||||
v=$(./weaver/weaver --version | grep -o '[0-9].*')
|
||||
v=$(./prog/weaver/weaver --version | grep -oP '(?<=^weave router ).*')
|
||||
if ! [ "$v" == "$VERSION" ]; then
|
||||
echo "Version of distributable "$v" does not match release version $VERSION" >&2
|
||||
exit 1
|
||||
@@ -71,24 +109,13 @@ build() {
|
||||
echo '** Release artefacts in' $RELEASE_DIR
|
||||
}
|
||||
|
||||
publish() {
|
||||
draft() {
|
||||
setup
|
||||
|
||||
cd $PWD/$RELEASE_DIR
|
||||
|
||||
echo "== Sanity checks"
|
||||
|
||||
if ! which github-release >/dev/null; then
|
||||
echo "Please install git-release:" >&2
|
||||
echo -e "\tgo get github.com/aktau/github-release" >&2
|
||||
echo "and create a git token per https://github.com/aktau/github-release" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! [ $(git rev-list -1 $LATEST_TAG) == $(git rev-list -1 latest_release) ]; then
|
||||
echo -e "\u2757 The tag latest_release does not point to the same commit as $LATEST_TAG"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
## Check that the tag exists by looking at github
|
||||
if ! curl -sSf https://api.github.com/repos/$GITHUB_USER/weave/git/tags/$LATEST_TAG_SHA >/dev/null 2>&1; then
|
||||
echo -e "\u2757 Tag $LATEST_TAG is not on GitHub, or is not the same as the local tag" >&2
|
||||
@@ -98,19 +125,9 @@ publish() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
## Check that the 'latest_release' tag exists by looking at github
|
||||
if ! curl -sSf https://api.github.com/repos/$GITHUB_USER/weave/git/tags/$LATEST_RELEASE_SHA >/dev/null 2>&1; then
|
||||
echo -e "\u2757 Tag latest_release is not on GitHub, or is not the same as the local tag" >&2
|
||||
echo -e "\thttps://github.com/$GITHUB_USER/weave/tags" >&2
|
||||
echo "You may need to" >&2
|
||||
echo -e "\tgit tag -af latest_release"
|
||||
echo -e "\tgit push -f git@github.com:$GITHUB_USER/weave latest_release"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "\u2713 Tag $LATEST_TAG exists in GitHub repo $GITHUB_USER/weave"
|
||||
|
||||
## Check that the version doesn't already exist by looking at github
|
||||
## Check that the version does not already exist by looking at github
|
||||
## releases
|
||||
if github-release info --user $GITHUB_USER --repo weave --tag $LATEST_TAG >/dev/null 2>&1; then
|
||||
echo -e "\u2757 Release $LATEST_TAG already exists on GitHub" >&2
|
||||
@@ -120,13 +137,13 @@ publish() {
|
||||
|
||||
echo '** Sanity checks OK for publishing tag' $LATEST_TAG as $DOCKERHUB_USER/weave:$VERSION
|
||||
|
||||
echo "== Tagging and publishing images on docker hub as user $DOCKERHUB_USER"
|
||||
make SUDO=$SUDO WEAVE_VERSION=$VERSION DOCKERHUB_USER=$DOCKERHUB_USER publish
|
||||
RELEASE_ARGS="--draft"
|
||||
if [ "$RELEASE_TYPE" = 'PRERELEASE' ] ; then
|
||||
RELEASE_ARGS="$RELEASE_ARGS --pre-release"
|
||||
fi
|
||||
|
||||
echo "== Creating GitHub release $RELEASE_NAME $VERSION"
|
||||
# This cannot be done as a draft because of a bug in
|
||||
# github-release: https://github.com/aktau/github-release/issues/7
|
||||
github-release release \
|
||||
echo "== Creating GitHub release $RELEASE_ARGS $RELEASE_NAME $VERSION"
|
||||
github-release release $RELEASE_ARGS \
|
||||
--user $GITHUB_USER \
|
||||
--repo weave \
|
||||
--tag $LATEST_TAG \
|
||||
@@ -140,52 +157,126 @@ publish() {
|
||||
--name "weave" \
|
||||
--file "./weave"
|
||||
|
||||
echo "** Published release $RELEASE_NAME $VERSION"
|
||||
echo "** Draft $TYPE $RELEASE_NAME $VERSION created at"
|
||||
echo -e "\thttps://github.com/$GITHUB_USER/weave/releases/$LATEST_TAG"
|
||||
}
|
||||
|
||||
if github-release info --user $GITHUB_USER --repo weave \
|
||||
--tag latest_release >/dev/null 2>&1; then
|
||||
github-release delete \
|
||||
--user $GITHUB_USER \
|
||||
--repo weave \
|
||||
--tag latest_release
|
||||
publish() {
|
||||
setup
|
||||
cd $PWD/$RELEASE_DIR
|
||||
|
||||
UPDATE_LATEST=false
|
||||
if [ "$RELEASE_TYPE" = 'MAINLINE' ] ; then
|
||||
UPDATE_LATEST=true
|
||||
fi
|
||||
|
||||
github-release release \
|
||||
--user $GITHUB_USER \
|
||||
--repo weave \
|
||||
--tag latest_release \
|
||||
--name "$RELEASE_NAME latest ($VERSION)" \
|
||||
--description "$RELEASE_DESCRIPTION"
|
||||
if [ "$RELEASE_TYPE" = 'PRERELEASE' ] ; then
|
||||
echo "== Tagging and pushing images on docker hub as user $DOCKERHUB_USER"
|
||||
make UPDATE_LATEST=$UPDATE_LATEST SUDO=$SUDO WEAVE_VERSION=$VERSION DOCKERHUB_USER=$DOCKERHUB_USER publish
|
||||
echo "** Docker images tagged and pushed"
|
||||
|
||||
github-release upload \
|
||||
--user $GITHUB_USER \
|
||||
--repo weave \
|
||||
--tag latest_release \
|
||||
--name "weave" \
|
||||
--file "./weave"
|
||||
echo "== Publishing pre-release on GitHub"
|
||||
|
||||
echo "** Published $RELEASE_NAME as latest_release at"
|
||||
echo -e "\thttps://github.com/$GITHUB_USER/weave/releases/latest_release"
|
||||
github-release publish \
|
||||
--user $GITHUB_USER \
|
||||
--repo weave \
|
||||
--tag $LATEST_TAG
|
||||
|
||||
echo "** Pre-release $RELEASE_NAME $VERSION published at"
|
||||
echo -e "\thttps://github.com/$GITHUB_USER/weave/releases/$LATEST_TAG"
|
||||
else
|
||||
echo "== Sanity checks"
|
||||
if ! [ "$LATEST_TAG_COMMIT_SHA" == "$LATEST_RELEASE_COMMIT_SHA" ] ; then
|
||||
echo -e "\u2757 The tag latest_release does not point to the same commit as $LATEST_TAG" >&2
|
||||
echo "You may need to" >&2
|
||||
echo -e "\tgit tag -af latest_release $LATEST_TAG" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
## Check that the 'latest_release' tag exists by looking at github
|
||||
if ! curl -sSf https://api.github.com/repos/$GITHUB_USER/weave/git/tags/$LATEST_RELEASE_SHA >/dev/null 2>&1; then
|
||||
echo -e "\u2757 Tag latest_release is not on GitHub, or is not the same as the local tag" >&2
|
||||
echo -e "\thttps://github.com/$GITHUB_USER/weave/tags" >&2
|
||||
echo "You may need to" >&2
|
||||
echo -e "\tgit push -f git@github.com:$GITHUB_USER/weave latest_release" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo '** Sanity checks OK for publishing tag' $LATEST_TAG as $DOCKERHUB_USER/weave:$VERSION
|
||||
|
||||
echo "== Tagging and pushing images on docker hub as user $DOCKERHUB_USER"
|
||||
make UPDATE_LATEST=$UPDATE_LATEST SUDO=$SUDO WEAVE_VERSION=$VERSION DOCKERHUB_USER=$DOCKERHUB_USER publish
|
||||
echo "** Docker images tagged and pushed"
|
||||
|
||||
echo "== Publishing release on GitHub"
|
||||
|
||||
github-release publish \
|
||||
--user $GITHUB_USER \
|
||||
--repo weave \
|
||||
--tag $LATEST_TAG
|
||||
|
||||
if github-release info --user $GITHUB_USER --repo weave \
|
||||
--tag latest_release >/dev/null 2>&1; then
|
||||
github-release delete \
|
||||
--user $GITHUB_USER \
|
||||
--repo weave \
|
||||
--tag latest_release
|
||||
fi
|
||||
|
||||
github-release release \
|
||||
--user $GITHUB_USER \
|
||||
--repo weave \
|
||||
--tag latest_release \
|
||||
--name "$RELEASE_NAME latest ($VERSION)" \
|
||||
--description "[Release Notes](https://github.com/$GITHUB_USER/weave/releases/$LATEST_TAG)"
|
||||
|
||||
github-release upload \
|
||||
--user $GITHUB_USER \
|
||||
--repo weave \
|
||||
--tag latest_release \
|
||||
--name "weave" \
|
||||
--file "./weave"
|
||||
|
||||
echo "** Release $RELEASE_NAME $VERSION published at"
|
||||
echo -e "\thttps://github.com/$GITHUB_USER/weave/releases/$LATEST_TAG"
|
||||
echo -e "\thttps://github.com/$GITHUB_USER/weave/releases/latest_release"
|
||||
fi
|
||||
}
|
||||
|
||||
usage() {
|
||||
echo "Usage:"
|
||||
echo -e "\t./bin/release build"
|
||||
echo "-- Build artefacts for the latest version tag"
|
||||
echo -e "\t./bin/release draft"
|
||||
echo "-- Create draft release with artefacts in GitHub"
|
||||
echo -e "\t./bin/release publish"
|
||||
echo "-- Publish the artefacts for the latest tag to GitHub and DockerHub"
|
||||
echo "-- Publish the GitHub release and update DockerHub"
|
||||
exit 1
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
# Ensure required tooling is installed
|
||||
if ! which github-release >/dev/null; then
|
||||
echo "Please install git-release:" >&2
|
||||
echo -e "\tgo get github.com/weaveworks/github-release" >&2
|
||||
echo "and create a git token per https://github.com/weaveworks/github-release" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
[ $# -eq 0 ] && usage
|
||||
COMMAND=$1
|
||||
shift
|
||||
|
||||
case "$COMMAND" in
|
||||
build)
|
||||
build
|
||||
build "$@"
|
||||
;;
|
||||
draft)
|
||||
draft "$@"
|
||||
;;
|
||||
publish)
|
||||
publish
|
||||
publish "$@"
|
||||
;;
|
||||
*)
|
||||
echo "Unknown command \"$1\""
|
||||
echo "Unknown command \"$COMMAND\""
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
Executable
+57
@@ -0,0 +1,57 @@
|
||||
#!/bin/sh
|
||||
|
||||
## This is the shape of unencrypted `do-setup-ci-secrets`
|
||||
##
|
||||
## cat > /tmp/boto_gsutil_cert.pem << EOF
|
||||
## -----BEGIN PRIVATE KEY-----
|
||||
## <...>
|
||||
## -----END PRIVATE KEY-----
|
||||
## EOF
|
||||
##
|
||||
## cat > ~/.boto << EOF
|
||||
## [Credentials]
|
||||
## gs_service_client_id = <...>
|
||||
## gs_service_key_file = /tmp/boto_gsutil_cert.pem
|
||||
##
|
||||
## [Boto]
|
||||
## https_validate_certificates = True
|
||||
##
|
||||
## [GSUtil]
|
||||
## content_language = en
|
||||
## default_api_version = 2
|
||||
## default_project_id = <...>
|
||||
## EOF
|
||||
|
||||
openssl base64 -d << EOF \
|
||||
| openssl aes-256-cbc -d -K $1 -iv $2 \
|
||||
-out bin/do-setup-ci-secrets
|
||||
mOYNzNsAdwva0LbB2YFMY9VgL0SITFu1bPrbOujyLtOrBNjmOYbzR5d/sG+ARMFr
|
||||
7NZ5OKcvMbItllz0n7TtNkzkCOMztMC870Wcx3nGJzbBephdcYwos4tBlp5bihTU
|
||||
AXgxLUiDT1broYAFKnbE1aZC/XyNxuLTN+nXJWIWeijaXTgvhkEEm3tab+z7ioCb
|
||||
tlg71W1BrUuocctAJAuVwH/uTZAaXaJGJj1s2S7F2DrFnG6sFlrp8rnCucPLWKnC
|
||||
4IwJFC5saR5gxK6wECffwmwbpeXVOoQ58OgzxoZTEyAr9nhVbaBt5UZz3tfHlx8W
|
||||
Rn5jHehACw5kg5eMtGPqMEksJ63LX4h3mfSTLJXJHEmOrzCRpFrigVrsbA408U2u
|
||||
8jJTHJmKrGfwBh5ZFzBg6NuV4VplOp+ZquJLgegv2WH/iu/ciopXqB2YDCdx5ZBi
|
||||
nHV2Puth7kjS4EMCKUGfeqpesjL9Djb8cygzv/J3ZBmLjNH9dqlSXWy2DcFZA1Iy
|
||||
pFTTHkVU3w5sNz3nYOuT2UPQvULBW0FUndDKX6olyVa0mlxPwGBP9N3yGg918/rw
|
||||
7rNV/r54lA334F5QMhDrVxar6sl9wfLA+Z+8qemBbNFlLog1uQGhuwWK2nFu1tRM
|
||||
9uyvQIq0oq5NHKpogWc7NEuk7CPe1JyfRIcbMVj8+ilmKjUPB2HPC43IDmKt9/56
|
||||
rY3IXAcxZ/ClGfGpeNHXqop9T/WhfaowponOZFcdGSUddG0OswqkTE4KIv71mgTF
|
||||
jXooMkTQBOG+gBnCe+8bCRdRCG3anxHX74za4dGD58cOJtl6DYWnjWuR91NWnxdT
|
||||
1Zr3iUe41txg9hCNY9LHo2QHGLPxe6pmNnSClQ1mcFhuY6NF8RmCX3HAGKJQkxJb
|
||||
vpLGbXTXjAIFgIYgLNgKP2RLvCif/LmSWgkvE3ZU9GXtCUbm6xgJodwOqXiI9uuO
|
||||
4H4+/vpGHnSFENiclkgMFrLG4yiMu8ArgmnCBzg9E2AoOX5zCrh/2ZRTU8Dk4mt7
|
||||
jUd5SKQLsjB9QtB+y3tRO7zY9RDvR6iBOPRsjx6t8EJMCfPb06KchfMJw+VGxvMM
|
||||
48jtmepREFnhd/8XDRGnw5bznjRuwohGik8BToltjIviMkkpifKWaLwlaOiDXaHh
|
||||
VBFNXnUKvOBwRMU+PZqv4+DP2qff1gSXnGKqA5GIURYrK9P7TqHHNTnmjBdfJLzc
|
||||
0x+W1DTiYhLT2vrLrkEpZ99o6qrg+lwvjat6IsJF8xoQEo4pLD/ZRPJ0WzoDyctT
|
||||
oWZ5rMsN1TzTvJrBrTAWIJyhIzZcuNwChMrU6qogMr0fd6TSxWhWn0sOJ5y8AaTR
|
||||
ej4JHx0qroS/9JPmgFYU61pX3c0ypdef/sSHWG23mcFzLTgwe2d4ty9VakypZy3T
|
||||
sXCl6EHVVXlUFWhY4zQCjdZpYJv/rtoKL1BL4fPE2NV5WTjCL2GNvaO5SfHR7pA0
|
||||
yQJR/iU+cS2n8UXsWNjyV+/qEn6+QXuyVHGLbxyH8EeqlliAbyHsc2rzVI8Xctzo
|
||||
W5DuIqxwHAkNNurhmx27ktIIQ6HhaCp028SWwvLttkbBBgF4v1mmd4u/2bKbKjUd
|
||||
dOaetaIUx1hU9OAeaePvVkeTDcc3mlFtPARCS9Fqj16hgJQ43onZyC0H1K2WAxmU
|
||||
GsmzjCHlTNquzFTuuQihYGZKfsWIh9z6HEOkvNJ3VEQ=
|
||||
EOF
|
||||
|
||||
exec sh bin/do-setup-ci-secrets
|
||||
Executable
+115
@@ -0,0 +1,115 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu
|
||||
|
||||
# openssl enc -in do-setup-circleci-secrets.orig -out setup-circleci-secrets.orig -e -aes256 -pass stdin
|
||||
# openssl base64 < setup-circleci-secrets.orig
|
||||
|
||||
openssl base64 -d << EOF \
|
||||
| openssl enc \
|
||||
-out bin/do-setup-circleci-secrets \
|
||||
-d -aes256 -pass pass:$1
|
||||
U2FsdGVkX1/3QHnDgpZn0srp+GiEQBTlag0CBdsw6Bp6iaIKy6Jy8gFshrsmvl3+
|
||||
c+7hn1nqsBVpzVH0S5QA3Ex5nx9rl7/dcGoogsySFcwMmBFcauT0UTMK1bvX0Hqx
|
||||
QrshYSndUkww72KA7EsSnjR1BBJWm3AhCDOTIcnO6nQhig1Fc+SBEj5FGS5xTQ5Z
|
||||
/8hDsKf54Ms49vOO6QeE5w5ndZ+k7IvVJfq9Evhb7htD20RntDhR86foAwMnLV7R
|
||||
Wh9qAqI3gnRq0GmbeRKaHmB7pcLq9f0OdScqM/Tn1D+q0556fqJjBziXpZiGbxVs
|
||||
DxEp8zDJ8yNDAi0Wb2NXjoA79SXJ8Q7iTieaST4y2OmXQpY+Vw8RVDXxi6ERyVnH
|
||||
41OeMzyLm0qvCoMl32pe1sSG5DRCUiAm7wOtDTVvNDRqmGJYUgwwexyayecJI1wQ
|
||||
LlhYLJavUvw3UGDZOld8BaMQhfOKBVCdFrjmA4HuVxmOf3029qkgzOCSZiQVNqs+
|
||||
fvM6J05zBuk+GPy8YInvRkzfxum/x00vmUyGjMs/lHDFfrmYDFK3ux41EoVqvWLG
|
||||
S5uYxdZJFdR3Xsrs+sMr51XNBQGMmz6KoEEzFYyHs5fZgtdpRdPAuQS1m6FqgZyi
|
||||
wSz6+EKsALtmGorEp+CF8JN5YzKJCrKzNkg0fILBqsqmJZGP64lmX69950lRLNGM
|
||||
/l3KVR2zH31b4AnnzTcvldVSCKDXjaMw3JA2Ce/MK6XhOWOcpEv4oDt/62lbhUKP
|
||||
QUQTPbfpQRvVfBcoIrfAm3ghi/CZc2TDVEuD7DhasLTZD3r7LoK261BWC9O5Wubi
|
||||
vlXuFIF+uyrpTDf6EJKE/+XywtArppffMP6RkHu1Iqnd8JtsqPaIZsmMmZbjgb6j
|
||||
BX4ZLb5U9R7jYoSgIke4+u6geFcrMQ+Lny4r7MxXaS0Y1UJ6Jo1bwpYw1TBIobZc
|
||||
eQ30+xhbSeRC9vEw1NXI6qM2Ai2gIHtAZvEiBkBBXTG5Ld7N/mBAmDIJEzVRxh6F
|
||||
jRkB3dzESF6eeTBm61OQepwV3aW6lslMaf26drPGv2W2q+K/WOpQOejfggtHUgVs
|
||||
9SWqtn7hHYSpIASe1Ihi14HAWv3kPhQien0zY1DLKJWf4ilbRaICn1QSJjNuKwtl
|
||||
A2ZwEw7FFddFcsF4okHPkxEoCre3shq0DEw9UA1gnwEaEF1Q+i9cvLtQg56zK3+R
|
||||
MfTzFUv2K1z3j1s5tC6Z6LzL8j682EFOll7MkihqiIWh+po1zcNs6Ln0SkjB3v9T
|
||||
OFRYVJis9GqXYO9PUW1mI69udR4nXbKlBl5tWNyAxn9hufJHLSAmbGMwActKLRM5
|
||||
kROapKrN1Ra/sHUmhoSSDUK9AXeYKkqPXVMbhly9xC1Kya4hFaFokjRQIMLPmXdQ
|
||||
lP46RL3HOq0v1BkXg05poSwDm+BtitZkcIhOzcmeucUqn1whsz39y3VoOhJqQu2j
|
||||
0dUcj19Fue1CDZfwzACb9mLZr643h0yCIHQaVT7is/yiN2vEt0S0zynvOA1D+Xgy
|
||||
4bFdjqed9DgBShLwF/lh4/hur5iZPpVr9A8rt3Vq0k5A1tdjsZpXP3qv5jLc7lJv
|
||||
cAQxdVLEuv8WPwwTV/EQsoH7lpoEdjktw/gu0v5PkUgR/WBxEqJq4liTekViQ+8X
|
||||
BAmdm/FGWeIxSSEm0a8I+muiBpIuKjuQoJit1f0k+lLUohoVDqpDDfeYHh1TxXHE
|
||||
xSKAKNmFSc2XOr5Zg2mOInxeH7WoxH6iylXKADBgEgAQ4C55DoAPRjFNtGGz2iJl
|
||||
WkzhQOfgsDss1VxpPZ4SWDQO+xxuuEnYcCYdJ6Tyi5hAwZ0+RZmXx0g9BCK+5qzc
|
||||
Ffei066kCItBcayekwCs0i86r8JvDtsMUUkzEKDQTSWHpP8ypIZoz3P+Skdqcs6p
|
||||
IcZHy2fODIsClctRAsHOjZ9BnWSY2rqf1lysqvRfOkNTViTOUVx2hf+LuL+8rT6g
|
||||
K4GvS39Q+c9FYSMjCNxv1m37NfHJv2W7AlqVV95gMBEFB97M3388T7PfvmSLsyPv
|
||||
qAKDE7genTOycQvEX9oeEACxPo08ai2Mgs/YXCig0g0btZoScoKPelQKtD0HLpZ+
|
||||
j7ruaKFy4/vySDMostB3AKSkCE/SkGu/W9oI37n1guSYC/9yJjtTcwZULXAClfwt
|
||||
LyHvLAw4+7BdGJqDe9Tf8z4zqVLax+BcBHkW7pbP+pvxC6fhIA4PO8k5HwwAVlRw
|
||||
68lG6e3z9+Vst4zrFLewHJvkDRSOTSecs9h9SPOjReTK0ovlDzMHVakfUvqpk9mv
|
||||
nCj8SWaMPF70z138AuRuKN7X4exNXPBFnOcseKp/NpT1DyvgoHm/oIO65mq9vvo+
|
||||
42uSbyyegYiTdVK4SaXBc2hixkhaTteqdEy6GxfWqKCvHtYuRD2LyUSjZn/0Y6CG
|
||||
3wZapylKL1sxfH+b1gogv2wAA8KtZAfiTlMISuymwQr5RbOFma7UVMVecCa7XrNG
|
||||
fZwO8R+6++ru0efDzeC/gtLOItjn+yVfsbk2gfu0FikHIV1sogOjnsv3dl6IpH/s
|
||||
w6WbuK/yZhyrrmhIIueHF30ES512Peogf/Gxp8U/ZAmqBa7p4qsv93XWDR52IPYz
|
||||
yU1MtboLD02J39daBNrYZuO4Yfo/xAvU7Ijg2KIR9HS0bSWNuSFAZaZXlJ2bSag3
|
||||
8l0dZMx2fZa0FALxWvY2+nd1tWcH+Wo99RSxX+4TH3guCpe1x/58yKOKL4d1xRpu
|
||||
02nGaqerV9bHU6WjCUYTrrZurTp6yqivpSFCALDMhAU5Ihd/Qjz69zg4uZ7iqbNd
|
||||
WqIYJ3SAOei2tSfoOJbdh8mohotnMFFbupck3Fv+lXAXYDp0V/sSuK32MSeJxan1
|
||||
q3t0eenAh6zoBP0Tj53N8B9YmrgZ1i1AljQqYsAdb8N5Z3vecuzQa946U8H4axEc
|
||||
BL5fgmqSfsbj1EjJVUrw3cE9hgqeqAYBBvZsyW5zSAsLq/RJKYtBxDGADqr0TOOu
|
||||
TjVqfPObb1u9EAQj15ALBhhPORa2gBAK5gnhJWW5deNgA0UOYoWARFYfATuQvr1T
|
||||
9xTVbqzZ0VJR4n7X5JyxJo2tZ44QYVzA0fqX0lt+uRkarrkbZMgplKp4MJTFCp2N
|
||||
tLciWS0YSHPbKTm7svw5mROazUvEoj1N3SYrJT+Ai9UgSL1cA4FmQ5Jsi+GOeQbH
|
||||
7E4pzjeST4b1aATY9ZZSy4TzK+UNYBcCVqY1dN5Xnk0e5yPMnrkkcYI41s/6oOaW
|
||||
lQkp/yrHkvpzQWZa7vF2yWg54j3eX8YWx/TsLvmuJmSQqiIHfu9TXcAIPDispCpF
|
||||
gp4ZbYPZ8RDQLw3ROc+bZ9E0J8e+FHwgXEELsMLW9n4d3PTj9pLFej1vmBKa03o3
|
||||
g+JViNxb1gUPNmC6ADk4o7TQzBrjV2pmmuCza16kOmq6oAbJPOeCLtVWA5HJTX8d
|
||||
AuOzBLO37tWP457dNqoNDaCMik3LCy8cslZjAz02D6VG/QGOAXP9x8bJ3zg9N7vW
|
||||
EOn1BMtIeMI4yIxpC6HgotNWDNbUBWremAFWYQRgYQqo9L1SdW4hz6X8FqdlBFvK
|
||||
ml7sdUwjEy2tNW+aiZI4cgAlI/VvMWYr5kPfhvaUu1wGYeS1TD1B+DPcjcXpOGWo
|
||||
vB1f/uvGc61zZWD0ph9f3S9OnvEko5qV3VhxOnxYUZTrqlM7LVgy/sHrSaPdR7Eo
|
||||
nb4QGdi5wEGMxVgQlDGwOmEmS5nCaA0RcfHbkQV4SebRleocLB9qcSwj5JzPICAB
|
||||
sn1qeSv45LnGdTzUnCQErw1EzgrK1FXOknujjZ3GX5dM7a6Gag0E6cAxoQeC8Mh0
|
||||
aUWolyBo9aSXltn7Vnywoe4X3v96eVH6tUQtN3wt51LBlfSDTWOQDeXLFyW57oYL
|
||||
/ErbmJLaSHpANBcC9InI+rxNYePTTba7K8rdPDppmMEmdOXjaL+5aPay2Bay9K8T
|
||||
FUyZdoyrbzxFZYT9/YyVJV/ukVcT9ru3HpzLTg9Vgc/CyRfIvCZY/Gzh+pPUbRsR
|
||||
pHYDT09BFS+mjszTkcdbceweYXJvknXpajMiISrPUUkb9rwCTmLS4QR4y7I9zAgJ
|
||||
9U72mCZB9yhhzS17kyKAANihXvOFrMXCO7QTWZbtSkobMBS2SybDxYP3rM5b6824
|
||||
HQaY4MZW5qCndydfz4fa908XbPTDka9Nu3CZfI3fhGiiGDFWViotGEYE4i9BuGZJ
|
||||
vmtzII7zeIoHwM4t5fb5mYDd9H+Rkct1yATGEIFEx7cu1fj4Ht2177EzhdkF3Q6I
|
||||
Fccn43yGDW06nFvEjnjb+Lz4EvtGp6dDefUhNBohdzd4Rm2o+etKobm0lZ1xOX74
|
||||
2gOGBLfXS6PF+msNkWP76mqKE6hG0+R1iPrN06Arti+ZRBhNGJDp1pnOERl7IyB0
|
||||
hiLUqae7xVSMVRDeq4O04EUpMXh5ixs3RJXjthYILt7vX+6qskJC3rMuyThrsvMi
|
||||
Kk0wErDpzR5U7KFnM2fk4jY5by/0b/jHZ/PWFGnaFrXNOiwK/mV1RIJtg5FCrvMv
|
||||
xJN5Qo7o2ZYzUL+IBf5mP5O49FCnz3e++9r4ySY/K57ybhQft1l+cQPWS7xuvVAg
|
||||
wZGM9DPl9AhBus0I3V3XzfkcWWHkMynMRcfTnkBHrmKjMopIOggsq05JPalM9b0V
|
||||
NfwZngVvTFbQejbGjgQQQ+G7spav/icCIA8Iu8JPHaKZt2IX77L1xMcQOouqMrx8
|
||||
JCXnJ7zsuU+8W3GQGm70H2kcen4bf4beA8TSkFmbVsaqDNtGaG+ZopWO8ctZX3CP
|
||||
jFgIYgEGi137IWvs1KD5QNFHZI6l0JMzglmvcfnViRPDHLi+EQXAoL/98j/YzA9t
|
||||
a9L4xMO0oucovKkqYOS2cdFaQKVIUM9L5ibcbW7Tol4ugMo5ibIsTgjZoyFohG6t
|
||||
acg+h2wTTyxtrIapvOOBeAo9V/iQowTAPQf38LKQZ5tApksiCG1qvJerKALTMp1J
|
||||
yUHjTCPCPsaiXVVO0mfa9OPtzVvi/icLmlz7umk82mew13djedgMS3/ghq5aEHbg
|
||||
e7rtI5snFlm/GQnV6IaCDytb+jYxqQ0DZ9Mb0CHcMpnLyiBO6nzHfadpZYlrxrpx
|
||||
dPx9MVqfLavj1UVjKuVj4Mgec9J38IBBRGMJZ4MGhde2hrw/s2jVzG1BzaIzcvFa
|
||||
bWiJOkKKRC4BY9DBDYfae5c/obwzkxLmvkbWAVzoFXc+46K1IK6bbC2a4EerOAxc
|
||||
C7B3X8BucD8OtiPORwXgjctd9AJ7cXq0EEVGoZZ8jkILnklsK6RXsxrT4gSOGFpx
|
||||
cBU7TF4kHpH+o4KGQmjlFA2VYP1tXirjEep57LOHfL+zy+HiSXdsbexf6odkqLYA
|
||||
1RR0TzSkmqJYvP0GfHPMvp2xredx1kP934fhTIUcWvErrG3NPd/Dod4BzQye1nNK
|
||||
8Y8aFYF6fACcD4SFZ11ecK9DJmB+WXGlsxtO3w1LeEysny2lVkkH6HuMRIzPyXAM
|
||||
/sEpwh3hwPJosAh6vGM6JxuL7CXP8hC1oUvK91VR0hxeDcO/LgdcHtDnlNMqt1wC
|
||||
jvYt9Hg+3aY5GehBnQPRppnIMaGhiBrRC6HjBg8Umt6UTUr5PXNY2aia5wRcx4M6
|
||||
Tj0O0dAt8WWZ9ySgzOoq4SRoaAp6m4E7CpRNMgXH031OWEeHqtt8rsYqAcvcQLuW
|
||||
vIwts5hiXZOfgXyR6SBY3ANE1SjDY46na2G4oiG8FI7eAqYgehjHrXqCnGrzjefE
|
||||
V1eVcIr7DBYuM8wq7C9x+qdMir6RNV4MrVD+7rO/drivnd3MaqDHlG0LfMZo6GIK
|
||||
QZGIy0z1W5lOXm9x3RAz7/Ea11ynkZyPDDxBE5NRcV6JfukdjDrvsaQA/Qk2Wo7h
|
||||
PdUZjv17H4syMdap6tUDTc8Cohut6n1UxnDnhaAYEDGR3TgPqI5vEaJ+yoaM9itv
|
||||
b2I/K+0uK6GfWwca7PeTtYn92/5etO4czG99xEBFzw7tiEmBTA8KfseZIUe/i3fr
|
||||
9LO+rKEGalFB4mVTYyCKPYYx4hOIvjdCox/jEBTFyYUEhgGnZvArgw9XabvII7eM
|
||||
c1FmwnN2CM/MHUzM0Kw7cS/oaoRFcG/PqioO/krL2Bd+xThwSH4Zn+Y1UVO7aXNy
|
||||
90MOehHSc2nhN984Y2re3ffVsjPhC3KdWxr8jbho/Az65rUkYg64roiO2ZqdqRvf
|
||||
aYtX9fCe8N8SzgLTlWZt+l8sjY8HP2k9NoajphV1nG4iC/pX0ZJiVQ+VBRNaGhSl
|
||||
6IhM9gM1ZsI86cULJpOTAhNu1fZBv+Mq8IXYD8SqlNVwVtjvshtlzBN4InSGEKiO
|
||||
HM7coo7aDuu+Go5AEiIVMbR09jAa0VINsCVScfSbTFhlGRTrZr2DSO/s8WxbV+Mb
|
||||
EQAfZmyrLF71Z3MfIukg+ps5iuqFhZ+gJSXXSFwumpg=
|
||||
EOF
|
||||
|
||||
exec sh bin/do-setup-circleci-secrets
|
||||
+4
-1
@@ -1,6 +1,9 @@
|
||||
FROM ubuntu
|
||||
|
||||
RUN apt-get -y update && apt-get -y install --no-install-recommends build-essential git ca-certificates docker.io mercurial libpcap-dev curl make pkg-config gcc bison flex
|
||||
RUN apt-get -y update && apt-get -y install --no-install-recommends ca-certificates apt-transport-https
|
||||
RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 36A1D7869245C8950F966E92D8576A8BA88D21E9
|
||||
RUN echo deb https://get.docker.io/ubuntu docker main > /etc/apt/sources.list.d/docker.list
|
||||
RUN apt-get -y update && apt-get -y install --no-install-recommends build-essential git lxc-docker-1.6.2 mercurial libpcap-dev curl make pkg-config gcc bison flex python-requests
|
||||
|
||||
# When doing a build in a container, "apt-get update" happens twice,
|
||||
# which can be a very significant overhead for incremental builds.
|
||||
|
||||
+2
-1
@@ -6,7 +6,7 @@ export GOPATH
|
||||
|
||||
WEAVE_SRC=$GOPATH/src/github.com/weaveworks/weave
|
||||
|
||||
if [ $# -eq 0 ] ; then
|
||||
if [ $# -eq 0 -o "$1" = "tests" ] ; then
|
||||
# No arguments. Expect that the weave repo will be bind-mounted
|
||||
# into $GOPATH
|
||||
if ! [ -e $WEAVE_SRC ] ; then
|
||||
@@ -35,6 +35,7 @@ EOF
|
||||
echo "weave:x:$uid:$gid::$WEAVE_SRC:/bin/sh" >>/etc/passwd
|
||||
echo "weave:*:::::::" >>/etc/shadow
|
||||
echo "weave ALL=(ALL) NOPASSWD: ALL" >>/etc/sudoers
|
||||
|
||||
su weave -c "PATH=$PATH make -C $WEAVE_SRC build"
|
||||
else
|
||||
# There are arguments to pass to git-clone
|
||||
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
general:
|
||||
branches:
|
||||
ignore:
|
||||
- gh\-pages
|
||||
- /pull\/.*/
|
||||
|
||||
machine:
|
||||
services:
|
||||
- docker
|
||||
environment:
|
||||
GOPATH: /home/ubuntu:$GOPATH
|
||||
SRCDIR: /home/ubuntu/src/github.com/weaveworks/weave
|
||||
PATH: $PATH:$HOME/.local/bin:$HOME/google-cloud-sdk/bin/
|
||||
CLOUDSDK_CORE_DISABLE_PROMPTS: 1
|
||||
WEAVE_TEST: $HOME/docker/weave-test.tar
|
||||
|
||||
dependencies:
|
||||
cache_directories:
|
||||
- "~/docker"
|
||||
override:
|
||||
- if [[ -e "$WEAVE_TEST" ]]; then
|
||||
docker load -i $WEAVE_TEST;
|
||||
else
|
||||
docker build -t weaveworks/weave-build build;
|
||||
docker build -t weaveworks/weave-test test/image;
|
||||
mkdir -p $(dirname "$WEAVE_TEST");
|
||||
docker save weaveworks/weave-test >$WEAVE_TEST;
|
||||
fi
|
||||
post:
|
||||
- sudo apt-get update && sudo apt-get install bc jq && pip install requests
|
||||
- curl https://sdk.cloud.google.com | bash
|
||||
- bin/setup-circleci-secrets "$SECRET_PASSWORD"
|
||||
- mkdir -p $(dirname $SRCDIR) && cp -r $(pwd)/ $SRCDIR
|
||||
- cd $SRCDIR/test; ./gce.sh make_template:
|
||||
parallel: false
|
||||
|
||||
test:
|
||||
override:
|
||||
- cd $SRCDIR; make lint:
|
||||
parallel: true
|
||||
- docker run -v /home/ubuntu:/home/go -e COVERDIR=test/coverage -e SLOW=true -e CIRCLECI -e CIRCLE_BUILD_NUM -e CIRCLE_NODE_TOTAL -e CIRCLE_NODE_INDEX weaveworks/weave-test:
|
||||
parallel: true
|
||||
- docker run -v /var/run/docker.sock:/run/docker.sock -v /home/ubuntu:/home/go -e COVERAGE=true weaveworks/weave-build:
|
||||
parallel: true
|
||||
- cd $SRCDIR/test; ./gce.sh setup && eval $(./gce.sh hosts) && ./setup.sh:
|
||||
parallel: true
|
||||
- cd $SRCDIR/test; eval $(./gce.sh hosts); export COVERAGE=true; ./run_all.sh:
|
||||
parallel: true
|
||||
timeout: 300
|
||||
- cd $SRCDIR; make clean-bin:
|
||||
parallel: true
|
||||
- docker run -v /var/run/docker.sock:/run/docker.sock -v /home/ubuntu:/home/go weaveworks/weave-build:
|
||||
parallel: true
|
||||
post:
|
||||
- cd $SRCDIR/test; ./gce.sh destroy:
|
||||
parallel: true
|
||||
|
||||
teardown:
|
||||
pre:
|
||||
- test "$CIRCLE_NODE_INDEX" != "0" || (cd $SRCDIR/test; ./gen_coverage_reports.sh):
|
||||
parallel: true
|
||||
- test "$CIRCLE_NODE_INDEX" != "0" || (go get github.com/mattn/goveralls && goveralls -repotoken $COVERALLS_REPO_TOKEN -coverprofile=$SRCDIR/test/profile.cov -service=circleci || true):
|
||||
parallel: true
|
||||
- test "$CIRCLE_NODE_INDEX" != "0" || (cd $SRCDIR/test; cp coverage.* $CIRCLE_ARTIFACTS):
|
||||
parallel: true
|
||||
|
||||
deployment:
|
||||
hub:
|
||||
branch: master
|
||||
owner: weaveworks
|
||||
commands:
|
||||
- docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS
|
||||
- cd $SRCDIR; make publish
|
||||
@@ -0,0 +1,132 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
docker "github.com/fsouza/go-dockerclient"
|
||||
|
||||
. "github.com/weaveworks/weave/common"
|
||||
)
|
||||
|
||||
// An observer for container events
|
||||
type ContainerObserver interface {
|
||||
ContainerStarted(ident string)
|
||||
ContainerDied(ident string)
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
*docker.Client
|
||||
}
|
||||
|
||||
// NewClient creates a new Docker client and checks we can talk to Docker
|
||||
func NewClient(apiPath string) (*Client, error) {
|
||||
if apiPath != "" && !strings.Contains(apiPath, "://") {
|
||||
apiPath = "tcp://" + apiPath
|
||||
}
|
||||
dc, err := docker.NewClient(apiPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client := &Client{dc}
|
||||
|
||||
return client, client.checkWorking()
|
||||
}
|
||||
|
||||
func NewVersionedClient(apiPath string, apiVersionString string) (*Client, error) {
|
||||
if !strings.Contains(apiPath, "://") {
|
||||
apiPath = "tcp://" + apiPath
|
||||
}
|
||||
dc, err := docker.NewVersionedClient(apiPath, apiVersionString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client := &Client{dc}
|
||||
|
||||
return client, client.checkWorking()
|
||||
}
|
||||
|
||||
func NewVersionedClientFromEnv(apiVersionString string) (*Client, error) {
|
||||
dc, err := docker.NewVersionedClientFromEnv(apiVersionString)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client := &Client{dc}
|
||||
|
||||
return client, client.checkWorking()
|
||||
}
|
||||
|
||||
func (c *Client) checkWorking() error {
|
||||
_, err := c.Version()
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) Info() string {
|
||||
if env, err := c.Version(); err != nil {
|
||||
return fmt.Sprintf("Docker API error: %s", err)
|
||||
} else {
|
||||
return fmt.Sprintf("Docker API on %s: %v", c.Endpoint(), env)
|
||||
}
|
||||
}
|
||||
|
||||
// AddObserver adds an observer for docker events
|
||||
func (c *Client) AddObserver(ob ContainerObserver) error {
|
||||
events := make(chan *docker.APIEvents)
|
||||
if err := c.AddEventListener(events); err != nil {
|
||||
Log.Errorf("[docker] Unable to add listener to Docker API: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
for event := range events {
|
||||
switch event.Status {
|
||||
case "start":
|
||||
id := event.ID
|
||||
ob.ContainerStarted(id)
|
||||
case "die":
|
||||
id := event.ID
|
||||
ob.ContainerDied(id)
|
||||
}
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsContainerNotRunning returns true if we have checked with Docker that the ID is not running
|
||||
func (c *Client) IsContainerNotRunning(idStr string) bool {
|
||||
container, err := c.InspectContainer(idStr)
|
||||
if err == nil {
|
||||
return !container.State.Running
|
||||
}
|
||||
if _, notThere := err.(*docker.NoSuchContainer); notThere {
|
||||
return true
|
||||
}
|
||||
Log.Errorf("[docker] Could not check container status: %s", err)
|
||||
return false
|
||||
}
|
||||
|
||||
// This is intended to find an IP address that we can reach the container on;
|
||||
// if it is on the Docker bridge network then that address; if on the host network
|
||||
// then localhost
|
||||
func (c *Client) GetContainerIP(nameOrID string) (string, error) {
|
||||
Log.Debugf("Getting IP for container %s", nameOrID)
|
||||
info, err := c.InspectContainer(nameOrID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if info.NetworkSettings.Networks != nil {
|
||||
Log.Debugln("Networks: ", info.NetworkSettings.Networks)
|
||||
if bridgeNetwork, ok := info.NetworkSettings.Networks["bridge"]; ok {
|
||||
return bridgeNetwork.IPAddress, nil
|
||||
} else if _, ok := info.NetworkSettings.Networks["host"]; ok {
|
||||
return "127.0.0.1", nil
|
||||
}
|
||||
} else if info.HostConfig.NetworkMode == "host" {
|
||||
return "127.0.0.1", nil
|
||||
}
|
||||
if info.NetworkSettings.IPAddress == "" {
|
||||
return "", errors.New("No IP address found for container " + nameOrID)
|
||||
}
|
||||
return info.NetworkSettings.IPAddress, nil
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package common
|
||||
|
||||
func CheckFatal(e error) {
|
||||
if e != nil {
|
||||
Error.Fatal(e)
|
||||
}
|
||||
}
|
||||
|
||||
func CheckWarn(e error) {
|
||||
if e != nil {
|
||||
Warning.Println(e)
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
)
|
||||
|
||||
func init() {
|
||||
InitLogging(ioutil.Discard, os.Stdout, os.Stdout, os.Stderr)
|
||||
}
|
||||
+51
-26
@@ -1,41 +1,66 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
standardLogFlags = log.Ldate | log.Ltime | log.Lmicroseconds
|
||||
)
|
||||
type textFormatter struct {
|
||||
}
|
||||
|
||||
// Largely taken from
|
||||
// http://www.goinggo.net/2013/11/using-log-package-in-go.html
|
||||
// Based off logrus.TextFormatter, which behaves completely
|
||||
// differently when you don't want colored output
|
||||
func (f *textFormatter) Format(entry *logrus.Entry) ([]byte, error) {
|
||||
b := &bytes.Buffer{}
|
||||
|
||||
levelText := strings.ToUpper(entry.Level.String())[0:4]
|
||||
timeStamp := entry.Time.Format("2006/01/02 15:04:05.000000")
|
||||
if len(entry.Data) > 0 {
|
||||
fmt.Fprintf(b, "%s: %s %-44s ", levelText, timeStamp, entry.Message)
|
||||
for k, v := range entry.Data {
|
||||
fmt.Fprintf(b, " %s=%v", k, v)
|
||||
}
|
||||
} else {
|
||||
// No padding when there's no fields
|
||||
fmt.Fprintf(b, "%s: %s %s", levelText, timeStamp, entry.Message)
|
||||
}
|
||||
|
||||
b.WriteByte('\n')
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
|
||||
var (
|
||||
Debug *log.Logger
|
||||
Info *log.Logger
|
||||
Warning *log.Logger
|
||||
Error *log.Logger
|
||||
standardTextFormatter = &textFormatter{}
|
||||
)
|
||||
|
||||
func InitLogging(debugHandle io.Writer,
|
||||
infoHandle io.Writer,
|
||||
warningHandle io.Writer,
|
||||
errorHandle io.Writer) {
|
||||
var (
|
||||
Log *logrus.Logger
|
||||
)
|
||||
|
||||
Debug = log.New(debugHandle, "DEBUG: ", standardLogFlags)
|
||||
Info = log.New(infoHandle, "INFO: ", standardLogFlags)
|
||||
Warning = log.New(warningHandle, "WARNING: ", standardLogFlags)
|
||||
Error = log.New(errorHandle, "ERROR: ", standardLogFlags)
|
||||
func init() {
|
||||
Log = logrus.New()
|
||||
Log.Formatter = standardTextFormatter
|
||||
}
|
||||
|
||||
func InitDefaultLogging(debug bool) {
|
||||
debugOut := ioutil.Discard
|
||||
if debug {
|
||||
debugOut = os.Stderr
|
||||
func SetLogLevel(levelname string) {
|
||||
level, err := logrus.ParseLevel(levelname)
|
||||
if err != nil {
|
||||
Log.Fatal(err)
|
||||
}
|
||||
Log.Level = level
|
||||
}
|
||||
|
||||
func CheckFatal(e error) {
|
||||
if e != nil {
|
||||
Log.Fatal(e)
|
||||
}
|
||||
}
|
||||
|
||||
func CheckWarn(e error) {
|
||||
if e != nil {
|
||||
Log.Warnln(e)
|
||||
}
|
||||
InitLogging(debugOut, os.Stdout, os.Stdout, os.Stderr)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package mflagext
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/docker/pkg/mflag"
|
||||
)
|
||||
|
||||
type listOpts struct {
|
||||
value *[]string
|
||||
hasBeenSet bool
|
||||
}
|
||||
|
||||
func ListVar(p *[]string, names []string, value []string, usage string) {
|
||||
*p = value
|
||||
mflag.Var(&listOpts{p, false}, names, usage)
|
||||
}
|
||||
|
||||
func (opts *listOpts) Set(value string) error {
|
||||
if opts.hasBeenSet {
|
||||
(*opts.value) = append((*opts.value), value)
|
||||
} else {
|
||||
(*opts.value) = []string{value}
|
||||
opts.hasBeenSet = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (opts *listOpts) String() string {
|
||||
return fmt.Sprintf("%v", []string(*opts.value))
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package odp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"syscall"
|
||||
|
||||
"github.com/weaveworks/go-odp/odp"
|
||||
)
|
||||
|
||||
// ODP admin functionality
|
||||
|
||||
func CreateDatapath(dpname string) (err error, supported bool) {
|
||||
dpif, err := odp.NewDpif()
|
||||
if err != nil {
|
||||
if odp.IsKernelLacksODPError(err) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return err, true
|
||||
}
|
||||
|
||||
defer dpif.Close()
|
||||
|
||||
dp, err := dpif.CreateDatapath(dpname)
|
||||
if err != nil && !odp.IsDatapathNameAlreadyExistsError(err) {
|
||||
return err, true
|
||||
}
|
||||
|
||||
// Pick an ephemeral port number to use in probing for vxlan
|
||||
// support.
|
||||
udpconn, err := net.ListenUDP("udp4", nil)
|
||||
if err != nil {
|
||||
return err, true
|
||||
}
|
||||
|
||||
// we leave the UDP socket open, so creating a vxlan vport on
|
||||
// the same port number should fail. But that's fine: It's
|
||||
// still sufficient to probe for support.
|
||||
portno := uint16(udpconn.LocalAddr().(*net.UDPAddr).Port)
|
||||
vpid, err := dp.CreateVport(odp.NewVxlanVportSpec(
|
||||
fmt.Sprintf("vxlan-%d", portno), portno))
|
||||
if nlerr, ok := err.(odp.NetlinkError); ok {
|
||||
if syscall.Errno(nlerr) == syscall.EAFNOSUPPORT {
|
||||
dp.Delete()
|
||||
return fmt.Errorf("kernel does not have Open vSwitch VXLAN support"), false
|
||||
}
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
dp.DeleteVport(vpid)
|
||||
}
|
||||
|
||||
udpconn.Close()
|
||||
return nil, true
|
||||
}
|
||||
|
||||
func DeleteDatapath(dpname string) error {
|
||||
dpif, err := odp.NewDpif()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer dpif.Close()
|
||||
|
||||
dp, err := dpif.LookupDatapath(dpname)
|
||||
if err != nil {
|
||||
if odp.IsNoSuchDatapathError(err) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return dp.Delete()
|
||||
}
|
||||
|
||||
func AddDatapathInterface(dpname string, ifname string) error {
|
||||
dpif, err := odp.NewDpif()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer dpif.Close()
|
||||
|
||||
dp, err := dpif.LookupDatapath(dpname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = dp.CreateVport(odp.NewNetdevVportSpec(ifname))
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// A subsystem/server/... that can be stopped or queried about the status with a signal
|
||||
type SignalReceiver interface {
|
||||
Stop() error
|
||||
}
|
||||
|
||||
func SignalHandlerLoop(ss ...SignalReceiver) {
|
||||
sigs := make(chan os.Signal, 1)
|
||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM)
|
||||
buf := make([]byte, 1<<20)
|
||||
for {
|
||||
switch <-sigs {
|
||||
case syscall.SIGINT, syscall.SIGTERM:
|
||||
Log.Infof("=== received SIGINT/SIGTERM ===\n*** exiting")
|
||||
for _, subsystem := range ss {
|
||||
subsystem.Stop()
|
||||
}
|
||||
return
|
||||
case syscall.SIGQUIT:
|
||||
stacklen := runtime.Stack(buf, true)
|
||||
Log.Infof("=== received SIGQUIT ===\n*** goroutine dump...\n%s\n*** end", buf[:stacklen])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"github.com/fsouza/go-dockerclient"
|
||||
. "github.com/weaveworks/weave/common"
|
||||
)
|
||||
|
||||
type ContainerObserver interface {
|
||||
ContainerDied(ident string) error
|
||||
}
|
||||
|
||||
func checkError(err error, apiPath string) {
|
||||
if err != nil {
|
||||
Error.Fatalf("[updater] Unable to connect to Docker API on %s: %s",
|
||||
apiPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
func Start(apiPath string, ob ContainerObserver) error {
|
||||
client, err := docker.NewClient(apiPath)
|
||||
checkError(err, apiPath)
|
||||
|
||||
env, err := client.Version()
|
||||
checkError(err, apiPath)
|
||||
|
||||
events := make(chan *docker.APIEvents)
|
||||
err = client.AddEventListener(events)
|
||||
checkError(err, apiPath)
|
||||
|
||||
Info.Printf("[updater] Using Docker API on %s: %v", apiPath, env)
|
||||
|
||||
go func() {
|
||||
for event := range events {
|
||||
handleEvent(ob, event, client)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleEvent(ob ContainerObserver, event *docker.APIEvents, client *docker.Client) error {
|
||||
switch event.Status {
|
||||
case "die":
|
||||
id := event.ID
|
||||
ob.ContainerDied(id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Assert test is true, panic otherwise
|
||||
func Assert(test bool) {
|
||||
if !test {
|
||||
panic("Assertion failure")
|
||||
}
|
||||
}
|
||||
|
||||
func ErrorMessages(errors []error) string {
|
||||
var result []string
|
||||
for _, err := range errors {
|
||||
result = append(result, err.Error())
|
||||
}
|
||||
return strings.Join(result, "\n")
|
||||
}
|
||||
+19
-19
@@ -34,7 +34,7 @@ Router:
|
||||
the known locations of peers as they're discovered and is informed
|
||||
when connections we have made die. It proactively tries to create
|
||||
connections to remote peers we're not connected to using a random
|
||||
binary exponential backoff to regulate the period between connection
|
||||
exponential backoff to regulate the period between connection
|
||||
attempts.
|
||||
|
||||
- Local Peer
|
||||
@@ -66,23 +66,23 @@ Router:
|
||||
simply responds to traffic received from the remote peer via TCP
|
||||
5. We register this connection with the local peer
|
||||
6a. If we initiated this connection then we now start sending fast
|
||||
heartbeats to the remote peer so that the remote peer can determine what
|
||||
address/port it should use to send UDP back to us. To do this, we
|
||||
spawn off two further threads which are the forwarder
|
||||
loops. These receive frames which are to be sent to the remote
|
||||
peer. One of them sends frames without the DF flag set. To do this
|
||||
it just sends the packets out of the UDP Listener socket. The
|
||||
other needs to send frames with the DF flag set and needs its own
|
||||
socket so that it can do PMTU discovery easily. To do this, it
|
||||
uses a Raw IP socket (IP has no ports, so there's no collision
|
||||
issue with the UDP Listener socket) and so it must add UDP headers
|
||||
itself.
|
||||
6b. If we did not initiate this connection then the UDP Listener
|
||||
heartbeats to the remote peer so that the remote peer can
|
||||
determine what address/port it should use to send UDP back to
|
||||
us. To do this, we spawn off a "forwarder" thread to send
|
||||
heartbeats, monitor incoming heartbeats, and some other auxiliary
|
||||
duties. It also consumes frames to be encapsulated and send via
|
||||
UDP from two channels, for DF and non-DF cases. In the non-DF
|
||||
case, it can just send the packets out of the UDP Listener
|
||||
socket. In the DF case, it needs its own socket so that it can do
|
||||
PMTU discovery easily. To do this, it uses a Raw IP socket (IP has
|
||||
no ports, so there's no collision issue with the UDP Listener
|
||||
socket) and so it must add UDP headers itself.
|
||||
6b. If we did not initiate this connection then the UDP Listener
|
||||
should start receiving fast heartbeats from the remote peer. From
|
||||
those it should be able to identify the local connection via the
|
||||
local peer. It will tell the local connection (communicating to
|
||||
the actor thread) about the UDP address of the remote peer. The
|
||||
local connection will then start its forwarder threads as
|
||||
local connection will then start its forwarder thread as
|
||||
described in 6a, and start sending fast heartbeats. We send to the
|
||||
remote peer via TCP a ConnectionEstablished message. The remote
|
||||
peer receives this (on the TCP receiver process), tells the
|
||||
@@ -160,7 +160,7 @@ if a node receives a packet that is bigger than the next MTU, it is
|
||||
required to drop the packet and send back an ICMP 3,4 packet which
|
||||
informs the sending node of the next hop MTU (RFC 1191). In theory,
|
||||
these ICMP packets should have all the reverse NAT and so forth
|
||||
applied, to make it all the way back to the sender. In practise this
|
||||
applied, to make it all the way back to the sender. In practice, this
|
||||
does often work, but in some cases all ICMP are sometimes blocked,
|
||||
often by idiots who don't understand that ICMP is really the error
|
||||
channel of all network traffic.
|
||||
@@ -187,16 +187,16 @@ If weave determines that fragmentation is reliable then when weave
|
||||
sniffs large packets (i.e. packets larger than the MTU and thus don't
|
||||
have DF set) then weave will encapsulate these packets as necessary
|
||||
and similarly send them out as-is, without DF set, trusting to the
|
||||
network to do all necessary fragementation and reassembly.
|
||||
network to do all necessary fragmentation and reassembly.
|
||||
|
||||
However, if weave determines that fragementation is not reliable
|
||||
However, if weave determines that fragmentation is not reliable
|
||||
between any two peers then it will manually fragment larger packets
|
||||
correctly according to the IP spec, and will then send them between
|
||||
weave peers with the DF flag set (i.e. the fragmentation will ensure
|
||||
that the encapsulatated traffic will not be greater than the PMTU
|
||||
that the encapsulated traffic will not be greater than the PMTU
|
||||
between weave peers). Because the fragmentation is done according to
|
||||
the IP spec, we don't need to do reassembly ourselves - on the
|
||||
receiving weave, we jus inject all the fragments and rely on the
|
||||
receiving weave, we just inject all the fragments and rely on the
|
||||
stack to do reassembly.
|
||||
|
||||
Sometimes PMTU discovery doesn't work as ICMP packets sometimes get
|
||||
|
||||
+2
-2
@@ -125,8 +125,8 @@ VM2> nc 192.168.122.2 6783
|
||||
If all is well, you should now be able to launch weave on VM1
|
||||
then on VM2:
|
||||
|
||||
VM1> weaver -iface veth6 -name vm1
|
||||
VM2> weaver -iface veth6 -name vm2 192.168.122.2
|
||||
VM1> weaver --iface veth6 --name vm1
|
||||
VM2> weaver --iface veth6 --name vm2 192.168.122.2
|
||||
|
||||
Once the connection is up and running, you should be able to connect
|
||||
from VM1/ns2 to VM2/ns2 and vice versa:
|
||||
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
# Overlay Method Selection
|
||||
|
||||
Weave automatically selects the best working overlay implementation
|
||||
when connecting to a new peer. It does this by initiating both
|
||||
forwarders in parallel, eventually settling on the most preferred
|
||||
established forwarder at which point the other forwarder is shut down,
|
||||
assuming it had not failed already. In the event that the remaining
|
||||
forwarder fails at a later time (e.g. because of a heartbeat timeout)
|
||||
the control plane TCP connection will recycle allowing the selection
|
||||
algorithm to run again. By default fast datapath is preferred over
|
||||
Sleeve.
|
||||
|
||||
# Local Bridging
|
||||
|
||||
Unlike the weave bridge netdev used by Sleeve, the OVS datapath has no
|
||||
inherent bridging capability. Consequently fastdp implements an
|
||||
ethernet bridge in addition to the vxlan overlay, maintaining its own
|
||||
forwarding database by learning MAC addresses and dispatching
|
||||
broadcast, unicast & multicast traffic accordingly.
|
||||
|
||||
# Short Peer IDs
|
||||
|
||||
The router relies on having access to the source and destination peer
|
||||
when deciding how to forward packets. When the Sleeve overlay is in
|
||||
use this information is conveyed directly within the encapsulation by
|
||||
including the names of the peers in question, a solution not
|
||||
accomodated directly by the vxlan wire format. Fortunately vxlan does
|
||||
have a twenty four bit segment ID field in the header we can use to
|
||||
encode this data - the challenge is to identify peers uniquely with a
|
||||
twelve bit identifier instead of the seventeen bytes used by Sleeve.
|
||||
In practice this is achieved by peers adopting a random 'short ID' and
|
||||
resolving ownership collisions of same via the existing gossip
|
||||
mechanism.
|
||||
|
||||
# Heartbeats via vxlan
|
||||
|
||||
Heartbeats are implemented by sending an encapsulated ethernet frame
|
||||
with source and destination MAC addresses of `00:00:00:00:00:00` and a
|
||||
payload consisting of the connection UID and total ethernet frame
|
||||
length. The vxlan vport miss handler on the receiving side detects the
|
||||
all-zero addresses and acknowledges the heartbeat via the TCP control
|
||||
channel after validating the connection UID and frame length against
|
||||
their expected values.
|
||||
|
||||
# PMTU Discovery
|
||||
|
||||
The length of the heartbeat ethernet frame is set to the MTU of the
|
||||
datapath interface. In the event the vxlan packet is dropped or
|
||||
truncated, the heartbeat will not be acknowledged; this lack of
|
||||
acknowledgement will cause the peers to fall back to the Sleeve
|
||||
overlay, which has a more sophisticated dynamic mechanism for coping
|
||||
with low path MTUs.
|
||||
|
||||
To avoid triggering this fallback in typical deployments, the datapath
|
||||
interface is statically configured with an MTU of 1410 bytes allowing
|
||||
it to work with most underlay network provider MTUs, including GCE at
|
||||
1460 bytes (the fifty byte difference accomodates the vxlan overhead).
|
||||
This value can be overriden by setting `WEAVE_MTU` at launch if
|
||||
necessary.
|
||||
|
||||
# Virtual Ports
|
||||
|
||||
There are three kinds of virtual port associated with the weave
|
||||
datapath:
|
||||
|
||||
* `internal` - one of these is created automatically, named after the
|
||||
datapath. It corresponds to the network interface of the same name
|
||||
that appears on the host when the datapath is created; it is the
|
||||
ingress/egress port for `weave expose`
|
||||
* `netdev` - one for each application container, corresponding to the
|
||||
host end of the veth pair
|
||||
* `vxlan` - one for each UDP port on which we are listening for vxlan
|
||||
encapsulated packets. Typically there is only one of these, but
|
||||
there may be more in a network in which peers do not all use the same
|
||||
port configuration
|
||||
|
||||
# Misses and Flow Creation
|
||||
|
||||
As mentioned above, the datapath has no inherent behaviour - any
|
||||
ingressing packet which does not match a flow is passed to a userspace
|
||||
miss handler. The miss handler is then responsible for a) instructing
|
||||
the datapath to take actions for the packet that caused the miss (for
|
||||
example by copying it to one or more ports) and optionally b)
|
||||
installing flow rules which will allow the datapath to act on similar
|
||||
packets in future without invoking the miss handler.
|
||||
|
||||
At a high level the fast datapath implementation can be viewed as a
|
||||
set of miss handlers that determine what actions the OVS datapath
|
||||
should take based on router topology information, together with some
|
||||
additional machinery that manages the expiry of resulting flows when
|
||||
that state changes.
|
||||
|
||||
When a miss handler is invoked, it has two pieces of context: a byte
|
||||
array containing the packet that triggered the miss, and a set of
|
||||
'flow keys' that have been extracted from the packet by the kernel
|
||||
module. The following flow keys are of interest to weave:
|
||||
|
||||
* `InPort` - the identifier of the ingress virtual port
|
||||
* `Ethernet` - the source and destination MAC addresses of the
|
||||
ethernet frame
|
||||
* `Tunnel` - tunnel identifier (see section on short peer IDs above),
|
||||
and source/dest IPv4 addresses. Only present for ingress via a vxlan
|
||||
vport
|
||||
|
||||
Crucially, the router must use this information alone to determine
|
||||
which actions to take - this allows the specification of a flow which
|
||||
matches these keys and instructs the kernel to take action
|
||||
automatically in future without further userspace involvement.
|
||||
|
||||
Two actions are of interest:
|
||||
|
||||
* `Output` - output the packet to the specified vport
|
||||
* `SetTunnel` - update the effective vxlan tunnel parameters. Only
|
||||
required prior to output from a vxlan vport
|
||||
|
||||
It is possible to have multiple actions in a flow, so the router can
|
||||
for example create a single rule that copies broadcast traffic to all
|
||||
'local' vports (save the ingress vport obviously) as well as to vxlan
|
||||
vports for onward peers as dictated by the routing topology.
|
||||
|
||||
Every flow specified by the router has the following characteristics:
|
||||
|
||||
* An `InPort` key matching the ingress vport
|
||||
* A `Tunnel` key if the ingress vport is of type vxlan
|
||||
* An `Ethernet` key matching the source and destination MAC
|
||||
* A list of `Output` (for internal and netdev vports) and
|
||||
`SetTunnel`+`Output` (for vxlan vports) actions
|
||||
|
||||
Under certain circumstances the miss handler will instruct the
|
||||
datapath to execute a set of actions for a packet without creating a
|
||||
corresponding flow:
|
||||
|
||||
* When broadcasting packets to discover the path to unknown
|
||||
destination MACs. This condition is transient in the overwhelming
|
||||
majority of cases and so the benefit of creating a flow is
|
||||
outweighed by the need to invalidate it once the MAC is learned
|
||||
* When the packet also needs to be forwarded to one or more peers via
|
||||
Sleeve. In this case the router needs to handle all subsequent
|
||||
matching packets as there is no OVS flow action to do it without
|
||||
userspace involvement
|
||||
|
||||
# Flow Invalidation
|
||||
|
||||
Once a flow has been created for a particular combination of keys, the
|
||||
miss handler will never again be invoked for matching packets. It is
|
||||
therefore extremely important that we detect events which invalidate
|
||||
existing flow actions:
|
||||
|
||||
* Addition of netdev vports to the datapath
|
||||
* Route invalidation (topology change)
|
||||
* Short peer ID collision
|
||||
|
||||
The response in each case is the same - delete all flows from the
|
||||
datapath, allowing them to be recreated taking into account the
|
||||
updated state.
|
||||
|
||||
In addition to these event based invalidations there is an expiry
|
||||
process that executes every five minutes. This process enumerates all
|
||||
flows in the datapath, removing any which have not been used since the
|
||||
last check; this cleans up:
|
||||
|
||||
* Flows referring to netdev vports which have been removed
|
||||
* Flows created by forwarders which have been stopped
|
||||
* Flows related to MAC addresses which have not communicated recently
|
||||
|
||||
Flows keyed on tunnel IPv4 address do not need to be cleared when a
|
||||
peer appears to change IP address due to NAT; this will cause a miss
|
||||
resulting in a new flow, and the old flow will expire naturally via
|
||||
the timer mechanism.
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 276 KiB After Width: | Height: | Size: 43 KiB |
+220
@@ -0,0 +1,220 @@
|
||||
See the [requirements](https://github.com/zettio/weave/wiki/IP-allocation-requirements).
|
||||
|
||||
At its highest level, the idea is that we start with a certain IP
|
||||
address space, known to all peers, and divide it up between the
|
||||
peers. This allows peers to allocate and free individual IPs locally
|
||||
until they run out.
|
||||
|
||||
We use a CRDT to represent shared knowledge about the space,
|
||||
transmitted over the Weave Gossip mechanism, together with
|
||||
point-to-point messages for one peer to request more space from
|
||||
another.
|
||||
|
||||
The allocator running at each peer also has an http interface which
|
||||
the container infrastructure (e.g. the Weave script, or a Docker
|
||||
plugin) uses to request IP addresses.
|
||||
|
||||

|
||||
|
||||
## Commands
|
||||
|
||||
The commands supported by the allocator via the http interface are:
|
||||
|
||||
- Allocate: request one IP address for a container in a subnet
|
||||
- Lookup: fetch the previously-allocated IP address for a container in a subnet
|
||||
- Free: return an IP address that is currently allocated
|
||||
- Claim: request a specific IP address for a container (e.g. because
|
||||
it is already using that address)
|
||||
|
||||
Each http request either specifies a subnet, or if no subnet is
|
||||
specified this is taken as a request to allocate in a pre-defined
|
||||
default subnet.
|
||||
|
||||
The allocator also watches via the Docker event mechanism: if a
|
||||
container dies then all IP addresses allocated to that container are
|
||||
freed.
|
||||
|
||||
## Definitions
|
||||
|
||||
1. Allocations. We use the word 'allocation' to refer to a specific
|
||||
IP address being assigned, e.g. so it can be given to a container.
|
||||
|
||||
2. Range. Most of the time, instead of dealing with individual IP
|
||||
addresses, we operate on them in contiguous groups, for which we
|
||||
use the word "range".
|
||||
|
||||
3. Ring. We consider the address space as a ring, so ranges wrap
|
||||
around from the highest address to the lowest address.
|
||||
|
||||
4. Peer. A Peer is a node on the Weave network. It can own zero or
|
||||
more ranges.
|
||||
|
||||
### The Allocation Process
|
||||
|
||||
When a peer owns some range(s), it can allocate freely from within
|
||||
those ranges to containers on the same machine. If it runs out of
|
||||
space in a subnet (all owned ranges are full), it will ask another
|
||||
peer for space:
|
||||
- it picks a peer to ask at random, weighted by the amount of space
|
||||
owned by each peer in the subnet
|
||||
- if the target peer decides to give up space, it unicasts a message
|
||||
back to the asker with the newly-updated ring.
|
||||
- if the target peer has no space, it unicasts a message back to the
|
||||
asker with its current copy of the ring, on the basis that the
|
||||
requestor must have acted on out-of-date information.
|
||||
- it will continue to ask for space until it receives some, or its
|
||||
copy of the ring tells it all peers are full in that subnet.
|
||||
|
||||
### Claiming an address
|
||||
|
||||
If a Weave process is restarted, in most cases it will hear from
|
||||
another peer which ranges it used to own, but it needs to know which
|
||||
individual IP addresses are assigned to containers in order to avoid
|
||||
giving the same address out in subsequent allocation requests. The
|
||||
weave script invokes the `claim` command in `populate_ipam` to do
|
||||
this.
|
||||
|
||||
When the Allocator is told to claim an address, there are four
|
||||
possibilities:
|
||||
- the address is outside of the space managed by this Allocator, in
|
||||
which case we ignore the request.
|
||||
- the address is owned by this peer, in which case we record the
|
||||
address as being assigned to a particular container and return
|
||||
success.
|
||||
- the address is owned by a different peer, in which case we return
|
||||
failure.
|
||||
- we have not yet heard of any address ranges being owned by anyone,
|
||||
in which case we wait until we do hear.
|
||||
|
||||
This approach fails if the peer does not hear from another peer about
|
||||
the ranges it used to own, e.g. if all peers in a network partition
|
||||
are restarted at once.
|
||||
|
||||
### The Ring CRDT
|
||||
|
||||
We use a Convergent Replicated Data Type - a CRDT - so that peers can
|
||||
make changes concurrently and communicate them without central
|
||||
coordination. To achieve this, we arrange that peers only make changes
|
||||
to the data structure in ranges that they own (except under
|
||||
administrator command - see later).
|
||||
|
||||
The data structure is a set of tokens, each containing the name of an
|
||||
owning peer. A peer can own many ranges. Each token is placed at the
|
||||
start address of a range, and the set is kept ordered so each range
|
||||
goes from one token to the next. Each range on the ring includes the
|
||||
start address but does not include the end address (which is the start
|
||||
of the next range). Ranges wrap, so the 'next' token after the last
|
||||
one is the first token.
|
||||
|
||||

|
||||
|
||||
In more detail:
|
||||
- Each token is a tuple {peer name, version}, placed
|
||||
at an IP address.
|
||||
- Peer names are taken from Weave: they are unique and survive across restarts.
|
||||
- The contents of a token can only be updated by the owning peer, and
|
||||
when this is done the version is incremented
|
||||
- The ring data structure is always gossiped in its entirety
|
||||
- The merge operation when a peer receives a ring via gossip is:
|
||||
- Tokens with unique addresses are just copied into the combined ring
|
||||
- For tokens at the same address, pick the one with the highest
|
||||
version number
|
||||
- The data gossiped about the ring also includes the amount of free
|
||||
space in each range: this is not essential but it improves the
|
||||
selection of which peer to ask for space.
|
||||
- When a peer is asked for space, there are four scenarios:
|
||||
1. It has an empty range; it can change the peer associated with
|
||||
the token at the beginning of the range and increment the version.
|
||||
2. It has a range which can be subdivided by a single token to form
|
||||
a free range. It inserts said token, owned by the peer requesting
|
||||
space.
|
||||
3. It has a 'hole' in the middle of a range; an empty range can be
|
||||
created by inserting two tokens, one at the beginning of the hole
|
||||
owned by the peer requesting the space, and one at the end of the
|
||||
hole owned by the requestee.
|
||||
4. It has no space.
|
||||
|
||||
## Initialisation
|
||||
|
||||
The previous sections describe how peers coordinate changes to the
|
||||
ring. But how is the initial state of the ring established? If a new
|
||||
peer joins a long-standing cluster, it can learn about the ring state
|
||||
from other peers. But in a freshly started cluster, the initial ring
|
||||
state must be established from a clean slate. And it must be
|
||||
consistent across all peers. This is a distributed consensus problem,
|
||||
and we solve it using the Paxos algorithm.
|
||||
|
||||
Although Paxos has a reputation for being hard to understand and to
|
||||
implement, the implementation used for ring initialization is
|
||||
relatively straightforward:
|
||||
|
||||
- We only need to establish consensus for a single value, rather than
|
||||
a succession of values (as in a replicated transaction log). Thus
|
||||
we implement basic Paxos, rather than multi-Paxos, and the
|
||||
implementation closely follows the outline of basic Paxos described
|
||||
[on
|
||||
wikipedia](http://en.wikipedia.org/wiki/Paxos_%28computer_science%29#Basic_Paxos).
|
||||
|
||||
- All peers play all the roles described in basic Paxos: Proposer,
|
||||
acceptor and listener.
|
||||
|
||||
- Paxos is usually described in terms of unicast messages from one
|
||||
agent to another, although three of the four messages are fanned out
|
||||
to all agents in a certain role ("prepare" and "accept request" from
|
||||
a proposer to all acceptors; "accepted" from an acceptor to all
|
||||
learners). So most implementations involve a communications layer
|
||||
to implement the required communication patterns. But our
|
||||
implementation is built on top of the weave gossip layer, which
|
||||
gives us a broadcast medium. Using broadcast for the one truly
|
||||
unicast message ("promise") may seem wasteful, but as we only
|
||||
establish consensus for a single value, it is not a major concern.
|
||||
|
||||
The value the peers obtain consensus on is the set of peers that will
|
||||
be represented in the initial ring (with each peer getting an equal
|
||||
share of the address space). When a proposing peer gets to choose the
|
||||
value, it includes all the peers it has heard from during the Paxos
|
||||
phase, which is at least a quorum.
|
||||
|
||||
Once a consensus is reached, it is used to initialize the ring. If a
|
||||
peer hears about an initialized ring via gossip, that implies that a
|
||||
consensus was reached, so it will stop participating in Paxos.
|
||||
|
||||
Paxos requires that we know the quorum size for the cluster. As weave
|
||||
clusters are a dynamic entity, there is no fixed quorum size. But the
|
||||
Paxos phase used to initialize the ring should normally be
|
||||
short-lived, so we just need to know the initial cluster size. A
|
||||
heuristic is used to derive this from the list of addresses passed to
|
||||
`weave launch`, but the user can set it explicitly.
|
||||
|
||||
## Peer shutdown
|
||||
|
||||
When a peer leaves (a `weave reset` command), it grants all its own
|
||||
tokens to another peer, then broadcasts the updated ring.
|
||||
|
||||
After sending the message, the peer terminates - it does not wait for
|
||||
any response.
|
||||
|
||||
Failures:
|
||||
- message lost
|
||||
- the space will be unusable by other peers because it will still be
|
||||
seen as owned.
|
||||
|
||||
To cope with the situation where a peer has left or died without
|
||||
managing to tell its peers, an administrator may go to any other peer
|
||||
and command that it take over the dead peer's tokens (with `weave
|
||||
rmpeer`). This information will then be gossipped out to the network.
|
||||
|
||||
(This operation is not guaranteed to be safe - if the dead peer made
|
||||
some transfers which have are known to other peers but have not
|
||||
reached the peer that does the `rmpeer`, then we get an inconsistent
|
||||
ring. We could make it safe, up to partitions, by inquiring of every
|
||||
live peer what its view of the dead peer is, and making sure we use
|
||||
the latest info.)
|
||||
|
||||
## Limitations
|
||||
|
||||
Nothing is persisted. If one peer restarts it should receive gossip
|
||||
from other peers showing what it used to own, and in that case it can
|
||||
recover quite well. If, however, there are no peers left alive, or
|
||||
this peer cannot establish network communication with those that are,
|
||||
then it cannot recover.
|
||||
@@ -0,0 +1,145 @@
|
||||
# Release Process
|
||||
## Prerequisites
|
||||
|
||||
* Up-to-date versions of `git`, `go` etc
|
||||
* Install GitHub release tool `go get github.com/weaveworks/github-release`
|
||||
* Create a [github token for
|
||||
github-release](https://help.github.com/articles/creating-an-access-token-for-command-line-use/);
|
||||
set and export `$GITHUB_TOKEN` with this value
|
||||
* Update all dependencies with `make update`
|
||||
|
||||
## Release Types
|
||||
|
||||
The release script behaves differently depending on the kind of
|
||||
release you are doing. There are three types:
|
||||
|
||||
* **Mainline** - a release (typically from master) with version tag
|
||||
`vX.Y.Z` where Z is zero (e.g. `v2.1.0`)
|
||||
* **Branch** - a bugfix release (typically from a branch) with version tag
|
||||
`vX.Y.Z` where Z is non-zero (e.g `v2.1.1`)
|
||||
* **Prerelease** - a release from an arbitrary branch with an arbitrary
|
||||
version tag (e.g. `feature-preview-20150904`)
|
||||
|
||||
N.B. the release script _infers the release type from the format of the
|
||||
version tag_. Ensure your tag is in the correct format and the desired
|
||||
behaviours for that type of release will be obtained from the script.
|
||||
|
||||
## Build Phase
|
||||
### Update CHANGELOG.md
|
||||
|
||||
* Checkout the branch from which you wish to release
|
||||
* Choose a version tag (see above) henceforth referred to as `$TAG`.
|
||||
* Add a changelog entry for the new tag at the top of `CHANGELOG.md`.
|
||||
The first line must be a markdown header of the form `## Release
|
||||
$TAG` for **Prerelease** builds, `## Release ${TAG#v}` otherwise.
|
||||
|
||||
Commit the changelog update:
|
||||
|
||||
git commit -m "Add release $TAG" CHANGELOG.md
|
||||
|
||||
### Create Version Tag
|
||||
|
||||
Next you must tag the changelog commit with `$TAG`
|
||||
|
||||
git tag -a -m "Release $TAG" $TAG
|
||||
|
||||
### Execute Build
|
||||
|
||||
You are now ready to perform the build. If you have skipped the
|
||||
previous steps (e.g. because you're doing a rebuild), you must ensure
|
||||
that `HEAD` points to the tagged commit. You may then execute
|
||||
|
||||
bin/release build
|
||||
|
||||
This has the following effects:
|
||||
|
||||
* `git tag --points-at HEAD` is used to determine `$TAG` (hence the
|
||||
`HEAD` requirement)
|
||||
* Your *local* repository is cloned into `releases/$TAG`
|
||||
* `CHANGELOG.md` is checked to ensure it has an entry for `$TAG`
|
||||
* Distributables injected with `$TAG` are built
|
||||
* Tests are executed
|
||||
|
||||
## Draft Phase
|
||||
### Push Version Tag Upstream
|
||||
|
||||
First you must push your version tag upstream, so that an associated
|
||||
GitHub release may be created:
|
||||
|
||||
git push git@github.com:weaveworks/weave $TAG
|
||||
|
||||
N.B. if you're testing the release process, push to your fork
|
||||
instead!
|
||||
|
||||
### Create Draft Release
|
||||
|
||||
You're now ready to draft your release notes:
|
||||
|
||||
bin/release draft
|
||||
|
||||
This has the following effects:
|
||||
|
||||
* A [release](https://help.github.com/articles/about-releases) is
|
||||
created in GitHub for `$TAG`. This release is in the draft state, so
|
||||
it is only visible to contributors; for **Prerelease** builds the
|
||||
pre-release attribute will also be set
|
||||
* The `weave` script is uploaded as an attachment to the release
|
||||
|
||||
Navigate to https://github.com/weaveworks/weave/releases, 'Edit' the
|
||||
draft and input the release notes. When you are done make sure you
|
||||
'Save draft' (and not 'Publish release'!).
|
||||
|
||||
Once the release notes have passed review, proceed to the publish
|
||||
phase.
|
||||
|
||||
## Publish Phase
|
||||
### Move/Force Push `latest_release` Tag
|
||||
|
||||
This step must only be performed for **Mainline** and **Branch**
|
||||
releases:
|
||||
|
||||
git tag -af -m "Release $TAG" latest_release $TAG
|
||||
git push -f git@github.com:weaveworks/weave latest_release
|
||||
|
||||
The `latest_release` tag *must* point at `$TAG`, *not* at `HEAD` -
|
||||
the build script will complain otherwise.
|
||||
|
||||
N.B. if you're testing the release process, push to your fork
|
||||
instead!
|
||||
|
||||
### Publish Release & Distributable Artefacts
|
||||
|
||||
You can now publish the release and upload the remaining
|
||||
distributables to DockerHub:
|
||||
|
||||
bin/release publish
|
||||
|
||||
The effects of this step depend on the inferred release type. The
|
||||
following occurs for all types:
|
||||
|
||||
* Docker images are tagged `$TAG` and pushed to DockerHub
|
||||
* GitHub release moves from draft to published state
|
||||
|
||||
Additionally, for **Mainline** and **Branch** types:
|
||||
|
||||
* Release named `latest_release` is updated on GitHub
|
||||
|
||||
Finally, for **Mainline** releases only:
|
||||
|
||||
* Images tagged `latest` are updated on DockerHub
|
||||
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
There's a few things that can go wrong.
|
||||
|
||||
* If the build is wonky, e.g., the tests don't pass, you can delete
|
||||
the directory in `./releases/`, fix whatever it is, move the
|
||||
version tag (which should still be only local) and have another go.
|
||||
* If the DockerHub pushes fail (which sadly seems to happen a lot),
|
||||
you can just run `./bin/release publish` again.
|
||||
* If you need to overwrite a release you can do so by manually
|
||||
deleting the GitHub version release and re-running the process.
|
||||
Please note that the DockerHub `latest` images, GitHub
|
||||
`latest_release` and download links may be in an inconsistent state
|
||||
until the overwrite is completed.
|
||||
@@ -0,0 +1,72 @@
|
||||
package ipam
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/weaveworks/weave/net/address"
|
||||
)
|
||||
|
||||
type allocateResult struct {
|
||||
addr address.Address
|
||||
err error
|
||||
}
|
||||
|
||||
type allocate struct {
|
||||
resultChan chan<- allocateResult
|
||||
ident string
|
||||
r address.Range // Range we are trying to allocate within
|
||||
hasBeenCancelled func() bool
|
||||
}
|
||||
|
||||
// Try returns true if the request is completed, false if pending
|
||||
func (g *allocate) Try(alloc *Allocator) bool {
|
||||
if g.hasBeenCancelled() {
|
||||
g.Cancel()
|
||||
return true
|
||||
}
|
||||
|
||||
if addr, found := alloc.lookupOwned(g.ident, g.r); found {
|
||||
g.resultChan <- allocateResult{addr, nil}
|
||||
return true
|
||||
}
|
||||
|
||||
if !alloc.universe.Overlaps(g.r) {
|
||||
g.resultChan <- allocateResult{0, fmt.Errorf("range %s out of bounds: %s", g.r, alloc.universe)}
|
||||
return true
|
||||
}
|
||||
|
||||
alloc.establishRing()
|
||||
|
||||
if ok, addr := alloc.space.Allocate(g.r); ok {
|
||||
// If caller hasn't supplied a unique ID, file it under the IP address
|
||||
// which lets the caller then release the address using DELETE /ip/address
|
||||
if g.ident == "_" {
|
||||
g.ident = addr.String()
|
||||
}
|
||||
alloc.debugln("Allocated", addr, "for", g.ident, "in", g.r)
|
||||
alloc.addOwned(g.ident, addr)
|
||||
g.resultChan <- allocateResult{addr, nil}
|
||||
return true
|
||||
}
|
||||
|
||||
// out of space
|
||||
donors := alloc.ring.ChoosePeersToAskForSpace(g.r.Start, g.r.End)
|
||||
for _, donor := range donors {
|
||||
if err := alloc.sendSpaceRequest(donor, g.r); err != nil {
|
||||
alloc.debugln("Problem asking peer", donor, "for space:", err)
|
||||
} else {
|
||||
alloc.debugln("Decided to ask peer", donor, "for space in range", g.r)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (g *allocate) Cancel() {
|
||||
g.resultChan <- allocateResult{0, &errorCancelled{"Allocate", g.ident}}
|
||||
}
|
||||
|
||||
func (g *allocate) ForContainer(ident string) bool {
|
||||
return g.ident == ident
|
||||
}
|
||||
@@ -0,0 +1,778 @@
|
||||
package ipam
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/weaveworks/weave/common"
|
||||
"github.com/weaveworks/weave/ipam/paxos"
|
||||
"github.com/weaveworks/weave/ipam/ring"
|
||||
"github.com/weaveworks/weave/ipam/space"
|
||||
"github.com/weaveworks/weave/mesh"
|
||||
"github.com/weaveworks/weave/net/address"
|
||||
)
|
||||
|
||||
// Kinds of message we can unicast to other peers
|
||||
const (
|
||||
msgSpaceRequest = iota
|
||||
msgRingUpdate
|
||||
msgSpaceRequestDenied
|
||||
|
||||
paxosInterval = time.Second * 5
|
||||
MinSubnetSize = 4 // first and last addresses are excluded, so 2 would be too small
|
||||
)
|
||||
|
||||
// operation represents something which Allocator wants to do, but
|
||||
// which may need to wait until some other message arrives.
|
||||
type operation interface {
|
||||
// Try attempts this operations and returns false if needs to be tried again.
|
||||
Try(alloc *Allocator) bool
|
||||
|
||||
Cancel()
|
||||
|
||||
// Does this operation pertain to the given container id?
|
||||
// Used for tidying up pending operations when containers die.
|
||||
ForContainer(ident string) bool
|
||||
}
|
||||
|
||||
// Allocator brings together Ring and space.Set, and does the
|
||||
// necessary plumbing. Runs as a single-threaded Actor, so no locks
|
||||
// are used around data structures.
|
||||
type Allocator struct {
|
||||
actionChan chan<- func()
|
||||
ourName mesh.PeerName
|
||||
universe address.Range // superset of all ranges
|
||||
ring *ring.Ring // information on ranges owned by all peers
|
||||
space space.Space // more detail on ranges owned by us
|
||||
owned map[string][]address.Address // who owns what addresses, indexed by container-ID
|
||||
nicknames map[mesh.PeerName]string // so we can map nicknames for rmpeer
|
||||
pendingAllocates []operation // held until we get some free space
|
||||
pendingClaims []operation // held until we know who owns the space
|
||||
gossip mesh.Gossip // our link to the outside world for sending messages
|
||||
paxos *paxos.Node
|
||||
paxosTicker *time.Ticker
|
||||
shuttingDown bool // to avoid doing any requests while trying to shut down
|
||||
isKnownPeer func(mesh.PeerName) bool
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
// NewAllocator creates and initialises a new Allocator
|
||||
func NewAllocator(ourName mesh.PeerName, ourUID mesh.PeerUID, ourNickname string, universe address.Range, quorum uint, isKnownPeer func(name mesh.PeerName) bool) *Allocator {
|
||||
return &Allocator{
|
||||
ourName: ourName,
|
||||
universe: universe,
|
||||
ring: ring.New(universe.Start, universe.End, ourName),
|
||||
owned: make(map[string][]address.Address),
|
||||
paxos: paxos.NewNode(ourName, ourUID, quorum),
|
||||
nicknames: map[mesh.PeerName]string{ourName: ourNickname},
|
||||
isKnownPeer: isKnownPeer,
|
||||
now: time.Now,
|
||||
}
|
||||
}
|
||||
|
||||
// Start runs the allocator goroutine
|
||||
func (alloc *Allocator) Start() {
|
||||
actionChan := make(chan func(), mesh.ChannelSize)
|
||||
alloc.actionChan = actionChan
|
||||
go alloc.actorLoop(actionChan)
|
||||
}
|
||||
|
||||
// Stop makes the actor routine exit, for test purposes ONLY because any
|
||||
// calls after this is processed will hang. Async.
|
||||
func (alloc *Allocator) Stop() {
|
||||
alloc.actionChan <- nil
|
||||
}
|
||||
|
||||
// Operation life cycle
|
||||
|
||||
// Given an operation, try it, and add it to the pending queue if it didn't succeed
|
||||
func (alloc *Allocator) doOperation(op operation, ops *[]operation) {
|
||||
alloc.actionChan <- func() {
|
||||
if alloc.shuttingDown {
|
||||
op.Cancel()
|
||||
return
|
||||
}
|
||||
if !op.Try(alloc) {
|
||||
*ops = append(*ops, op)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Given an operation, remove it from the pending queue
|
||||
// Note the op may not be on the queue; it may have
|
||||
// already succeeded. If it is on the queue, we call
|
||||
// cancel on it, allowing callers waiting for the resultChans
|
||||
// to unblock.
|
||||
func (alloc *Allocator) cancelOp(op operation, ops *[]operation) {
|
||||
for i, op := range *ops {
|
||||
if op == op {
|
||||
*ops = append((*ops)[:i], (*ops)[i+1:]...)
|
||||
op.Cancel()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cancel all operations in a queue
|
||||
func (alloc *Allocator) cancelOps(ops *[]operation) {
|
||||
for _, op := range *ops {
|
||||
op.Cancel()
|
||||
}
|
||||
*ops = []operation{}
|
||||
}
|
||||
|
||||
// Cancel all operations for a given container id, returns true
|
||||
// if we found any.
|
||||
func (alloc *Allocator) cancelOpsFor(ops *[]operation, ident string) bool {
|
||||
var found bool
|
||||
for i := 0; i < len(*ops); {
|
||||
if op := (*ops)[i]; op.ForContainer(ident) {
|
||||
found = true
|
||||
op.Cancel()
|
||||
*ops = append((*ops)[:i], (*ops)[i+1:]...)
|
||||
} else {
|
||||
i++
|
||||
}
|
||||
}
|
||||
return found
|
||||
}
|
||||
|
||||
// Try all pending operations
|
||||
func (alloc *Allocator) tryPendingOps() {
|
||||
// The slightly different semantics requires us to operate on 'claims' and
|
||||
// 'allocates' separately:
|
||||
// Claims must be tried before Allocates
|
||||
for i := 0; i < len(alloc.pendingClaims); {
|
||||
op := alloc.pendingClaims[i]
|
||||
if !op.Try(alloc) {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
alloc.pendingClaims = append(alloc.pendingClaims[:i], alloc.pendingClaims[i+1:]...)
|
||||
}
|
||||
|
||||
// When the first Allocate fails, bail - no need to
|
||||
// send too many begs for space.
|
||||
for i := 0; i < len(alloc.pendingAllocates); {
|
||||
op := alloc.pendingAllocates[i]
|
||||
if !op.Try(alloc) {
|
||||
break
|
||||
}
|
||||
alloc.pendingAllocates = append(alloc.pendingAllocates[:i], alloc.pendingAllocates[i+1:]...)
|
||||
}
|
||||
}
|
||||
|
||||
func (alloc *Allocator) spaceRequestDenied(sender mesh.PeerName, r address.Range) {
|
||||
for i := 0; i < len(alloc.pendingClaims); {
|
||||
claim := alloc.pendingClaims[i].(*claim)
|
||||
if r.Contains(claim.addr) {
|
||||
claim.deniedBy(alloc, sender)
|
||||
alloc.pendingClaims = append(alloc.pendingClaims[:i], alloc.pendingClaims[i+1:]...)
|
||||
continue
|
||||
}
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
type errorCancelled struct {
|
||||
kind string
|
||||
ident string
|
||||
}
|
||||
|
||||
func (e *errorCancelled) Error() string {
|
||||
return fmt.Sprintf("%s request for %s cancelled", e.kind, e.ident)
|
||||
}
|
||||
|
||||
// Actor client API
|
||||
|
||||
// Allocate (Sync) - get new IP address for container with given name in range
|
||||
// if there isn't any space in that range we block indefinitely
|
||||
func (alloc *Allocator) Allocate(ident string, r address.Range, hasBeenCancelled func() bool) (address.Address, error) {
|
||||
resultChan := make(chan allocateResult)
|
||||
op := &allocate{resultChan: resultChan, ident: ident, r: r, hasBeenCancelled: hasBeenCancelled}
|
||||
alloc.doOperation(op, &alloc.pendingAllocates)
|
||||
result := <-resultChan
|
||||
return result.addr, result.err
|
||||
}
|
||||
|
||||
// Lookup (Sync) - get existing IP address for container with given name in range
|
||||
func (alloc *Allocator) Lookup(ident string, r address.Range) (address.Address, error) {
|
||||
resultChan := make(chan allocateResult)
|
||||
alloc.actionChan <- func() {
|
||||
if addr, found := alloc.lookupOwned(ident, r); found {
|
||||
resultChan <- allocateResult{addr: addr}
|
||||
return
|
||||
}
|
||||
resultChan <- allocateResult{err: fmt.Errorf("lookup: no address found for %s in range %s", ident, r)}
|
||||
}
|
||||
result := <-resultChan
|
||||
return result.addr, result.err
|
||||
}
|
||||
|
||||
// Claim an address that we think we should own (Sync)
|
||||
func (alloc *Allocator) Claim(ident string, addr address.Address, noErrorOnUnknown bool) error {
|
||||
resultChan := make(chan error)
|
||||
op := &claim{resultChan: resultChan, ident: ident, addr: addr, noErrorOnUnknown: noErrorOnUnknown}
|
||||
alloc.doOperation(op, &alloc.pendingClaims)
|
||||
return <-resultChan
|
||||
}
|
||||
|
||||
// ContainerDied is provided to satisfy the updater interface; does a 'Delete' underneath. Sync.
|
||||
func (alloc *Allocator) ContainerDied(ident string) {
|
||||
if err := alloc.Delete(ident); err == nil {
|
||||
alloc.debugln("Container", ident, "died; released addresses")
|
||||
}
|
||||
}
|
||||
|
||||
func (alloc *Allocator) ContainerStarted(ident string) {}
|
||||
|
||||
// Delete (Sync) - release all IP addresses for container with given name
|
||||
func (alloc *Allocator) Delete(ident string) error {
|
||||
errChan := make(chan error)
|
||||
alloc.actionChan <- func() {
|
||||
addrs, found := alloc.owned[ident]
|
||||
for _, addr := range addrs {
|
||||
alloc.space.Free(addr)
|
||||
}
|
||||
delete(alloc.owned, ident)
|
||||
|
||||
// Also remove any pending ops
|
||||
found = alloc.cancelOpsFor(&alloc.pendingAllocates, ident) || found
|
||||
found = alloc.cancelOpsFor(&alloc.pendingClaims, ident) || found
|
||||
|
||||
if !found {
|
||||
errChan <- fmt.Errorf("Delete: no addresses for %s", ident)
|
||||
return
|
||||
}
|
||||
errChan <- nil
|
||||
}
|
||||
return <-errChan
|
||||
}
|
||||
|
||||
// Free (Sync) - release single IP address for container
|
||||
func (alloc *Allocator) Free(ident string, addrToFree address.Address) error {
|
||||
errChan := make(chan error)
|
||||
alloc.actionChan <- func() {
|
||||
addrs := alloc.owned[ident]
|
||||
for i, ownedAddr := range addrs {
|
||||
if ownedAddr == addrToFree {
|
||||
alloc.debugln("Freed", addrToFree, "for", ident)
|
||||
if len(addrs) == 1 {
|
||||
delete(alloc.owned, ident)
|
||||
} else {
|
||||
alloc.owned[ident] = append(addrs[:i], addrs[i+1:]...)
|
||||
}
|
||||
alloc.space.Free(addrToFree)
|
||||
errChan <- nil
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
errChan <- fmt.Errorf("Free: address %s not found for %s", addrToFree, ident)
|
||||
}
|
||||
return <-errChan
|
||||
}
|
||||
|
||||
func (alloc *Allocator) pickPeerFromNicknames(isValid func(mesh.PeerName) bool) mesh.PeerName {
|
||||
for name := range alloc.nicknames {
|
||||
if name != alloc.ourName && isValid(name) {
|
||||
return name
|
||||
}
|
||||
}
|
||||
return mesh.UnknownPeerName
|
||||
}
|
||||
|
||||
func (alloc *Allocator) pickPeerForTransfer() mesh.PeerName {
|
||||
// first try alive peers that actively participate in IPAM (i.e. have entries)
|
||||
if heir := alloc.ring.PickPeerForTransfer(alloc.isKnownPeer); heir != mesh.UnknownPeerName {
|
||||
return heir
|
||||
}
|
||||
// next try alive peers that have IPAM enabled but have no entries
|
||||
if heir := alloc.pickPeerFromNicknames(alloc.isKnownPeer); heir != mesh.UnknownPeerName {
|
||||
return heir
|
||||
}
|
||||
// next try disappeared peers that still have entries
|
||||
t := func(mesh.PeerName) bool { return true }
|
||||
if heir := alloc.ring.PickPeerForTransfer(t); heir != mesh.UnknownPeerName {
|
||||
return heir
|
||||
}
|
||||
// finally, disappeared peers that that passively participated in IPAM
|
||||
return alloc.pickPeerFromNicknames(t)
|
||||
}
|
||||
|
||||
// Shutdown (Sync)
|
||||
func (alloc *Allocator) Shutdown() {
|
||||
alloc.infof("Shutdown")
|
||||
doneChan := make(chan struct{})
|
||||
alloc.actionChan <- func() {
|
||||
alloc.shuttingDown = true
|
||||
alloc.cancelOps(&alloc.pendingClaims)
|
||||
alloc.cancelOps(&alloc.pendingAllocates)
|
||||
if heir := alloc.pickPeerForTransfer(); heir != mesh.UnknownPeerName {
|
||||
alloc.ring.Transfer(alloc.ourName, heir)
|
||||
alloc.space.Clear()
|
||||
alloc.gossip.GossipBroadcast(alloc.Gossip())
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
doneChan <- struct{}{}
|
||||
}
|
||||
<-doneChan
|
||||
}
|
||||
|
||||
// AdminTakeoverRanges (Sync) - take over the ranges owned by a given peer.
|
||||
// Only done on adminstrator command.
|
||||
func (alloc *Allocator) AdminTakeoverRanges(peerNameOrNickname string) error {
|
||||
resultChan := make(chan error)
|
||||
alloc.actionChan <- func() {
|
||||
peername, err := alloc.lookupPeername(peerNameOrNickname)
|
||||
if err != nil {
|
||||
resultChan <- fmt.Errorf("Cannot find peer '%s'", peerNameOrNickname)
|
||||
return
|
||||
}
|
||||
|
||||
alloc.debugln("AdminTakeoverRanges:", peername)
|
||||
if peername == alloc.ourName {
|
||||
resultChan <- fmt.Errorf("Cannot take over ranges from yourself!")
|
||||
return
|
||||
}
|
||||
|
||||
newRanges, err := alloc.ring.Transfer(peername, alloc.ourName)
|
||||
alloc.space.AddRanges(newRanges)
|
||||
resultChan <- err
|
||||
}
|
||||
return <-resultChan
|
||||
}
|
||||
|
||||
// Lookup a PeerName by nickname or stringified PeerName. We can't
|
||||
// call into the router for this because we are interested in peers
|
||||
// that have gone away but are still in the ring, which is why we
|
||||
// maintain our own nicknames map.
|
||||
func (alloc *Allocator) lookupPeername(name string) (mesh.PeerName, error) {
|
||||
for peername, nickname := range alloc.nicknames {
|
||||
if nickname == name {
|
||||
return peername, nil
|
||||
}
|
||||
}
|
||||
|
||||
return mesh.PeerNameFromString(name)
|
||||
}
|
||||
|
||||
// Restrict the peers in "nicknames" to those in the ring plus peers known to the router
|
||||
func (alloc *Allocator) pruneNicknames() {
|
||||
ringPeers := alloc.ring.PeerNames()
|
||||
for name := range alloc.nicknames {
|
||||
if _, ok := ringPeers[name]; !ok && !alloc.isKnownPeer(name) {
|
||||
delete(alloc.nicknames, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (alloc *Allocator) annotatePeernames(names []mesh.PeerName) []string {
|
||||
var res []string
|
||||
for _, name := range names {
|
||||
if nickname, found := alloc.nicknames[name]; found {
|
||||
res = append(res, fmt.Sprint(name, "(", nickname, ")"))
|
||||
} else {
|
||||
res = append(res, name.String())
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func decodeRange(msg []byte) (r address.Range, err error) {
|
||||
decoder := gob.NewDecoder(bytes.NewReader(msg))
|
||||
return r, decoder.Decode(&r)
|
||||
}
|
||||
|
||||
// OnGossipUnicast (Sync)
|
||||
func (alloc *Allocator) OnGossipUnicast(sender mesh.PeerName, msg []byte) error {
|
||||
alloc.debugln("OnGossipUnicast from", sender, ": ", len(msg), "bytes")
|
||||
resultChan := make(chan error)
|
||||
alloc.actionChan <- func() {
|
||||
switch msg[0] {
|
||||
case msgSpaceRequest:
|
||||
// some other peer asked us for space
|
||||
r, err := decodeRange(msg[1:])
|
||||
if err == nil {
|
||||
alloc.donateSpace(r, sender)
|
||||
}
|
||||
resultChan <- err
|
||||
case msgSpaceRequestDenied:
|
||||
r, err := decodeRange(msg[1:])
|
||||
if err == nil {
|
||||
alloc.spaceRequestDenied(sender, r)
|
||||
}
|
||||
resultChan <- err
|
||||
case msgRingUpdate:
|
||||
resultChan <- alloc.update(sender, msg[1:])
|
||||
}
|
||||
}
|
||||
return <-resultChan
|
||||
}
|
||||
|
||||
// OnGossipBroadcast (Sync)
|
||||
func (alloc *Allocator) OnGossipBroadcast(sender mesh.PeerName, msg []byte) (mesh.GossipData, error) {
|
||||
alloc.debugln("OnGossipBroadcast from", sender, ":", len(msg), "bytes")
|
||||
resultChan := make(chan error)
|
||||
alloc.actionChan <- func() {
|
||||
resultChan <- alloc.update(sender, msg)
|
||||
}
|
||||
return alloc.Gossip(), <-resultChan
|
||||
}
|
||||
|
||||
type gossipState struct {
|
||||
// We send a timstamp along with the information to be
|
||||
// gossipped in order to detect skewed clocks
|
||||
Now int64
|
||||
Nicknames map[mesh.PeerName]string
|
||||
|
||||
Paxos paxos.GossipState
|
||||
Ring *ring.Ring
|
||||
}
|
||||
|
||||
func (alloc *Allocator) encode() []byte {
|
||||
data := gossipState{
|
||||
Now: alloc.now().Unix(),
|
||||
Nicknames: alloc.nicknames,
|
||||
}
|
||||
|
||||
// We're only interested in Paxos until we have a Ring.
|
||||
if alloc.ring.Empty() {
|
||||
data.Paxos = alloc.paxos.GossipState()
|
||||
} else {
|
||||
data.Ring = alloc.ring
|
||||
}
|
||||
buf := new(bytes.Buffer)
|
||||
enc := gob.NewEncoder(buf)
|
||||
if err := enc.Encode(data); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// Encode (Sync)
|
||||
func (alloc *Allocator) Encode() []byte {
|
||||
resultChan := make(chan []byte)
|
||||
alloc.actionChan <- func() {
|
||||
resultChan <- alloc.encode()
|
||||
}
|
||||
return <-resultChan
|
||||
}
|
||||
|
||||
// OnGossip (Sync)
|
||||
func (alloc *Allocator) OnGossip(msg []byte) (mesh.GossipData, error) {
|
||||
alloc.debugln("Allocator.OnGossip:", len(msg), "bytes")
|
||||
resultChan := make(chan error)
|
||||
alloc.actionChan <- func() {
|
||||
resultChan <- alloc.update(mesh.UnknownPeerName, msg)
|
||||
}
|
||||
return nil, <-resultChan // for now, we never propagate updates. TBD
|
||||
}
|
||||
|
||||
// GossipData implementation is trivial - we always gossip the latest
|
||||
// data we have at time of sending
|
||||
type ipamGossipData struct {
|
||||
alloc *Allocator
|
||||
}
|
||||
|
||||
func (d *ipamGossipData) Merge(other mesh.GossipData) mesh.GossipData {
|
||||
return d // no-op
|
||||
}
|
||||
|
||||
func (d *ipamGossipData) Encode() [][]byte {
|
||||
return [][]byte{d.alloc.Encode()}
|
||||
}
|
||||
|
||||
// Gossip returns a GossipData implementation, which in this case always
|
||||
// returns the latest ring state (and does nothing on merge)
|
||||
func (alloc *Allocator) Gossip() mesh.GossipData {
|
||||
return &ipamGossipData{alloc}
|
||||
}
|
||||
|
||||
// SetInterfaces gives the allocator two interfaces for talking to the outside world
|
||||
func (alloc *Allocator) SetInterfaces(gossip mesh.Gossip) {
|
||||
alloc.gossip = gossip
|
||||
}
|
||||
|
||||
// ACTOR server
|
||||
|
||||
func (alloc *Allocator) actorLoop(actionChan <-chan func()) {
|
||||
for {
|
||||
var tickChan <-chan time.Time
|
||||
if alloc.paxosTicker != nil {
|
||||
tickChan = alloc.paxosTicker.C
|
||||
}
|
||||
|
||||
select {
|
||||
case action := <-actionChan:
|
||||
if action == nil {
|
||||
return
|
||||
}
|
||||
action()
|
||||
case <-tickChan:
|
||||
alloc.propose()
|
||||
}
|
||||
|
||||
alloc.assertInvariants()
|
||||
alloc.reportFreeSpace()
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
// Ensure we are making progress towards an established ring
|
||||
func (alloc *Allocator) establishRing() {
|
||||
if !alloc.ring.Empty() || alloc.paxosTicker != nil {
|
||||
return
|
||||
}
|
||||
|
||||
alloc.propose()
|
||||
if ok, cons := alloc.paxos.Consensus(); ok {
|
||||
// If the quorum was 1, then proposing immediately
|
||||
// leads to consensus
|
||||
alloc.createRing(cons.Value)
|
||||
} else {
|
||||
// re-try until we get consensus
|
||||
alloc.paxosTicker = time.NewTicker(paxosInterval)
|
||||
}
|
||||
}
|
||||
|
||||
func (alloc *Allocator) createRing(peers []mesh.PeerName) {
|
||||
alloc.debugln("Paxos consensus:", peers)
|
||||
alloc.ring.ClaimForPeers(normalizeConsensus(peers))
|
||||
alloc.gossip.GossipBroadcast(alloc.Gossip())
|
||||
alloc.ringUpdated()
|
||||
}
|
||||
|
||||
func (alloc *Allocator) ringUpdated() {
|
||||
// When we have a ring, we don't need paxos any more
|
||||
if alloc.paxos != nil {
|
||||
alloc.paxos = nil
|
||||
|
||||
if alloc.paxosTicker != nil {
|
||||
alloc.paxosTicker.Stop()
|
||||
alloc.paxosTicker = nil
|
||||
}
|
||||
}
|
||||
|
||||
alloc.space.UpdateRanges(alloc.ring.OwnedRanges())
|
||||
alloc.tryPendingOps()
|
||||
}
|
||||
|
||||
// For compatibility with sort.Interface
|
||||
type peerNames []mesh.PeerName
|
||||
|
||||
func (a peerNames) Len() int { return len(a) }
|
||||
func (a peerNames) Less(i, j int) bool { return a[i] < a[j] }
|
||||
func (a peerNames) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
|
||||
// When we get a consensus from Paxos, the peer names are not in a
|
||||
// defined order and may contain duplicates. This function sorts them
|
||||
// and de-dupes.
|
||||
func normalizeConsensus(consensus []mesh.PeerName) []mesh.PeerName {
|
||||
if len(consensus) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
peers := make(peerNames, len(consensus))
|
||||
copy(peers, consensus)
|
||||
sort.Sort(peers)
|
||||
|
||||
dst := 0
|
||||
for src := 1; src < len(peers); src++ {
|
||||
if peers[dst] != peers[src] {
|
||||
dst++
|
||||
peers[dst] = peers[src]
|
||||
}
|
||||
}
|
||||
|
||||
return peers[:dst+1]
|
||||
}
|
||||
|
||||
func (alloc *Allocator) propose() {
|
||||
alloc.debugf("Paxos proposing")
|
||||
alloc.paxos.Propose()
|
||||
alloc.gossip.GossipBroadcast(alloc.Gossip())
|
||||
}
|
||||
|
||||
func encodeRange(r address.Range) []byte {
|
||||
buf := new(bytes.Buffer)
|
||||
enc := gob.NewEncoder(buf)
|
||||
if err := enc.Encode(r); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
func (alloc *Allocator) sendSpaceRequest(dest mesh.PeerName, r address.Range) error {
|
||||
msg := append([]byte{msgSpaceRequest}, encodeRange(r)...)
|
||||
return alloc.gossip.GossipUnicast(dest, msg)
|
||||
}
|
||||
|
||||
func (alloc *Allocator) sendSpaceRequestDenied(dest mesh.PeerName, r address.Range) error {
|
||||
msg := append([]byte{msgSpaceRequestDenied}, encodeRange(r)...)
|
||||
return alloc.gossip.GossipUnicast(dest, msg)
|
||||
}
|
||||
|
||||
func (alloc *Allocator) sendRingUpdate(dest mesh.PeerName) {
|
||||
msg := append([]byte{msgRingUpdate}, alloc.encode()...)
|
||||
alloc.gossip.GossipUnicast(dest, msg)
|
||||
}
|
||||
|
||||
func (alloc *Allocator) update(sender mesh.PeerName, msg []byte) error {
|
||||
reader := bytes.NewReader(msg)
|
||||
decoder := gob.NewDecoder(reader)
|
||||
var data gossipState
|
||||
var err error
|
||||
|
||||
if err := decoder.Decode(&data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
deltat := time.Unix(data.Now, 0).Sub(alloc.now())
|
||||
if deltat > time.Hour || -deltat > time.Hour {
|
||||
return fmt.Errorf("clock skew of %v detected, ignoring update", deltat)
|
||||
}
|
||||
|
||||
// Merge nicknames
|
||||
for peer, nickname := range data.Nicknames {
|
||||
alloc.nicknames[peer] = nickname
|
||||
}
|
||||
|
||||
// only one of Ring and Paxos should be present. And we
|
||||
// shouldn't get updates for a empty Ring. But tolerate
|
||||
// them just in case.
|
||||
if data.Ring != nil {
|
||||
switch err = alloc.ring.Merge(*data.Ring); err {
|
||||
case ring.ErrDifferentSeeds:
|
||||
return fmt.Errorf("IP allocation was seeded by different peers (received: %v, ours: %v)",
|
||||
alloc.annotatePeernames(data.Ring.Seeds), alloc.annotatePeernames(alloc.ring.Seeds))
|
||||
case ring.ErrDifferentRange:
|
||||
return fmt.Errorf("Incompatible IP allocation ranges (received: %s, ours: %s)",
|
||||
data.Ring.Range().AsCIDRString(), alloc.ring.Range().AsCIDRString())
|
||||
default:
|
||||
if err == nil && !alloc.ring.Empty() {
|
||||
alloc.pruneNicknames()
|
||||
alloc.ringUpdated()
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if data.Paxos != nil {
|
||||
if alloc.ring.Empty() {
|
||||
if alloc.paxos.Update(data.Paxos) {
|
||||
if alloc.paxos.Think() {
|
||||
// If something important changed, broadcast
|
||||
alloc.gossip.GossipBroadcast(alloc.Gossip())
|
||||
}
|
||||
|
||||
if ok, cons := alloc.paxos.Consensus(); ok {
|
||||
alloc.createRing(cons.Value)
|
||||
}
|
||||
}
|
||||
} else if sender != mesh.UnknownPeerName {
|
||||
// Sender is trying to initialize a ring, but we have one
|
||||
// already - send it straight back
|
||||
alloc.sendRingUpdate(sender)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (alloc *Allocator) donateSpace(r address.Range, to mesh.PeerName) {
|
||||
// No matter what we do, we'll send a unicast gossip
|
||||
// of our ring back to tha chap who asked for space.
|
||||
// This serves to both tell him of any space we might
|
||||
// have given him, or tell him where he might find some
|
||||
// more.
|
||||
defer alloc.sendRingUpdate(to)
|
||||
|
||||
alloc.debugln("Peer", to, "asked me for space")
|
||||
chunk, ok := alloc.space.Donate(r)
|
||||
if !ok {
|
||||
free := alloc.space.NumFreeAddressesInRange(r)
|
||||
common.Assert(free == 0)
|
||||
alloc.debugln("No space to give to peer", to)
|
||||
// separate message maintains backwards-compatibility:
|
||||
// down-level peers will ignore this and still get the ring update.
|
||||
alloc.sendSpaceRequestDenied(to, r)
|
||||
return
|
||||
}
|
||||
alloc.debugln("Giving range", chunk, "to", to)
|
||||
alloc.ring.GrantRangeToHost(chunk.Start, chunk.End, to)
|
||||
alloc.sendRingUpdate(to)
|
||||
}
|
||||
|
||||
func (alloc *Allocator) assertInvariants() {
|
||||
// We need to ensure all ranges the ring thinks we own have
|
||||
// a corresponding space in the space set, and vice versa
|
||||
checkSpace := space.New()
|
||||
checkSpace.AddRanges(alloc.ring.OwnedRanges())
|
||||
ranges := checkSpace.OwnedRanges()
|
||||
spaces := alloc.space.OwnedRanges()
|
||||
|
||||
common.Assert(len(ranges) == len(spaces))
|
||||
|
||||
for i := 0; i < len(ranges); i++ {
|
||||
r := ranges[i]
|
||||
s := spaces[i]
|
||||
common.Assert(s.Start == r.Start && s.End == r.End)
|
||||
}
|
||||
}
|
||||
|
||||
func (alloc *Allocator) reportFreeSpace() {
|
||||
ranges := alloc.ring.OwnedRanges()
|
||||
if len(ranges) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
freespace := make(map[address.Address]address.Offset)
|
||||
for _, r := range ranges {
|
||||
freespace[r.Start] = alloc.space.NumFreeAddressesInRange(r)
|
||||
}
|
||||
alloc.ring.ReportFree(freespace)
|
||||
}
|
||||
|
||||
// Owned addresses
|
||||
|
||||
// NB: addr must not be owned by ident already
|
||||
func (alloc *Allocator) addOwned(ident string, addr address.Address) {
|
||||
alloc.owned[ident] = append(alloc.owned[ident], addr)
|
||||
}
|
||||
|
||||
func (alloc *Allocator) lookupOwned(ident string, r address.Range) (address.Address, bool) {
|
||||
for _, addr := range alloc.owned[ident] {
|
||||
if r.Contains(addr) {
|
||||
return addr, true
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func (alloc *Allocator) findOwner(addr address.Address) string {
|
||||
for ident, addrs := range alloc.owned {
|
||||
for _, candidate := range addrs {
|
||||
if candidate == addr {
|
||||
return ident
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Logging
|
||||
|
||||
func (alloc *Allocator) infof(fmt string, args ...interface{}) {
|
||||
common.Log.Infof("[allocator %s] "+fmt, append([]interface{}{alloc.ourName}, args...)...)
|
||||
}
|
||||
func (alloc *Allocator) debugln(args ...interface{}) {
|
||||
common.Log.Debugln(append([]interface{}{fmt.Sprintf("[allocator %s]:", alloc.ourName)}, args...)...)
|
||||
}
|
||||
func (alloc *Allocator) debugf(fmt string, args ...interface{}) {
|
||||
common.Log.Debugf("[allocator %s] "+fmt, append([]interface{}{alloc.ourName}, args...)...)
|
||||
}
|
||||
@@ -0,0 +1,576 @@
|
||||
package ipam
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/weaveworks/weave/common"
|
||||
"github.com/weaveworks/weave/net/address"
|
||||
"github.com/weaveworks/weave/testing/gossip"
|
||||
)
|
||||
|
||||
const (
|
||||
testStart1 = "10.0.1.0"
|
||||
testStart2 = "10.0.2.0"
|
||||
testStart3 = "10.0.3.0"
|
||||
)
|
||||
|
||||
func returnFalse() bool { return false }
|
||||
|
||||
func TestAllocFree(t *testing.T) {
|
||||
const (
|
||||
container1 = "abcdef"
|
||||
container2 = "baddf00d"
|
||||
container3 = "b01df00d"
|
||||
universe = "10.0.3.0/26"
|
||||
subnet1 = "10.0.3.0/28"
|
||||
subnet2 = "10.0.3.32/28"
|
||||
testAddr1 = "10.0.3.1"
|
||||
testAddr2 = "10.0.3.33"
|
||||
spaceSize = 62 // 64 IP addresses in /26, minus .0 and .63
|
||||
)
|
||||
|
||||
alloc, subnet := makeAllocatorWithMockGossip(t, "01:00:00:01:00:00", universe, 1)
|
||||
defer alloc.Stop()
|
||||
_, cidr1, _ := address.ParseCIDR(subnet1)
|
||||
_, cidr2, _ := address.ParseCIDR(subnet2)
|
||||
|
||||
alloc.claimRingForTesting()
|
||||
addr1, err := alloc.Allocate(container1, cidr1.HostRange(), returnFalse)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, testAddr1, addr1.String(), "address")
|
||||
|
||||
addr2, err := alloc.Allocate(container1, cidr2.HostRange(), returnFalse)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, testAddr2, addr2.String(), "address")
|
||||
|
||||
// Ask for another address for a different container and check it's different
|
||||
addr1b, _ := alloc.Allocate(container2, cidr1.HostRange(), returnFalse)
|
||||
if addr1b.String() == testAddr1 {
|
||||
t.Fatalf("Expected different address but got %s", addr1b.String())
|
||||
}
|
||||
|
||||
// Ask for the first container again and we should get the same addresses again
|
||||
addr1a, _ := alloc.Allocate(container1, cidr1.HostRange(), returnFalse)
|
||||
require.Equal(t, testAddr1, addr1a.String(), "address")
|
||||
addr2a, _ := alloc.Allocate(container1, cidr2.HostRange(), returnFalse)
|
||||
require.Equal(t, testAddr2, addr2a.String(), "address")
|
||||
|
||||
// Now delete the first container, and we should get its addresses back
|
||||
require.NoError(t, alloc.Delete(container1))
|
||||
addr3, _ := alloc.Allocate(container3, cidr1.HostRange(), returnFalse)
|
||||
require.Equal(t, testAddr1, addr3.String(), "address")
|
||||
addr4, _ := alloc.Allocate(container3, cidr2.HostRange(), returnFalse)
|
||||
require.Equal(t, testAddr2, addr4.String(), "address")
|
||||
|
||||
alloc.ContainerDied(container2)
|
||||
alloc.ContainerDied(container3)
|
||||
require.Equal(t, address.Offset(spaceSize), alloc.NumFreeAddresses(subnet))
|
||||
}
|
||||
|
||||
func TestBootstrap(t *testing.T) {
|
||||
const (
|
||||
donateSize = 5
|
||||
donateStart = "10.0.1.7"
|
||||
ourNameString = "01:00:00:01:00:00"
|
||||
peerNameString = "02:00:00:02:00:00"
|
||||
)
|
||||
|
||||
alloc1, subnet := makeAllocatorWithMockGossip(t, ourNameString, testStart1+"/22", 2)
|
||||
defer alloc1.Stop()
|
||||
|
||||
// Simulate another peer on the gossip network
|
||||
alloc2, _ := makeAllocatorWithMockGossip(t, peerNameString, testStart1+"/22", 2)
|
||||
defer alloc2.Stop()
|
||||
|
||||
alloc1.OnGossipBroadcast(alloc2.ourName, alloc2.Encode())
|
||||
|
||||
alloc1.actionChan <- func() { alloc1.tryPendingOps() }
|
||||
|
||||
ExpectBroadcastMessage(alloc1, nil) // alloc1 will try to form consensus
|
||||
done := make(chan bool)
|
||||
go func() {
|
||||
alloc1.Allocate("somecontainer", subnet, returnFalse)
|
||||
done <- true
|
||||
}()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
AssertNothingSent(t, done)
|
||||
|
||||
CheckAllExpectedMessagesSent(alloc1, alloc2)
|
||||
|
||||
alloc1.actionChan <- func() { alloc1.tryPendingOps() }
|
||||
AssertNothingSent(t, done)
|
||||
|
||||
CheckAllExpectedMessagesSent(alloc1, alloc2)
|
||||
|
||||
// alloc2 receives paxos update and broadcasts its reply
|
||||
ExpectBroadcastMessage(alloc2, nil)
|
||||
alloc2.OnGossipBroadcast(alloc1.ourName, alloc1.Encode())
|
||||
|
||||
ExpectBroadcastMessage(alloc1, nil)
|
||||
alloc1.OnGossipBroadcast(alloc2.ourName, alloc2.Encode())
|
||||
|
||||
// both nodes will get consensus now so initialize the ring
|
||||
ExpectBroadcastMessage(alloc2, nil)
|
||||
ExpectBroadcastMessage(alloc2, nil)
|
||||
alloc2.OnGossipBroadcast(alloc1.ourName, alloc1.Encode())
|
||||
|
||||
CheckAllExpectedMessagesSent(alloc1, alloc2)
|
||||
|
||||
alloc1.OnGossipBroadcast(alloc2.ourName, alloc2.Encode())
|
||||
// now alloc1 should have space
|
||||
|
||||
AssertSent(t, done)
|
||||
|
||||
CheckAllExpectedMessagesSent(alloc1, alloc2)
|
||||
}
|
||||
|
||||
func TestAllocatorClaim(t *testing.T) {
|
||||
const (
|
||||
container1 = "abcdef"
|
||||
container3 = "b01df00d"
|
||||
universe = "10.0.3.0/24"
|
||||
testAddr1 = "10.0.3.2"
|
||||
testAddr2 = "10.0.4.2"
|
||||
)
|
||||
|
||||
allocs, _, subnet := makeNetworkOfAllocators(2, universe)
|
||||
alloc := allocs[1]
|
||||
defer alloc.Stop()
|
||||
addr1, _ := address.ParseIP(testAddr1)
|
||||
|
||||
// First claim should trigger "dunno, I'm going to wait"
|
||||
err := alloc.Claim(container3, addr1, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Do one allocate to ensure paxos is all done
|
||||
alloc.Allocate("unused", subnet, returnFalse)
|
||||
// Do an allocate on the other peer, which we will try to claim later
|
||||
addrx, err := allocs[0].Allocate(container1, subnet, returnFalse)
|
||||
|
||||
// Now try the claim again
|
||||
err = alloc.Claim(container3, addr1, true)
|
||||
require.NoError(t, err)
|
||||
// Check we get this address back if we try an allocate
|
||||
addr3, _ := alloc.Allocate(container3, subnet, returnFalse)
|
||||
require.Equal(t, testAddr1, addr3.String(), "address")
|
||||
// one more claim should still work
|
||||
err = alloc.Claim(container3, addr1, true)
|
||||
require.NoError(t, err)
|
||||
// claim for a different container should fail
|
||||
err = alloc.Claim(container1, addr1, true)
|
||||
require.Error(t, err)
|
||||
// claiming the address allocated on the other peer should fail
|
||||
err = alloc.Claim(container1, addrx, true)
|
||||
require.Error(t, err, "claiming address allocated on other peer should fail")
|
||||
// Check an address outside of our universe
|
||||
addr2, _ := address.ParseIP(testAddr2)
|
||||
err = alloc.Claim(container1, addr2, true)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func (alloc *Allocator) pause() func() {
|
||||
paused := make(chan struct{})
|
||||
alloc.actionChan <- func() {
|
||||
paused <- struct{}{}
|
||||
<-paused
|
||||
}
|
||||
<-paused
|
||||
return func() {
|
||||
close(paused)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCancel(t *testing.T) {
|
||||
const (
|
||||
CIDR = "10.0.1.7/26"
|
||||
)
|
||||
|
||||
router := gossip.NewTestRouter(0.0)
|
||||
|
||||
alloc1, subnet := makeAllocator("01:00:00:02:00:00", CIDR, 2)
|
||||
alloc1.SetInterfaces(router.Connect(alloc1.ourName, alloc1))
|
||||
|
||||
alloc2, _ := makeAllocator("02:00:00:02:00:00", CIDR, 2)
|
||||
alloc2.SetInterfaces(router.Connect(alloc2.ourName, alloc2))
|
||||
alloc1.claimRingForTesting(alloc1, alloc2)
|
||||
alloc2.claimRingForTesting(alloc1, alloc2)
|
||||
|
||||
alloc1.Start()
|
||||
alloc2.Start()
|
||||
|
||||
// tell peers about each other
|
||||
alloc1.OnGossipBroadcast(alloc2.ourName, alloc2.Encode())
|
||||
|
||||
// Get some IPs, so each allocator has some space
|
||||
res1, _ := alloc1.Allocate("foo", subnet, returnFalse)
|
||||
common.Log.Debugf("res1 = %s", res1.String())
|
||||
res2, _ := alloc2.Allocate("bar", subnet, returnFalse)
|
||||
common.Log.Debugf("res2 = %s", res2.String())
|
||||
if res1 == res2 {
|
||||
require.FailNow(t, "Error: got same ips!")
|
||||
}
|
||||
|
||||
// Now we're going to pause alloc2 and ask alloc1
|
||||
// for an allocation
|
||||
unpause := alloc2.pause()
|
||||
|
||||
// Use up all the IPs that alloc1 owns, so the allocation after this will prompt a request to alloc2
|
||||
for i := 0; alloc1.NumFreeAddresses(subnet) > 0; i++ {
|
||||
alloc1.Allocate(fmt.Sprintf("tmp%d", i), subnet, returnFalse)
|
||||
}
|
||||
cancelChan := make(chan bool, 1)
|
||||
doneChan := make(chan bool)
|
||||
go func() {
|
||||
_, ok := alloc1.Allocate("baz", subnet,
|
||||
func() bool {
|
||||
select {
|
||||
case <-cancelChan:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
})
|
||||
doneChan <- ok == nil
|
||||
}()
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
AssertNothingSent(t, doneChan)
|
||||
|
||||
cancelChan <- true
|
||||
unpause()
|
||||
if <-doneChan {
|
||||
require.FailNow(t, "Error: got result from Allocate")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCancelOnDied(t *testing.T) {
|
||||
const (
|
||||
CIDR = "10.0.1.7/26"
|
||||
container1 = "abcdef"
|
||||
)
|
||||
|
||||
router := gossip.NewTestRouter(0.0)
|
||||
alloc1, subnet := makeAllocator("01:00:00:02:00:00", CIDR, 2)
|
||||
alloc1.SetInterfaces(router.Connect(alloc1.ourName, alloc1))
|
||||
alloc1.Start()
|
||||
|
||||
doneChan := make(chan bool)
|
||||
f := func() {
|
||||
_, ok := alloc1.Allocate(container1, subnet, returnFalse)
|
||||
doneChan <- ok == nil
|
||||
}
|
||||
|
||||
// Attempt two allocations in parallel, to check that this is handled correctly
|
||||
go f()
|
||||
go f()
|
||||
|
||||
// Nothing should happen, because we declared the quorum as 2
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
AssertNothingSent(t, doneChan)
|
||||
|
||||
alloc1.ContainerDied(container1)
|
||||
|
||||
// Check that the two allocations both exit with an error
|
||||
if <-doneChan || <-doneChan {
|
||||
require.FailNow(t, "Error: got result from Allocate")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGossipShutdown(t *testing.T) {
|
||||
const (
|
||||
container1 = "abcdef"
|
||||
container2 = "baddf00d"
|
||||
universe = "10.0.3.0/30"
|
||||
)
|
||||
|
||||
alloc, subnet := makeAllocatorWithMockGossip(t, "01:00:00:01:00:00", universe, 1)
|
||||
defer alloc.Stop()
|
||||
|
||||
alloc.claimRingForTesting()
|
||||
alloc.Allocate(container1, subnet, returnFalse)
|
||||
|
||||
alloc.Shutdown()
|
||||
|
||||
_, err := alloc.Allocate(container2, subnet, returnFalse) // trying to allocate after shutdown should fail
|
||||
require.False(t, err == nil, "no address")
|
||||
|
||||
CheckAllExpectedMessagesSent(alloc)
|
||||
}
|
||||
|
||||
func TestTransfer(t *testing.T) {
|
||||
const (
|
||||
cidr = "10.0.1.7/22"
|
||||
)
|
||||
allocs, router, subnet := makeNetworkOfAllocators(3, cidr)
|
||||
alloc1 := allocs[0]
|
||||
alloc2 := allocs[1]
|
||||
alloc3 := allocs[2] // This will be 'master' and get the first range
|
||||
|
||||
_, err := alloc2.Allocate("foo", subnet, returnFalse)
|
||||
require.True(t, err == nil, "Failed to get address")
|
||||
|
||||
_, err = alloc3.Allocate("bar", subnet, returnFalse)
|
||||
require.True(t, err == nil, "Failed to get address")
|
||||
|
||||
alloc2.gossip.GossipBroadcast(alloc2.Gossip())
|
||||
router.Flush()
|
||||
alloc2.gossip.GossipBroadcast(alloc3.Gossip())
|
||||
router.Flush()
|
||||
router.RemovePeer(alloc2.ourName)
|
||||
router.RemovePeer(alloc3.ourName)
|
||||
alloc2.Stop()
|
||||
alloc3.Stop()
|
||||
router.Flush()
|
||||
require.NoError(t, alloc1.AdminTakeoverRanges(alloc2.ourName.String()))
|
||||
require.NoError(t, alloc1.AdminTakeoverRanges(alloc3.ourName.String()))
|
||||
router.Flush()
|
||||
|
||||
require.Equal(t, address.Offset(1022), alloc1.NumFreeAddresses(subnet))
|
||||
|
||||
_, err = alloc1.Allocate("foo", subnet, returnFalse)
|
||||
require.True(t, err == nil, "Failed to get address")
|
||||
alloc1.Stop()
|
||||
}
|
||||
|
||||
func TestFakeRouterSimple(t *testing.T) {
|
||||
const (
|
||||
cidr = "10.0.1.7/22"
|
||||
)
|
||||
allocs, _, subnet := makeNetworkOfAllocators(2, cidr)
|
||||
defer stopNetworkOfAllocators(allocs)
|
||||
|
||||
alloc1 := allocs[0]
|
||||
//alloc2 := allocs[1]
|
||||
|
||||
_, err := alloc1.Allocate("foo", subnet, returnFalse)
|
||||
require.NoError(t, err, "Failed to get address")
|
||||
}
|
||||
|
||||
func TestAllocatorFuzz(t *testing.T) {
|
||||
const (
|
||||
firstpass = 1000
|
||||
secondpass = 10000
|
||||
nodes = 5
|
||||
maxAddresses = 1000
|
||||
concurrency = 5
|
||||
cidr = "10.0.1.7/22"
|
||||
)
|
||||
allocs, _, subnet := makeNetworkOfAllocators(nodes, cidr)
|
||||
defer stopNetworkOfAllocators(allocs)
|
||||
|
||||
// Test state
|
||||
// For each IP issued we store the allocator
|
||||
// that issued it and the name of the container
|
||||
// it was issued to.
|
||||
type result struct {
|
||||
name string
|
||||
alloc int32
|
||||
block bool
|
||||
}
|
||||
stateLock := sync.Mutex{}
|
||||
state := make(map[string]result)
|
||||
// Keep a list of addresses issued, so we
|
||||
// Can pick random ones
|
||||
var addrs []string
|
||||
numPending := 0
|
||||
|
||||
rand.Seed(0)
|
||||
|
||||
// Remove item from list by swapping it with last
|
||||
// and reducing slice length by 1
|
||||
rm := func(xs []string, i int32) []string {
|
||||
ls := len(xs) - 1
|
||||
xs[i] = xs[ls]
|
||||
return xs[:ls]
|
||||
}
|
||||
|
||||
bumpPending := func() bool {
|
||||
stateLock.Lock()
|
||||
if len(addrs)+numPending >= maxAddresses {
|
||||
stateLock.Unlock()
|
||||
return false
|
||||
}
|
||||
numPending++
|
||||
stateLock.Unlock()
|
||||
return true
|
||||
}
|
||||
|
||||
noteAllocation := func(allocIndex int32, name string, addr address.Address) {
|
||||
//common.Log.Infof("Allocate: got address %s for name %s", addr, name)
|
||||
addrStr := addr.String()
|
||||
|
||||
stateLock.Lock()
|
||||
defer stateLock.Unlock()
|
||||
|
||||
if res, existing := state[addrStr]; existing {
|
||||
panic(fmt.Sprintf("Dup found for address %s - %s and %s", addrStr,
|
||||
name, res.name))
|
||||
}
|
||||
|
||||
state[addrStr] = result{name, allocIndex, false}
|
||||
addrs = append(addrs, addrStr)
|
||||
numPending--
|
||||
}
|
||||
|
||||
// Do a Allocate and check the address
|
||||
// is unique. Needs a unique container
|
||||
// name.
|
||||
allocate := func(name string) {
|
||||
if !bumpPending() {
|
||||
return
|
||||
}
|
||||
|
||||
allocIndex := rand.Int31n(nodes)
|
||||
alloc := allocs[allocIndex]
|
||||
//common.Log.Infof("Allocate: asking allocator %d", allocIndex)
|
||||
addr, err := alloc.Allocate(name, subnet, returnFalse)
|
||||
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Could not allocate addr"))
|
||||
}
|
||||
|
||||
noteAllocation(allocIndex, name, addr)
|
||||
}
|
||||
|
||||
// Free a random address.
|
||||
free := func() {
|
||||
stateLock.Lock()
|
||||
if len(addrs) == 0 {
|
||||
stateLock.Unlock()
|
||||
return
|
||||
}
|
||||
// Delete an existing allocation
|
||||
// Pick random addr
|
||||
addrIndex := rand.Int31n(int32(len(addrs)))
|
||||
addr := addrs[addrIndex]
|
||||
res := state[addr]
|
||||
if res.block {
|
||||
stateLock.Unlock()
|
||||
return
|
||||
}
|
||||
addrs = rm(addrs, addrIndex)
|
||||
delete(state, addr)
|
||||
stateLock.Unlock()
|
||||
|
||||
alloc := allocs[res.alloc]
|
||||
//common.Log.Infof("Freeing %s (%s) on allocator %d", res.name, addr, res.alloc)
|
||||
|
||||
oldAddr, err := address.ParseIP(addr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
require.NoError(t, alloc.Free(res.name, oldAddr))
|
||||
}
|
||||
|
||||
// Do a Allocate on an existing container & allocator
|
||||
// and check we get the right answer.
|
||||
allocateAgain := func() {
|
||||
stateLock.Lock()
|
||||
addrIndex := rand.Int31n(int32(len(addrs)))
|
||||
addr := addrs[addrIndex]
|
||||
res := state[addr]
|
||||
if res.block {
|
||||
stateLock.Unlock()
|
||||
return
|
||||
}
|
||||
res.block = true
|
||||
state[addr] = res
|
||||
stateLock.Unlock()
|
||||
alloc := allocs[res.alloc]
|
||||
|
||||
//common.Log.Infof("Asking for %s (%s) on allocator %d again", res.name, addr, res.alloc)
|
||||
|
||||
newAddr, _ := alloc.Allocate(res.name, subnet, returnFalse)
|
||||
oldAddr, _ := address.ParseIP(addr)
|
||||
if newAddr != oldAddr {
|
||||
panic(fmt.Sprintf("Got different address for repeat request for %s: %s != %s", res.name, newAddr, oldAddr))
|
||||
}
|
||||
|
||||
stateLock.Lock()
|
||||
res.block = false
|
||||
state[addr] = res
|
||||
stateLock.Unlock()
|
||||
}
|
||||
|
||||
// Claim a random address for a unique container name - may not succeed
|
||||
claim := func(name string) {
|
||||
if !bumpPending() {
|
||||
return
|
||||
}
|
||||
allocIndex := rand.Int31n(nodes)
|
||||
addressIndex := rand.Int31n(int32(subnet.Size()))
|
||||
alloc := allocs[allocIndex]
|
||||
addr := address.Add(subnet.Start, address.Offset(addressIndex))
|
||||
err := alloc.Claim(name, addr, true)
|
||||
if err == nil {
|
||||
noteAllocation(allocIndex, name, addr)
|
||||
}
|
||||
}
|
||||
|
||||
// Run function _f_ _iterations_ times, in _concurrency_
|
||||
// number of goroutines
|
||||
doConcurrentIterations := func(iterations int, f func(int)) {
|
||||
iterationsPerThread := iterations / concurrency
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
for i := 0; i < concurrency; i++ {
|
||||
wg.Add(1)
|
||||
go func(j int) {
|
||||
defer wg.Done()
|
||||
for k := 0; k < iterationsPerThread; k++ {
|
||||
f((j * iterationsPerThread) + k)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// First pass, just allocate a bunch of ips
|
||||
doConcurrentIterations(firstpass, func(iteration int) {
|
||||
name := fmt.Sprintf("first%d", iteration)
|
||||
allocate(name)
|
||||
})
|
||||
|
||||
// Second pass, random ask for more allocations,
|
||||
// or remove existing ones, or ask for allocation
|
||||
// again.
|
||||
doConcurrentIterations(secondpass, func(iteration int) {
|
||||
r := rand.Float32()
|
||||
switch {
|
||||
case 0.0 <= r && r < 0.4:
|
||||
// Ask for a new allocation
|
||||
name := fmt.Sprintf("second%d", iteration)
|
||||
allocate(name)
|
||||
|
||||
case (0.4 <= r && r < 0.8):
|
||||
// free a random addr
|
||||
free()
|
||||
|
||||
case 0.8 <= r && r < 0.95:
|
||||
// ask for an existing name again, check we get same ip
|
||||
allocateAgain()
|
||||
|
||||
case 0.95 <= r && r < 1.0:
|
||||
name := fmt.Sprintf("second%d", iteration)
|
||||
claim(name)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestGossipSkew(t *testing.T) {
|
||||
alloc1, _ := makeAllocatorWithMockGossip(t, "01:00:00:01:00:00", "10.0.1.0/22", 2)
|
||||
defer alloc1.Stop()
|
||||
alloc2, _ := makeAllocatorWithMockGossip(t, "02:00:00:02:00:00", "10.0.1.0/22", 2)
|
||||
alloc2.now = func() time.Time { return time.Now().Add(time.Hour * 2) }
|
||||
defer alloc2.Stop()
|
||||
|
||||
if _, err := alloc1.OnGossipBroadcast(alloc2.ourName, alloc2.Encode()); err == nil {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
package ipam
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/weaveworks/weave/common"
|
||||
"github.com/weaveworks/weave/mesh"
|
||||
"github.com/weaveworks/weave/net/address"
|
||||
)
|
||||
|
||||
type claim struct {
|
||||
resultChan chan<- error
|
||||
ident string
|
||||
addr address.Address
|
||||
noErrorOnUnknown bool
|
||||
}
|
||||
|
||||
// Send an error (or nil for success) back to caller listening on resultChan
|
||||
func (c *claim) sendResult(result error) {
|
||||
// Make sure we only send a result once, since listener stops listening after that
|
||||
if c.resultChan != nil {
|
||||
c.resultChan <- result
|
||||
close(c.resultChan)
|
||||
c.resultChan = nil
|
||||
return
|
||||
}
|
||||
if result != nil {
|
||||
common.Log.Errorln("[allocator] " + result.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Try returns true for success (or failure), false if we need to try again later
|
||||
func (c *claim) Try(alloc *Allocator) bool {
|
||||
if !alloc.ring.Contains(c.addr) {
|
||||
// Address not within our universe; assume user knows what they are doing
|
||||
alloc.infof("Ignored address %s claimed by %s - not in our universe", c.addr, c.ident)
|
||||
c.sendResult(nil)
|
||||
return true
|
||||
}
|
||||
|
||||
alloc.establishRing()
|
||||
|
||||
switch owner := alloc.ring.Owner(c.addr); owner {
|
||||
case alloc.ourName:
|
||||
// success
|
||||
case mesh.UnknownPeerName:
|
||||
// If our ring doesn't know, it must be empty.
|
||||
if c.noErrorOnUnknown {
|
||||
alloc.infof("Claim %s for %s: address allocator still initializing; will try later.", c.addr, c.ident)
|
||||
c.sendResult(nil)
|
||||
} else {
|
||||
c.sendResult(fmt.Errorf("%s is in the range %s, but the allocator is not initialized yet", c.addr, alloc.universe.AsCIDRString()))
|
||||
}
|
||||
return false
|
||||
default:
|
||||
alloc.debugf("requesting address %s from other peer %s", c.addr, owner)
|
||||
err := alloc.sendSpaceRequest(owner, address.NewRange(c.addr, 1))
|
||||
if err != nil { // can't speak to owner right now
|
||||
if c.noErrorOnUnknown {
|
||||
alloc.infof("Claim %s for %s: %s; will try later.", c.addr, c.ident, err)
|
||||
c.sendResult(nil)
|
||||
} else { // just tell the user they can't do this.
|
||||
c.deniedBy(alloc, owner)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// We are the owner, check we haven't given it to another container
|
||||
switch existingIdent := alloc.findOwner(c.addr); existingIdent {
|
||||
case "":
|
||||
if err := alloc.space.Claim(c.addr); err == nil {
|
||||
alloc.debugln("Claimed", c.addr, "for", c.ident)
|
||||
alloc.addOwned(c.ident, c.addr)
|
||||
c.sendResult(nil)
|
||||
} else {
|
||||
c.sendResult(err)
|
||||
}
|
||||
case c.ident:
|
||||
// same identifier is claiming same address; that's OK
|
||||
c.sendResult(nil)
|
||||
default:
|
||||
// Addr already owned by container on this machine
|
||||
c.sendResult(fmt.Errorf("address %s is already owned by %s", c.addr.String(), existingIdent))
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *claim) deniedBy(alloc *Allocator, owner mesh.PeerName) {
|
||||
name, found := alloc.nicknames[owner]
|
||||
if found {
|
||||
name = " (" + name + ")"
|
||||
}
|
||||
c.sendResult(fmt.Errorf("address %s is owned by other peer %s%s", c.addr.String(), owner, name))
|
||||
}
|
||||
|
||||
func (c *claim) Cancel() {
|
||||
c.sendResult(&errorCancelled{"Claim", c.ident})
|
||||
}
|
||||
|
||||
func (c *claim) ForContainer(ident string) bool {
|
||||
return c.ident == ident
|
||||
}
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
package ipam
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"github.com/weaveworks/weave/common"
|
||||
"github.com/weaveworks/weave/common/docker"
|
||||
"github.com/weaveworks/weave/net/address"
|
||||
)
|
||||
|
||||
func badRequest(w http.ResponseWriter, err error) {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
common.Log.Warningln("[allocator]:", err.Error())
|
||||
}
|
||||
|
||||
func parseCIDR(w http.ResponseWriter, cidrStr string) (address.CIDR, bool) {
|
||||
subnetAddr, cidr, err := address.ParseCIDR(cidrStr)
|
||||
if err != nil {
|
||||
badRequest(w, err)
|
||||
return address.CIDR{}, false
|
||||
}
|
||||
if cidr.Start != subnetAddr {
|
||||
badRequest(w, fmt.Errorf("Invalid subnet %s - bits after network prefix are not all zero", cidrStr))
|
||||
return address.CIDR{}, false
|
||||
}
|
||||
return cidr, true
|
||||
}
|
||||
|
||||
func (alloc *Allocator) handleHTTPAllocate(dockerCli *docker.Client, w http.ResponseWriter, ident string, checkAlive bool, subnet address.CIDR) {
|
||||
closedChan := w.(http.CloseNotifier).CloseNotify()
|
||||
addr, err := alloc.Allocate(ident, subnet.HostRange(),
|
||||
func() bool {
|
||||
select {
|
||||
case <-closedChan:
|
||||
return true
|
||||
default:
|
||||
res := checkAlive && dockerCli != nil && dockerCli.IsContainerNotRunning(ident)
|
||||
checkAlive = false // we check only once; if the container dies later we learn about that through events
|
||||
return res
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
if _, ok := err.(*errorCancelled); ok { // cancellation is not really an error
|
||||
common.Log.Infoln("[allocator]:", err.Error())
|
||||
fmt.Fprint(w, "cancelled")
|
||||
return
|
||||
}
|
||||
badRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "%s/%d", addr, subnet.PrefixLen)
|
||||
}
|
||||
|
||||
// HandleHTTP wires up ipams HTTP endpoints to the provided mux.
|
||||
func (alloc *Allocator) HandleHTTP(router *mux.Router, defaultSubnet address.CIDR, dockerCli *docker.Client) {
|
||||
router.Methods("GET").Path("/ipinfo/defaultsubnet").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintf(w, "%s", defaultSubnet)
|
||||
})
|
||||
|
||||
router.Methods("PUT").Path("/ip/{id}/{ip}").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
ident := vars["id"]
|
||||
ipStr := vars["ip"]
|
||||
noErrorOnUnknown := r.FormValue("noErrorOnUnknown") == "true"
|
||||
if ip, err := address.ParseIP(ipStr); err != nil {
|
||||
badRequest(w, err)
|
||||
return
|
||||
} else if err := alloc.Claim(ident, ip, noErrorOnUnknown); err != nil {
|
||||
badRequest(w, fmt.Errorf("Unable to claim: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(204)
|
||||
})
|
||||
|
||||
router.Methods("GET").Path("/ip/{id}/{ip}/{prefixlen}").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
if subnet, ok := parseCIDR(w, vars["ip"]+"/"+vars["prefixlen"]); ok {
|
||||
addr, err := alloc.Lookup(vars["id"], subnet.HostRange())
|
||||
if err != nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(w, "%s/%d", addr, subnet.PrefixLen)
|
||||
}
|
||||
})
|
||||
|
||||
router.Methods("GET").Path("/ip/{id}").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
addr, err := alloc.Lookup(mux.Vars(r)["id"], defaultSubnet.HostRange())
|
||||
if err != nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(w, "%s/%d", addr, defaultSubnet.PrefixLen)
|
||||
})
|
||||
|
||||
router.Methods("POST").Path("/ip/{id}/{ip}/{prefixlen}").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
if subnet, ok := parseCIDR(w, vars["ip"]+"/"+vars["prefixlen"]); ok {
|
||||
alloc.handleHTTPAllocate(dockerCli, w, vars["id"], r.FormValue("check-alive") == "true", subnet)
|
||||
}
|
||||
})
|
||||
|
||||
router.Methods("POST").Path("/ip/{id}").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
alloc.handleHTTPAllocate(dockerCli, w, vars["id"], r.FormValue("check-alive") == "true", defaultSubnet)
|
||||
})
|
||||
|
||||
router.Methods("DELETE").Path("/ip/{id}/{ip}").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
ident := vars["id"]
|
||||
ipStr := vars["ip"]
|
||||
if ip, err := address.ParseIP(ipStr); err != nil {
|
||||
badRequest(w, err)
|
||||
return
|
||||
} else if err := alloc.Free(ident, ip); err != nil {
|
||||
badRequest(w, fmt.Errorf("Unable to free: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(204)
|
||||
})
|
||||
|
||||
router.Methods("DELETE").Path("/ip/{id}").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ident := mux.Vars(r)["id"]
|
||||
if err := alloc.Delete(ident); err != nil {
|
||||
badRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(204)
|
||||
})
|
||||
|
||||
router.Methods("DELETE").Path("/peer").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
alloc.Shutdown()
|
||||
w.WriteHeader(204)
|
||||
})
|
||||
|
||||
router.Methods("DELETE").Path("/peer/{id}").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ident := mux.Vars(r)["id"]
|
||||
if err := alloc.AdminTakeoverRanges(ident); err != nil {
|
||||
badRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(204)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package ipam
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/weaveworks/weave/common"
|
||||
"github.com/weaveworks/weave/net/address"
|
||||
)
|
||||
|
||||
func HTTPPost(t *testing.T, url string) string {
|
||||
resp, err := http.Post(url, "", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode, "http response")
|
||||
defer resp.Body.Close()
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
return string(body)
|
||||
}
|
||||
|
||||
func HTTPGet(t *testing.T, url string) string {
|
||||
resp, err := http.Get(url)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
return string(body)
|
||||
}
|
||||
|
||||
func doHTTP(method string, url string) (resp *http.Response, err error) {
|
||||
req, _ := http.NewRequest(method, url, nil)
|
||||
return http.DefaultClient.Do(req)
|
||||
}
|
||||
|
||||
func listenHTTP(alloc *Allocator, subnet address.CIDR) int {
|
||||
router := mux.NewRouter()
|
||||
router.HandleFunc("/status", func(w http.ResponseWriter, r *http.Request) {
|
||||
io.WriteString(w, fmt.Sprintln(alloc))
|
||||
})
|
||||
alloc.HandleHTTP(router, subnet, nil)
|
||||
|
||||
httpListener, err := net.Listen("tcp", ":0")
|
||||
if err != nil {
|
||||
common.Log.Fatal("Unable to create http listener: ", err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
srv := &http.Server{Handler: router}
|
||||
if err := srv.Serve(httpListener); err != nil {
|
||||
common.Log.Fatal("Unable to serve http: ", err)
|
||||
}
|
||||
}()
|
||||
return httpListener.Addr().(*net.TCPAddr).Port
|
||||
}
|
||||
|
||||
func identURL(port int, containerID string) string {
|
||||
return fmt.Sprintf("http://localhost:%d/ip/%s", port, containerID)
|
||||
}
|
||||
|
||||
func allocURL(port int, cidr string, containerID string) string {
|
||||
return fmt.Sprintf("http://localhost:%d/ip/%s/%s", port, containerID, cidr)
|
||||
}
|
||||
|
||||
func TestHttp(t *testing.T) {
|
||||
var (
|
||||
containerID = "deadbeef"
|
||||
container2 = "baddf00d"
|
||||
container3 = "b01df00d"
|
||||
universe = "10.0.0.0/8"
|
||||
testCIDR1 = "10.0.3.8/29"
|
||||
testCIDR2 = "10.2.0.0/16"
|
||||
testAddr1 = "10.0.3.9/29"
|
||||
testAddr2 = "10.2.0.1/16"
|
||||
)
|
||||
|
||||
alloc, _ := makeAllocatorWithMockGossip(t, "08:00:27:01:c3:9a", universe, 1)
|
||||
_, cidr, _ := address.ParseCIDR(universe)
|
||||
port := listenHTTP(alloc, cidr)
|
||||
alloc.claimRingForTesting()
|
||||
|
||||
// Allocate an address in each subnet, and check we got what we expected
|
||||
cidr1a := HTTPPost(t, allocURL(port, testCIDR1, containerID))
|
||||
require.Equal(t, testAddr1, cidr1a, "address")
|
||||
cidr2a := HTTPPost(t, allocURL(port, testCIDR2, containerID))
|
||||
require.Equal(t, testAddr2, cidr2a, "address")
|
||||
// Now, make the same requests again to check the operation is idempotent
|
||||
check := HTTPGet(t, allocURL(port, testCIDR1, containerID))
|
||||
require.Equal(t, cidr1a, check, "address")
|
||||
check = HTTPGet(t, allocURL(port, testCIDR2, containerID))
|
||||
require.Equal(t, cidr2a, check, "address")
|
||||
|
||||
// Ask the http server for a pair of addresses for another container and check they're different
|
||||
cidr1b := HTTPPost(t, allocURL(port, testCIDR1, container2))
|
||||
require.False(t, cidr1b == testAddr1, "address")
|
||||
cidr2b := HTTPPost(t, allocURL(port, testCIDR2, container2))
|
||||
require.False(t, cidr2b == testAddr2, "address")
|
||||
|
||||
// Now free the first container, and we should get its addresses back when we ask
|
||||
doHTTP("DELETE", identURL(port, containerID))
|
||||
|
||||
cidr1c := HTTPPost(t, allocURL(port, testCIDR1, container3))
|
||||
require.Equal(t, testAddr1, cidr1c, "address")
|
||||
cidr2c := HTTPPost(t, allocURL(port, testCIDR2, container3))
|
||||
require.Equal(t, testAddr2, cidr2c, "address")
|
||||
|
||||
// Would like to shut down the http server at the end of this test
|
||||
// but it's complicated.
|
||||
// See https://groups.google.com/forum/#!topic/golang-nuts/vLHWa5sHnCE
|
||||
}
|
||||
|
||||
func TestBadHttp(t *testing.T) {
|
||||
var (
|
||||
containerID = "deadbeef"
|
||||
testCIDR1 = "10.0.0.0/8"
|
||||
)
|
||||
|
||||
alloc, _ := makeAllocatorWithMockGossip(t, "08:00:27:01:c3:9a", testCIDR1, 1)
|
||||
defer alloc.Stop()
|
||||
_, cidr, _ := address.ParseCIDR(testCIDR1)
|
||||
port := listenHTTP(alloc, cidr)
|
||||
|
||||
alloc.claimRingForTesting()
|
||||
cidr1 := HTTPPost(t, allocURL(port, testCIDR1, containerID))
|
||||
parts := strings.Split(cidr1, "/")
|
||||
testAddr1 := parts[0]
|
||||
// Verb that's not handled
|
||||
resp, err := doHTTP("HEAD", fmt.Sprintf("http://localhost:%d/ip/%s/%s", port, containerID, testAddr1))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusNotFound, resp.StatusCode, "http response")
|
||||
// Mis-spelled URL
|
||||
resp, err = doHTTP("POST", fmt.Sprintf("http://localhost:%d/xip/%s/", port, containerID))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusNotFound, resp.StatusCode, "http response")
|
||||
// Malformed URL
|
||||
resp, err = doHTTP("POST", fmt.Sprintf("http://localhost:%d/ip/%s/foo/bar/baz", port, containerID))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusNotFound, resp.StatusCode, "http response")
|
||||
}
|
||||
|
||||
func TestHTTPCancel(t *testing.T) {
|
||||
var (
|
||||
containerID = "deadbeef"
|
||||
testCIDR1 = "10.0.3.0/29"
|
||||
)
|
||||
|
||||
alloc, _ := makeAllocatorWithMockGossip(t, "08:00:27:01:c3:9a", testCIDR1, 1)
|
||||
defer alloc.Stop()
|
||||
alloc.claimRingForTesting()
|
||||
_, cidr, _ := address.ParseCIDR(testCIDR1)
|
||||
port := listenHTTP(alloc, cidr)
|
||||
|
||||
// Stop the alloc so nothing actually works
|
||||
unpause := alloc.pause()
|
||||
|
||||
// Ask the http server for a new address
|
||||
done := make(chan *http.Response)
|
||||
req, _ := http.NewRequest("POST", allocURL(port, testCIDR1, containerID), nil)
|
||||
go func() {
|
||||
res, _ := http.DefaultClient.Do(req)
|
||||
done <- res
|
||||
}()
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
fmt.Println("Cancelling allocate")
|
||||
http.DefaultTransport.(*http.Transport).CancelRequest(req)
|
||||
unpause()
|
||||
res := <-done
|
||||
if res != nil {
|
||||
require.FailNow(t, "Error: Allocate returned non-nil")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
package paxos
|
||||
|
||||
import (
|
||||
"github.com/weaveworks/weave/mesh"
|
||||
)
|
||||
|
||||
// The node identifier. The use of the UID here is important: Paxos
|
||||
// acceptors must not forget their promises, so it's important that a
|
||||
// node does not restart and lose its Paxos state but claim to have
|
||||
// the same ID.
|
||||
type NodeID struct {
|
||||
Name mesh.PeerName
|
||||
UID mesh.PeerUID
|
||||
}
|
||||
|
||||
// note all fields exported in structs so we can Gob them
|
||||
type ProposalID struct {
|
||||
// round numbers begin at 1. round 0 indicates an
|
||||
// uninitialized ProposalID, and precedes all other ProposalIDs
|
||||
Round uint
|
||||
Proposer NodeID
|
||||
}
|
||||
|
||||
func (a ProposalID) precedes(b ProposalID) bool {
|
||||
switch {
|
||||
case a.Round != b.Round:
|
||||
return a.Round < b.Round
|
||||
case a.Proposer.Name != b.Proposer.Name:
|
||||
return a.Proposer.Name < b.Proposer.Name
|
||||
case a.Proposer.UID != b.Proposer.UID:
|
||||
return a.Proposer.UID < b.Proposer.UID
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (a ProposalID) valid() bool {
|
||||
return a.Round > 0
|
||||
}
|
||||
|
||||
// For seeding IPAM, the value we want consensus on is a set of peer names
|
||||
type Value []mesh.PeerName
|
||||
|
||||
// An AcceptedValue is a Value plus the proposal which originated that
|
||||
// Value. The origin is not essential, but makes comparing
|
||||
// AcceptedValues easy even if comparing Values is not.
|
||||
type AcceptedValue struct {
|
||||
Value Value
|
||||
Origin ProposalID
|
||||
}
|
||||
|
||||
type NodeClaims struct {
|
||||
// The node promises not to accept a proposal with id less
|
||||
// than this.
|
||||
Promise ProposalID
|
||||
|
||||
// The accepted proposal, if valid
|
||||
Accepted ProposalID
|
||||
AcceptedVal AcceptedValue
|
||||
}
|
||||
|
||||
func (a NodeClaims) equals(b NodeClaims) bool {
|
||||
return a.Promise == b.Promise && a.Accepted == b.Accepted &&
|
||||
a.AcceptedVal.Origin == b.AcceptedVal.Origin
|
||||
}
|
||||
|
||||
type GossipState map[NodeID]NodeClaims
|
||||
|
||||
type Node struct {
|
||||
id NodeID
|
||||
quorum uint
|
||||
knows GossipState
|
||||
}
|
||||
|
||||
func NewNode(name mesh.PeerName, uid mesh.PeerUID, quorum uint) *Node {
|
||||
return &Node{
|
||||
id: NodeID{name, uid},
|
||||
quorum: quorum,
|
||||
knows: map[NodeID]NodeClaims{},
|
||||
}
|
||||
}
|
||||
|
||||
func (node *Node) GossipState() GossipState {
|
||||
return node.knows
|
||||
}
|
||||
|
||||
// Update this node's information about what other nodes know.
|
||||
// Returns true if we learned something new.
|
||||
func (node *Node) Update(from GossipState) bool {
|
||||
changed := false
|
||||
|
||||
for i, fromClaims := range from {
|
||||
claims, ok := node.knows[i]
|
||||
if ok {
|
||||
if claims.Promise.precedes(fromClaims.Promise) {
|
||||
claims.Promise = fromClaims.Promise
|
||||
changed = true
|
||||
}
|
||||
|
||||
if claims.Accepted.precedes(fromClaims.Accepted) {
|
||||
claims.Accepted = fromClaims.Accepted
|
||||
claims.AcceptedVal = fromClaims.AcceptedVal
|
||||
changed = true
|
||||
}
|
||||
} else {
|
||||
claims = fromClaims
|
||||
changed = true
|
||||
}
|
||||
|
||||
node.knows[i] = claims
|
||||
}
|
||||
|
||||
return changed
|
||||
}
|
||||
|
||||
func max(a uint, b uint) uint {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// Initiate a new proposal, i.e. the Paxos "Prepare" step. This is
|
||||
// simply a matter of gossipping a new proposal that supersedes all
|
||||
// others.
|
||||
func (node *Node) Propose() {
|
||||
// Find the highest round number around
|
||||
round := uint(0)
|
||||
|
||||
for _, claims := range node.knows {
|
||||
round = max(round, claims.Promise.Round)
|
||||
round = max(round, claims.Accepted.Round)
|
||||
}
|
||||
|
||||
ourClaims := node.knows[node.id]
|
||||
ourClaims.Promise = ProposalID{
|
||||
Round: round + 1,
|
||||
Proposer: node.id,
|
||||
}
|
||||
node.knows[node.id] = ourClaims
|
||||
|
||||
// With a quorum of 1, we can immediately accept our proposal
|
||||
if node.quorum == 1 {
|
||||
node.Think()
|
||||
}
|
||||
}
|
||||
|
||||
// The heart of the consensus algorithm. Return true if we have
|
||||
// changed our claims.
|
||||
func (node *Node) Think() bool {
|
||||
ourClaims := node.knows[node.id]
|
||||
|
||||
// The "Promise" step of Paxos: Copy the highest known
|
||||
// promise.
|
||||
for _, claims := range node.knows {
|
||||
if ourClaims.Promise.precedes(claims.Promise) {
|
||||
ourClaims.Promise = claims.Promise
|
||||
}
|
||||
}
|
||||
|
||||
// The "Accept Request" step of Paxos: Acting as a proposer,
|
||||
// do we have a proposal that has been promised by a quorum?
|
||||
//
|
||||
// In Paxos, the "proposer" and "acceptor" roles are distinct,
|
||||
// so in principle a node acting as a proposer could continue
|
||||
// trying to get its proposal acccepted even after the same
|
||||
// node as an acceptor has superseded that proposal. But
|
||||
// that's pointless in a gossip context: If our promise
|
||||
// supersedes our own proposal, then anyone who hears about
|
||||
// that promise will not accept that proposal. So our
|
||||
// proposal is only in the running if it is also our promise.
|
||||
if ourClaims.Promise.Proposer == node.id {
|
||||
// Determine whether a quorum has promised, and the
|
||||
// best previously-accepted value if there is one.
|
||||
count := uint(0)
|
||||
var accepted ProposalID
|
||||
var acceptedVal AcceptedValue
|
||||
|
||||
for _, claims := range node.knows {
|
||||
if claims.Promise == ourClaims.Promise {
|
||||
count++
|
||||
|
||||
if accepted.precedes(claims.Accepted) {
|
||||
accepted = claims.Accepted
|
||||
acceptedVal = claims.AcceptedVal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if count >= node.quorum {
|
||||
if !accepted.valid() {
|
||||
acceptedVal.Value = node.pickValue()
|
||||
acceptedVal.Origin = ourClaims.Promise
|
||||
}
|
||||
|
||||
// We automatically accept our own proposal,
|
||||
// and that's how we communicate the "accept
|
||||
// request" to other nodes.
|
||||
ourClaims.Accepted = ourClaims.Promise
|
||||
ourClaims.AcceptedVal = acceptedVal
|
||||
}
|
||||
}
|
||||
|
||||
// The "Accepted" step of Paxos: If the proposal we promised
|
||||
// on got accepted by some other node, we accept it too.
|
||||
for _, claims := range node.knows {
|
||||
if claims.Accepted == ourClaims.Promise {
|
||||
ourClaims.Accepted = claims.Accepted
|
||||
ourClaims.AcceptedVal = claims.AcceptedVal
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ourClaims.equals(node.knows[node.id]) {
|
||||
return false
|
||||
}
|
||||
|
||||
node.knows[node.id] = ourClaims
|
||||
return true
|
||||
}
|
||||
|
||||
// When we get to pick a value, we use the set of peer names we know
|
||||
// about. This is not necessarily all peer names, but it is at least
|
||||
// a quorum, and so good enough for seeding the ring.
|
||||
func (node *Node) pickValue() Value {
|
||||
val := make([]mesh.PeerName, len(node.knows))
|
||||
i := 0
|
||||
for id := range node.knows {
|
||||
val[i] = id.Name
|
||||
i++
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
// Has a consensus been reached, based on the known claims of other nodes?
|
||||
func (node *Node) Consensus() (bool, AcceptedValue) {
|
||||
counts := map[ProposalID]uint{}
|
||||
|
||||
for _, claims := range node.knows {
|
||||
if claims.Accepted.valid() {
|
||||
origin := claims.AcceptedVal.Origin
|
||||
count := counts[origin] + 1
|
||||
counts[origin] = count
|
||||
if count >= node.quorum {
|
||||
return true, claims.AcceptedVal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false, AcceptedValue{}
|
||||
}
|
||||
|
||||
type Status struct {
|
||||
KnownNodes int
|
||||
Quorum uint
|
||||
}
|
||||
|
||||
func NewStatus(node *Node) *Status {
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
return &Status{len(node.knows), node.quorum}
|
||||
}
|
||||
@@ -0,0 +1,373 @@
|
||||
package paxos
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/weaveworks/weave/mesh"
|
||||
)
|
||||
|
||||
type TestNode struct {
|
||||
*Node
|
||||
|
||||
// Topology
|
||||
links []*Link
|
||||
isolated bool
|
||||
|
||||
// The first consensus the Node observed
|
||||
firstConsensus AcceptedValue
|
||||
}
|
||||
|
||||
type Link struct {
|
||||
from *TestNode
|
||||
to *TestNode
|
||||
converse *Link
|
||||
|
||||
// A link is considered ready if it is worthwhile gossipping
|
||||
// along it (i.e. unless we already gossipped along it and the
|
||||
// "from" node didn't change since then). If this is <0, then
|
||||
// the link is not ready. Otherwise, this gives the index in
|
||||
// the Model's readyLinks slice.
|
||||
ready int
|
||||
}
|
||||
|
||||
type Model struct {
|
||||
t *testing.T
|
||||
r *rand.Rand
|
||||
quorum uint
|
||||
nodes []TestNode
|
||||
readyLinks []*Link
|
||||
nextID uint
|
||||
}
|
||||
|
||||
func (m *Model) addLink(a, b *TestNode) {
|
||||
if a == b {
|
||||
// Never link a node to itself
|
||||
return
|
||||
}
|
||||
|
||||
ab := Link{from: a, to: b, ready: len(m.readyLinks)}
|
||||
ba := Link{from: b, to: a, converse: &ab, ready: len(m.readyLinks) + 1}
|
||||
ab.converse = &ba
|
||||
a.links = append(a.links, &ab)
|
||||
b.links = append(b.links, &ba)
|
||||
m.readyLinks = append(m.readyLinks, &ab, &ba)
|
||||
}
|
||||
|
||||
func (m *Model) linkExists(a, b *TestNode) bool {
|
||||
for _, l := range a.links {
|
||||
if l.to == b {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
type TestParams struct {
|
||||
// Number of nodes
|
||||
nodeCount uint
|
||||
|
||||
// Probability that two nodes are connected.
|
||||
connectedProb float32
|
||||
|
||||
// Probability that some node will re-propose at each
|
||||
// step. Setting this too high makes it likely that we'll fail
|
||||
// to converge.
|
||||
reproposeProb float32
|
||||
|
||||
// Probability that some node will be isolated at each step.
|
||||
isolateProb float32
|
||||
|
||||
// Probability that some node willbe restarted at each step.
|
||||
restartProb float32
|
||||
}
|
||||
|
||||
// Make a network of nodes with random topology
|
||||
func makeRandomModel(params *TestParams, r *rand.Rand, t *testing.T) *Model {
|
||||
m := Model{
|
||||
t: t,
|
||||
r: r,
|
||||
quorum: params.nodeCount/2 + 1,
|
||||
nodes: make([]TestNode, params.nodeCount),
|
||||
readyLinks: []*Link{},
|
||||
nextID: params.nodeCount + 1,
|
||||
}
|
||||
|
||||
for i := range m.nodes {
|
||||
m.nodes[i].Node = NewNode(mesh.PeerName(i/2+1),
|
||||
mesh.PeerUID(r.Int63()), m.quorum)
|
||||
m.nodes[i].Propose()
|
||||
}
|
||||
|
||||
for i := 1; i < len(m.nodes); i++ {
|
||||
// was node i connected to the other nodes yet?
|
||||
connected := false
|
||||
|
||||
for j := 0; j < i; j++ {
|
||||
if r.Float32() < params.connectedProb {
|
||||
connected = true
|
||||
m.addLink(&m.nodes[i], &m.nodes[j])
|
||||
}
|
||||
}
|
||||
|
||||
if !connected {
|
||||
// node i must be connected into the graph
|
||||
// somewhere. So if we didn't connect it
|
||||
// already, this is a last resort.
|
||||
m.addLink(&m.nodes[i], &m.nodes[r.Intn(i)])
|
||||
}
|
||||
}
|
||||
|
||||
return &m
|
||||
}
|
||||
|
||||
// Mark a link as unready
|
||||
func (m *Model) unreadyLink(link *Link) {
|
||||
i := link.ready
|
||||
if i >= 0 {
|
||||
m.readyLinks[i] = m.readyLinks[len(m.readyLinks)-1]
|
||||
m.readyLinks[i].ready = i
|
||||
m.readyLinks = m.readyLinks[:len(m.readyLinks)-1]
|
||||
link.ready = -1
|
||||
}
|
||||
}
|
||||
|
||||
// Mark a link as ready
|
||||
func (m *Model) readyLink(link *Link) {
|
||||
if link.ready < 0 {
|
||||
link.ready = len(m.readyLinks)
|
||||
m.readyLinks = append(m.readyLinks, link)
|
||||
}
|
||||
}
|
||||
|
||||
// Mark all the outgoing links from a node as ready
|
||||
func (m *Model) nodeChanged(node *TestNode) {
|
||||
for _, l := range node.links {
|
||||
m.readyLink(l)
|
||||
}
|
||||
}
|
||||
|
||||
// Isolate a node
|
||||
func (m *Model) isolateNode(node *TestNode) {
|
||||
node.isolated = true
|
||||
for _, l := range node.links {
|
||||
if l.ready >= 0 {
|
||||
m.unreadyLink(l)
|
||||
m.unreadyLink(l.converse)
|
||||
}
|
||||
}
|
||||
|
||||
// Isolating a node could partition the network. We don't
|
||||
// want to test such a case (because it could prevent
|
||||
// consensus). Checking for partitions would be more code
|
||||
// than its worth, so just add some links to prevent the
|
||||
// possibility of partitions.
|
||||
for i := 1; i < len(node.links); i++ {
|
||||
m.addLink(node.links[i-1].to, node.links[i].to)
|
||||
}
|
||||
}
|
||||
|
||||
// Restart a node
|
||||
func (m *Model) restart(node *TestNode) {
|
||||
node.Node = NewNode(mesh.PeerName(m.nextID),
|
||||
mesh.PeerUID(m.r.Int63()), m.quorum)
|
||||
m.nextID++
|
||||
node.Propose()
|
||||
|
||||
// The node is now ignorant, so we need to mark the links into
|
||||
// the node as ready.
|
||||
for _, l := range node.links {
|
||||
if !l.to.isolated {
|
||||
m.readyLink(l.converse)
|
||||
}
|
||||
}
|
||||
|
||||
// If a consensus was just accepted due to this node accepting
|
||||
// it, without other nodes hearing of it, and we then restart
|
||||
// this node, then a different consensus can occur later on.
|
||||
// If a tree falls with no one to hear it, does it make a
|
||||
// sound?
|
||||
node.firstConsensus = AcceptedValue{}
|
||||
}
|
||||
|
||||
func (m *Model) pickNode() *TestNode {
|
||||
for {
|
||||
node := &m.nodes[m.r.Intn(len(m.nodes))]
|
||||
if !node.isolated {
|
||||
return node
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) simulate(params *TestParams) bool {
|
||||
nodesLeft := uint(len(m.nodes))
|
||||
restarts := uint(0)
|
||||
|
||||
for step := 0; step < 1000000; step++ {
|
||||
if len(m.readyLinks) == 0 {
|
||||
// Everything has converged. This can be
|
||||
// because consensus was reached, or because
|
||||
// consensus became impossible, e.g. everyone
|
||||
// promised on a particular proposal, but then
|
||||
// the proposer restarted or was isolated. So
|
||||
// we detect the latter cases and force a new
|
||||
// proposal
|
||||
for i := range m.nodes {
|
||||
ok, _ := m.nodes[i].Consensus()
|
||||
if ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
node := m.pickNode()
|
||||
node.Propose()
|
||||
m.nodeChanged(node)
|
||||
}
|
||||
|
||||
// Pick a ready link at random
|
||||
i := m.r.Intn(len(m.readyLinks))
|
||||
link := m.readyLinks[i]
|
||||
if link.ready != i {
|
||||
m.t.Fatal("Link in readyLinks was not ready")
|
||||
}
|
||||
|
||||
// gossip across link
|
||||
node := link.to
|
||||
if node.Update(link.from.GossipState()) {
|
||||
node.Think()
|
||||
|
||||
if !node.firstConsensus.Origin.valid() {
|
||||
ok, val := node.Consensus()
|
||||
if ok {
|
||||
node.firstConsensus = val
|
||||
}
|
||||
}
|
||||
|
||||
m.nodeChanged(node)
|
||||
}
|
||||
|
||||
m.unreadyLink(link)
|
||||
|
||||
// Re-propose?
|
||||
if m.r.Float32() < params.reproposeProb {
|
||||
node := m.pickNode()
|
||||
node.Propose()
|
||||
m.nodeChanged(node)
|
||||
}
|
||||
|
||||
// Isolate?
|
||||
if nodesLeft > m.quorum && m.r.Float32() < params.isolateProb {
|
||||
m.isolateNode(m.pickNode())
|
||||
nodesLeft--
|
||||
|
||||
// We isolated a node, so get another node to
|
||||
// re-propose. In reality the lack of
|
||||
// consensus would be detected via a timeout
|
||||
node := m.pickNode()
|
||||
node.Propose()
|
||||
m.nodeChanged(node)
|
||||
}
|
||||
|
||||
// Restart?
|
||||
if restarts < m.quorum && m.r.Float32() < params.restartProb {
|
||||
restarts++
|
||||
node := m.pickNode()
|
||||
m.restart(node)
|
||||
m.nodeChanged(node)
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *Model) dump() {
|
||||
for i := range m.nodes {
|
||||
node := &m.nodes[i]
|
||||
fmt.Println(node.id)
|
||||
for n, claims := range node.knows {
|
||||
fmt.Printf("\t%d %v\n", n, claims)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate the final model state
|
||||
func (m *Model) validate() {
|
||||
var origin ProposalID
|
||||
|
||||
for i := range m.nodes {
|
||||
ok, val := m.nodes[i].Consensus()
|
||||
if !ok {
|
||||
//m.dump()
|
||||
m.t.Fatal("Node doesn't know about consensus")
|
||||
}
|
||||
|
||||
firstConsensus := m.nodes[i].firstConsensus
|
||||
if firstConsensus.Origin.valid() &&
|
||||
firstConsensus.Origin != val.Origin {
|
||||
//m.dump()
|
||||
m.t.Fatal("Consensus mismatch")
|
||||
}
|
||||
|
||||
if i == 0 {
|
||||
origin = val.Origin
|
||||
} else if val.Origin != origin {
|
||||
m.t.Fatal("Node disagrees about consensus")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSingleNode(t *testing.T) {
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
|
||||
// Test the single node case
|
||||
params := &TestParams{
|
||||
nodeCount: 1,
|
||||
connectedProb: 0,
|
||||
reproposeProb: 0,
|
||||
isolateProb: 0,
|
||||
restartProb: 0,
|
||||
}
|
||||
|
||||
m := makeRandomModel(params, r, t)
|
||||
|
||||
if !m.simulate(params) {
|
||||
m.t.Fatal("Failed to converge")
|
||||
}
|
||||
|
||||
m.validate()
|
||||
}
|
||||
|
||||
func TestPaxos(t *testing.T) {
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
|
||||
params := &TestParams{
|
||||
nodeCount: 10,
|
||||
connectedProb: 0.5,
|
||||
reproposeProb: 0.01,
|
||||
isolateProb: 0.01,
|
||||
|
||||
// Restarts cause failures due to the case where 1)
|
||||
// the gossip ordering is such that there is an
|
||||
// effective partition from the start, with a quorum
|
||||
// on one side and one node less than a quorum on the
|
||||
// other; 2) the quorum reaches a consensus; 3) a node
|
||||
// is restarted, and at the same time jumps the
|
||||
// partition, so the side that was now just less than
|
||||
// a quorum becomes a quorum; 4) the new quorum
|
||||
// reaches a contradictory consensus.
|
||||
restartProb: 0,
|
||||
}
|
||||
|
||||
for i := 0; i < 1000; i++ {
|
||||
m := makeRandomModel(params, r, t)
|
||||
|
||||
if !m.simulate(params) {
|
||||
m.t.Fatal("Failed to converge")
|
||||
}
|
||||
|
||||
m.validate()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package ring
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/weaveworks/weave/common"
|
||||
"github.com/weaveworks/weave/mesh"
|
||||
"github.com/weaveworks/weave/net/address"
|
||||
)
|
||||
|
||||
// Entry represents entries around the ring
|
||||
type entry struct {
|
||||
Token address.Address // The start of this range
|
||||
Peer mesh.PeerName // Who owns this range
|
||||
Version uint32 // Version of this range
|
||||
Free address.Offset // Number of free IPs in this range
|
||||
}
|
||||
|
||||
func (e *entry) Equal(e2 *entry) bool {
|
||||
return e.Token == e2.Token && e.Peer == e2.Peer &&
|
||||
e.Version == e2.Version
|
||||
}
|
||||
|
||||
func (e *entry) update(peername mesh.PeerName, free address.Offset) {
|
||||
e.Peer = peername
|
||||
e.Version++
|
||||
e.Free = free
|
||||
}
|
||||
|
||||
// For compatibility with sort.Interface
|
||||
type entries []*entry
|
||||
|
||||
func (es entries) Len() int { return len(es) }
|
||||
func (es entries) Less(i, j int) bool { return es[i].Token < es[j].Token }
|
||||
func (es entries) Swap(i, j int) { panic("Should never be swapping entries!") }
|
||||
|
||||
func (es entries) entry(i int) *entry {
|
||||
i = i % len(es)
|
||||
if i < 0 {
|
||||
i += len(es)
|
||||
}
|
||||
return es[i]
|
||||
}
|
||||
|
||||
func (es *entries) insert(e entry) {
|
||||
i := sort.Search(len(*es), func(j int) bool {
|
||||
return (*es)[j].Token >= e.Token
|
||||
})
|
||||
|
||||
if i < len(*es) && (*es)[i].Token == e.Token {
|
||||
panic("Trying to insert an existing token!")
|
||||
}
|
||||
|
||||
*es = append(*es, &entry{})
|
||||
copy((*es)[i+1:], (*es)[i:])
|
||||
(*es)[i] = &e
|
||||
}
|
||||
|
||||
func (es entries) get(token address.Address) (*entry, bool) {
|
||||
i := sort.Search(len(es), func(j int) bool {
|
||||
return es[j].Token >= token
|
||||
})
|
||||
|
||||
if i < len(es) && es[i].Token == token {
|
||||
return es[i], true
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Is token between entries at i and j?
|
||||
// NB i and j can overflow and will wrap
|
||||
// NBB this function does not work very well if there is only one
|
||||
// token on the ring; luckily an accurate answer is not needed
|
||||
// by the call sites in this case.
|
||||
func (es entries) between(token address.Address, i, j int) bool {
|
||||
common.Assert(i < j)
|
||||
|
||||
first := es.entry(i)
|
||||
second := es.entry(j)
|
||||
|
||||
switch {
|
||||
case first.Token == second.Token:
|
||||
// This implies there is only one token
|
||||
// on the ring (i < j and i.token == j.token)
|
||||
// In which case everything is between, expect
|
||||
// this one token
|
||||
return token != first.Token
|
||||
|
||||
case first.Token < second.Token:
|
||||
return first.Token <= token && token < second.Token
|
||||
|
||||
case second.Token < first.Token:
|
||||
return first.Token <= token || token < second.Token
|
||||
}
|
||||
|
||||
panic("Should never get here - switch covers all possibilities.")
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package ring
|
||||
|
||||
/* Exported variables for monitoring and management */
|
||||
|
||||
import (
|
||||
"expvar"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var (
|
||||
expRingSize = expvar.NewMap("ipam.ringSize")
|
||||
expRingEntries = expvar.NewMap("ipam.ringEntries")
|
||||
)
|
||||
|
||||
type _int int
|
||||
|
||||
func (i _int) String() string {
|
||||
return strconv.Itoa(int(i))
|
||||
}
|
||||
|
||||
type _uint32 uint32
|
||||
|
||||
func (i _uint32) String() string {
|
||||
return strconv.FormatUint(uint64(i), 10)
|
||||
}
|
||||
|
||||
func (r *Ring) updateExportedVariables() {
|
||||
ringName := r.Start.String()
|
||||
expRingSize.Set(ringName, _uint32(r.End-r.Start))
|
||||
expRingEntries.Set(ringName, _int(len(r.Entries)))
|
||||
}
|
||||
@@ -0,0 +1,564 @@
|
||||
/*
|
||||
Package ring implements a simple ring CRDT.
|
||||
*/
|
||||
package ring
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/weaveworks/weave/common"
|
||||
"github.com/weaveworks/weave/mesh"
|
||||
"github.com/weaveworks/weave/net/address"
|
||||
)
|
||||
|
||||
// Ring represents the ring itself
|
||||
type Ring struct {
|
||||
Start, End address.Address // [min, max) tokens in this ring. Due to wrapping, min == max (effectively)
|
||||
Peer mesh.PeerName // name of peer owning this ring instance
|
||||
Entries entries // list of entries sorted by token
|
||||
Seeds []mesh.PeerName // peers with which the ring was seeded
|
||||
}
|
||||
|
||||
func (r *Ring) assertInvariants() {
|
||||
err := r.checkInvariants()
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Errors returned by Merge
|
||||
var (
|
||||
ErrNotSorted = errors.New("Ring not sorted")
|
||||
ErrTokenRepeated = errors.New("Token appears twice in ring")
|
||||
ErrTokenOutOfRange = errors.New("Token is out of range")
|
||||
ErrDifferentSeeds = errors.New("Received ring was seeded differently from ours!")
|
||||
ErrDifferentRange = errors.New("Received range differs from ours!")
|
||||
ErrNewerVersion = errors.New("Received new version for entry I own!")
|
||||
ErrInvalidEntry = errors.New("Received invalid state update!")
|
||||
ErrEntryInMyRange = errors.New("Received new entry in my range!")
|
||||
ErrNotFound = errors.New("No entries for peer found")
|
||||
)
|
||||
|
||||
func (r *Ring) checkInvariants() error {
|
||||
if !sort.IsSorted(r.Entries) {
|
||||
return ErrNotSorted
|
||||
}
|
||||
|
||||
// Check no token appears twice
|
||||
// We know it's sorted...
|
||||
for i := 1; i < len(r.Entries); i++ {
|
||||
if r.Entries[i-1].Token == r.Entries[i].Token {
|
||||
return ErrTokenRepeated
|
||||
}
|
||||
}
|
||||
|
||||
if len(r.Entries) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check tokens are in range
|
||||
if r.Entries.entry(0).Token < r.Start {
|
||||
return ErrTokenOutOfRange
|
||||
}
|
||||
if r.Entries.entry(-1).Token >= r.End {
|
||||
return ErrTokenOutOfRange
|
||||
}
|
||||
|
||||
// Check all the freespaces are in range
|
||||
for i, entry := range r.Entries {
|
||||
next := r.Entries.entry(i + 1)
|
||||
distance := r.distance(entry.Token, next.Token)
|
||||
|
||||
if entry.Free > distance {
|
||||
return fmt.Errorf("Entry %s-%s reporting too much free space: %d > %d", entry.Token, next.Token, entry.Free, distance)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// New creates an empty ring belonging to peer.
|
||||
func New(start, end address.Address, peer mesh.PeerName) *Ring {
|
||||
common.Assert(start < end)
|
||||
|
||||
ring := &Ring{Start: start, End: end, Peer: peer, Entries: make([]*entry, 0)}
|
||||
ring.updateExportedVariables()
|
||||
return ring
|
||||
}
|
||||
|
||||
func (r *Ring) Range() address.Range {
|
||||
return address.Range{Start: r.Start, End: r.End}
|
||||
}
|
||||
|
||||
// Returns the distance between two tokens on this ring, dealing
|
||||
// with ranges which cross the origin
|
||||
func (r *Ring) distance(start, end address.Address) address.Offset {
|
||||
if end > start {
|
||||
return address.Offset(end - start)
|
||||
}
|
||||
|
||||
return address.Offset((r.End - start) + (end - r.Start))
|
||||
}
|
||||
|
||||
// GrantRangeToHost modifies the ring such that range [start, end)
|
||||
// is assigned to peer. This may insert up to two new tokens.
|
||||
// Preconditions:
|
||||
// - start < end
|
||||
// - [start, end) must be owned by the calling peer
|
||||
func (r *Ring) GrantRangeToHost(start, end address.Address, peer mesh.PeerName) {
|
||||
//fmt.Printf("%s GrantRangeToHost [%v,%v) -> %s\n", r.Peer, start, end, peer)
|
||||
|
||||
r.assertInvariants()
|
||||
defer r.assertInvariants()
|
||||
defer r.updateExportedVariables()
|
||||
|
||||
// ----------------- Start of Checks -----------------
|
||||
|
||||
common.Assert(start < end)
|
||||
common.Assert(r.Start <= start && start < r.End)
|
||||
common.Assert(r.Start < end && end <= r.End)
|
||||
common.Assert(len(r.Entries) > 0)
|
||||
|
||||
// Look for the left-most entry greater than start, then go one previous
|
||||
// to get the right-most entry less than or equal to start
|
||||
preceedingPos := sort.Search(len(r.Entries), func(j int) bool {
|
||||
return r.Entries[j].Token > start
|
||||
})
|
||||
preceedingPos--
|
||||
|
||||
// Check all tokens up to end are owned by us
|
||||
for pos := preceedingPos; pos < len(r.Entries) && r.Entries.entry(pos).Token < end; pos++ {
|
||||
common.Assert(r.Entries.entry(pos).Peer == r.Peer)
|
||||
}
|
||||
|
||||
// ----------------- End of Checks -----------------
|
||||
|
||||
// Free space at start is max(length of range, distance to next token)
|
||||
startFree := r.distance(start, r.Entries.entry(preceedingPos+1).Token)
|
||||
if length := r.distance(start, end); startFree > length {
|
||||
startFree = length
|
||||
}
|
||||
// Is there already a token at start, update it
|
||||
if previousEntry := r.Entries.entry(preceedingPos); previousEntry.Token == start {
|
||||
previousEntry.update(peer, startFree)
|
||||
} else {
|
||||
// Otherwise, these isn't a token here, insert a new one.
|
||||
r.Entries.insert(entry{Token: start, Peer: peer, Free: startFree})
|
||||
preceedingPos++
|
||||
// Reset free space on previous entry, which we own.
|
||||
previousEntry.update(r.Peer, r.distance(previousEntry.Token, start))
|
||||
}
|
||||
|
||||
// Give all intervening tokens to the other peer
|
||||
pos := preceedingPos + 1
|
||||
for ; pos < len(r.Entries) && r.Entries.entry(pos).Token < end; pos++ {
|
||||
entry := r.Entries.entry(pos)
|
||||
entry.update(peer, address.Min(entry.Free, r.distance(entry.Token, end)))
|
||||
}
|
||||
|
||||
// There is never an entry with a token of r.End, as the end of
|
||||
// the ring is exclusive.
|
||||
if end == r.End {
|
||||
end = r.Start
|
||||
}
|
||||
|
||||
// If there is a token equal to the end of the range, we don't need to do anything further
|
||||
if _, found := r.Entries.get(end); found {
|
||||
return
|
||||
}
|
||||
|
||||
// If not, we need to insert a token such that we claim this bit on the end.
|
||||
endFree := r.distance(end, r.Entries.entry(pos).Token)
|
||||
r.Entries.insert(entry{Token: end, Peer: r.Peer, Free: endFree})
|
||||
}
|
||||
|
||||
// Merge the given ring into this ring and return any new ranges added
|
||||
func (r *Ring) Merge(gossip Ring) error {
|
||||
r.assertInvariants()
|
||||
defer r.assertInvariants()
|
||||
defer r.updateExportedVariables()
|
||||
|
||||
// Don't panic when checking the gossiped in ring.
|
||||
// In this case just return any error found.
|
||||
if err := gossip.checkInvariants(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(gossip.Seeds) > 0 && len(r.Seeds) > 0 {
|
||||
if len(gossip.Seeds) != len(r.Seeds) {
|
||||
return ErrDifferentSeeds
|
||||
}
|
||||
for i, seed := range gossip.Seeds {
|
||||
if seed != r.Seeds[i] {
|
||||
return ErrDifferentSeeds
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if r.Start != gossip.Start || r.End != gossip.End {
|
||||
return ErrDifferentRange
|
||||
}
|
||||
|
||||
// Now merge their ring with yours, in a temporary ring.
|
||||
var result entries
|
||||
addToResult := func(e entry) { result = append(result, &e) }
|
||||
|
||||
var mine, theirs *entry
|
||||
var previousOwner *mesh.PeerName
|
||||
// i is index into r.Entries; j is index into gossip.Entries
|
||||
var i, j int
|
||||
for i < len(r.Entries) && j < len(gossip.Entries) {
|
||||
mine, theirs = r.Entries[i], gossip.Entries[j]
|
||||
switch {
|
||||
case mine.Token < theirs.Token:
|
||||
addToResult(*mine)
|
||||
previousOwner = &mine.Peer
|
||||
i++
|
||||
case mine.Token > theirs.Token:
|
||||
// insert, checking that a range owned by us hasn't been split
|
||||
if previousOwner != nil && *previousOwner == r.Peer && theirs.Peer != r.Peer {
|
||||
return ErrEntryInMyRange
|
||||
}
|
||||
addToResult(*theirs)
|
||||
previousOwner = nil
|
||||
j++
|
||||
case mine.Token == theirs.Token:
|
||||
// merge
|
||||
switch {
|
||||
case mine.Version >= theirs.Version:
|
||||
if mine.Version == theirs.Version && !mine.Equal(theirs) {
|
||||
common.Log.Debugf("Error merging entries at %s - %v != %v", mine.Token, mine, theirs)
|
||||
return ErrInvalidEntry
|
||||
}
|
||||
addToResult(*mine)
|
||||
previousOwner = &mine.Peer
|
||||
case mine.Version < theirs.Version:
|
||||
if mine.Peer == r.Peer { // We shouldn't receive updates to our own tokens
|
||||
return ErrNewerVersion
|
||||
}
|
||||
addToResult(*theirs)
|
||||
previousOwner = nil
|
||||
}
|
||||
i++
|
||||
j++
|
||||
}
|
||||
}
|
||||
|
||||
// At this point, either i is at the end of r or j is at the end
|
||||
// of gossip, so copy over the remaining entries.
|
||||
|
||||
for ; i < len(r.Entries); i++ {
|
||||
mine = r.Entries[i]
|
||||
addToResult(*mine)
|
||||
}
|
||||
|
||||
for ; j < len(gossip.Entries); j++ {
|
||||
theirs = gossip.Entries[j]
|
||||
if previousOwner != nil && *previousOwner == r.Peer && theirs.Peer != r.Peer {
|
||||
return ErrEntryInMyRange
|
||||
}
|
||||
addToResult(*theirs)
|
||||
previousOwner = nil
|
||||
}
|
||||
|
||||
if len(r.Seeds) == 0 {
|
||||
r.Seeds = gossip.Seeds
|
||||
}
|
||||
r.Entries = result
|
||||
return nil
|
||||
}
|
||||
|
||||
// Empty returns true if the ring has no entries
|
||||
func (r *Ring) Empty() bool {
|
||||
return len(r.Entries) == 0
|
||||
}
|
||||
|
||||
// Given a slice of ranges which are all in the right order except
|
||||
// possibly the last one spans zero, fix that up and return the slice
|
||||
func (r *Ring) splitRangesOverZero(ranges []address.Range) []address.Range {
|
||||
if len(ranges) == 0 {
|
||||
return ranges
|
||||
}
|
||||
lastRange := ranges[len(ranges)-1]
|
||||
// if end token == start (ie last) entry on ring, we want to actually use r.End
|
||||
if lastRange.End == r.Start {
|
||||
ranges[len(ranges)-1].End = r.End
|
||||
} else if lastRange.End <= lastRange.Start {
|
||||
// We wrapped; want to split around 0
|
||||
// First shuffle everything up as we want results to be sorted
|
||||
ranges = append(ranges, address.Range{})
|
||||
copy(ranges[1:], ranges[:len(ranges)-1])
|
||||
ranges[0] = address.Range{Start: r.Start, End: lastRange.End}
|
||||
ranges[len(ranges)-1].End = r.End
|
||||
}
|
||||
return ranges
|
||||
}
|
||||
|
||||
// OwnedRanges returns slice of Ranges, ordered by IP, indicating which
|
||||
// ranges are owned by this peer. Will split ranges which
|
||||
// span 0 in the ring.
|
||||
func (r *Ring) OwnedRanges() (result []address.Range) {
|
||||
r.assertInvariants()
|
||||
|
||||
for i, entry := range r.Entries {
|
||||
if entry.Peer == r.Peer {
|
||||
nextEntry := r.Entries.entry(i + 1)
|
||||
result = append(result, address.Range{Start: entry.Token, End: nextEntry.Token})
|
||||
}
|
||||
}
|
||||
|
||||
return r.splitRangesOverZero(result)
|
||||
}
|
||||
|
||||
// For printing status
|
||||
type RangeInfo struct {
|
||||
Peer mesh.PeerName
|
||||
address.Range
|
||||
Version uint32
|
||||
}
|
||||
|
||||
func (r *Ring) AllRangeInfo() (result []RangeInfo) {
|
||||
for i, entry := range r.Entries {
|
||||
nextEntry := r.Entries.entry(i + 1)
|
||||
ranges := []address.Range{{Start: entry.Token, End: nextEntry.Token}}
|
||||
ranges = r.splitRangesOverZero(ranges)
|
||||
for _, r := range ranges {
|
||||
result = append(result, RangeInfo{entry.Peer, r, entry.Version})
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ClaimForPeers claims the entire ring for the array of peers passed
|
||||
// in. Only works for empty rings.
|
||||
func (r *Ring) ClaimForPeers(peers []mesh.PeerName) {
|
||||
common.Assert(r.Empty())
|
||||
defer r.assertInvariants()
|
||||
defer r.updateExportedVariables()
|
||||
|
||||
totalSize := r.distance(r.Start, r.End)
|
||||
share := totalSize/address.Offset(len(peers)) + 1
|
||||
remainder := totalSize % address.Offset(len(peers))
|
||||
pos := r.Start
|
||||
|
||||
for i, peer := range peers {
|
||||
if address.Offset(i) == remainder {
|
||||
share--
|
||||
if share == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if e, found := r.Entries.get(pos); found {
|
||||
e.update(peer, share)
|
||||
} else {
|
||||
r.Entries.insert(entry{Token: pos, Peer: peer, Free: share})
|
||||
}
|
||||
|
||||
pos += address.Address(share)
|
||||
}
|
||||
|
||||
common.Assert(pos == r.End)
|
||||
|
||||
r.Seeds = peers
|
||||
}
|
||||
|
||||
func (r *Ring) FprintWithNicknames(w io.Writer, m map[mesh.PeerName]string) {
|
||||
for _, entry := range r.Entries {
|
||||
nickname, found := m[entry.Peer]
|
||||
if found {
|
||||
nickname = fmt.Sprintf(" (%s)", nickname)
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "\n %s -> %s%s (v%d)", entry.Token,
|
||||
entry.Peer, nickname, entry.Version)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Ring) String() string {
|
||||
var buffer bytes.Buffer
|
||||
fmt.Fprintf(&buffer, "Ring [%s, %s)", r.Start, r.End)
|
||||
r.FprintWithNicknames(&buffer, make(map[mesh.PeerName]string))
|
||||
return buffer.String()
|
||||
}
|
||||
|
||||
// ReportFree is used by the allocator to tell the ring how many free
|
||||
// ips are in a given range, so that ChoosePeersToAskForSpace can make
|
||||
// more intelligent decisions.
|
||||
func (r *Ring) ReportFree(freespace map[address.Address]address.Offset) {
|
||||
r.assertInvariants()
|
||||
defer r.assertInvariants()
|
||||
defer r.updateExportedVariables()
|
||||
|
||||
common.Assert(!r.Empty())
|
||||
entries := r.Entries
|
||||
|
||||
// As OwnedRanges splits around the origin, we need to
|
||||
// detect that here and fix up freespace
|
||||
if free, found := freespace[r.Start]; found && entries.entry(0).Token != r.Start {
|
||||
lastToken := entries.entry(-1).Token
|
||||
prevFree, found := freespace[lastToken]
|
||||
common.Assert(found)
|
||||
freespace[lastToken] = prevFree + free
|
||||
delete(freespace, r.Start)
|
||||
}
|
||||
|
||||
for start, free := range freespace {
|
||||
// Look for entry
|
||||
i := sort.Search(len(entries), func(j int) bool {
|
||||
return entries[j].Token >= start
|
||||
})
|
||||
|
||||
// Are you trying to report free on space I don't own?
|
||||
common.Assert(i < len(entries) && entries[i].Token == start && entries[i].Peer == r.Peer)
|
||||
|
||||
// Check we're not reporting more space than the range
|
||||
entry, next := entries.entry(i), entries.entry(i+1)
|
||||
maxSize := r.distance(entry.Token, next.Token)
|
||||
common.Assert(free <= maxSize)
|
||||
|
||||
if entries[i].Free == free {
|
||||
return
|
||||
}
|
||||
|
||||
entries[i].Free = free
|
||||
entries[i].Version++
|
||||
}
|
||||
}
|
||||
|
||||
type weightedPeer struct {
|
||||
weight float64
|
||||
peername mesh.PeerName
|
||||
}
|
||||
type weightedPeers []weightedPeer
|
||||
|
||||
// Note Less is using > so that bigger weights sort earlier
|
||||
func (ws weightedPeers) Less(i, j int) bool { return ws[i].weight > ws[j].weight }
|
||||
func (ws weightedPeers) Len() int { return len(ws) }
|
||||
func (ws weightedPeers) Swap(i, j int) { ws[i], ws[j] = ws[j], ws[i] }
|
||||
|
||||
// ChoosePeersToAskForSpace returns all peers we can ask for space in
|
||||
// the range [start, end), in weighted-random order. Assumes start<end.
|
||||
func (r *Ring) ChoosePeersToAskForSpace(start, end address.Address) []mesh.PeerName {
|
||||
var (
|
||||
sum address.Offset
|
||||
totalSpacePerPeer = make(map[mesh.PeerName]address.Offset) // Compute total free space per peer
|
||||
)
|
||||
|
||||
// iterate through tokens
|
||||
for i, entry := range r.Entries {
|
||||
// Ignore entries that don't span the range we want
|
||||
if i+1 < len(r.Entries) && r.Entries.entry(i+1).Token-1 < start {
|
||||
continue
|
||||
}
|
||||
if entry.Token >= end {
|
||||
break
|
||||
}
|
||||
// Ignore ranges with no free space
|
||||
if entry.Free <= 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Don't talk to yourself
|
||||
if entry.Peer == r.Peer {
|
||||
continue
|
||||
}
|
||||
|
||||
totalSpacePerPeer[entry.Peer] += entry.Free
|
||||
sum += entry.Free
|
||||
}
|
||||
|
||||
// Compute weighted random numbers, then sort.
|
||||
// This isn't perfect, e.g. an item with weight 2 will get chosen more than
|
||||
// twice as often as an item with weight 1, but it's good enough for our purposes.
|
||||
ws := make(weightedPeers, 0, len(totalSpacePerPeer))
|
||||
for peername, space := range totalSpacePerPeer {
|
||||
ws = append(ws, weightedPeer{weight: float64(space) * rand.Float64(), peername: peername})
|
||||
}
|
||||
sort.Sort(ws)
|
||||
result := make([]mesh.PeerName, len(ws))
|
||||
for i, wp := range ws {
|
||||
result[i] = wp.peername
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (r *Ring) PickPeerForTransfer(isValid func(mesh.PeerName) bool) mesh.PeerName {
|
||||
for _, entry := range r.Entries {
|
||||
if entry.Peer != r.Peer && isValid(entry.Peer) {
|
||||
return entry.Peer
|
||||
}
|
||||
}
|
||||
return mesh.UnknownPeerName
|
||||
}
|
||||
|
||||
// Transfer will mark all entries associated with 'from' peer as owned by 'to' peer
|
||||
// and return ranges indicating the new space we picked up
|
||||
func (r *Ring) Transfer(from, to mesh.PeerName) ([]address.Range, error) {
|
||||
r.assertInvariants()
|
||||
defer r.assertInvariants()
|
||||
defer r.updateExportedVariables()
|
||||
|
||||
var newRanges []address.Range
|
||||
found := false
|
||||
|
||||
for i, entry := range r.Entries {
|
||||
if entry.Peer == from {
|
||||
found = true
|
||||
entry.Peer = to
|
||||
entry.Version++
|
||||
newRanges = append(newRanges, address.Range{Start: entry.Token, End: r.Entries.entry(i + 1).Token})
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
|
||||
return r.splitRangesOverZero(newRanges), nil
|
||||
}
|
||||
|
||||
// Contains returns true if addr is in this ring
|
||||
func (r *Ring) Contains(addr address.Address) bool {
|
||||
return addr >= r.Start && addr < r.End
|
||||
}
|
||||
|
||||
// Owner returns the peername which owns the range containing addr
|
||||
func (r *Ring) Owner(token address.Address) mesh.PeerName {
|
||||
common.Assert(r.Start <= token && token < r.End)
|
||||
|
||||
r.assertInvariants()
|
||||
// There can be no owners on an empty ring
|
||||
if r.Empty() {
|
||||
return mesh.UnknownPeerName
|
||||
}
|
||||
|
||||
// Look for the right-most entry, less than or equal to token
|
||||
preceedingEntry := sort.Search(len(r.Entries), func(j int) bool {
|
||||
return r.Entries[j].Token > token
|
||||
})
|
||||
preceedingEntry--
|
||||
entry := r.Entries.entry(preceedingEntry)
|
||||
return entry.Peer
|
||||
}
|
||||
|
||||
// Get the set of PeerNames mentioned in the ring
|
||||
func (r *Ring) PeerNames() map[mesh.PeerName]struct{} {
|
||||
res := make(map[mesh.PeerName]struct{})
|
||||
|
||||
for _, entry := range r.Entries {
|
||||
res[entry.Peer] = struct{}{}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().UTC().UnixNano())
|
||||
}
|
||||
@@ -0,0 +1,746 @@
|
||||
package ring
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/weaveworks/weave/common"
|
||||
"github.com/weaveworks/weave/mesh"
|
||||
"github.com/weaveworks/weave/net/address"
|
||||
)
|
||||
|
||||
var (
|
||||
peer1name, _ = mesh.PeerNameFromString("01:00:00:00:02:00")
|
||||
peer2name, _ = mesh.PeerNameFromString("02:00:00:00:02:00")
|
||||
peer3name, _ = mesh.PeerNameFromString("03:00:00:00:02:00")
|
||||
|
||||
start, end = ParseIP("10.0.0.0"), ParseIP("10.0.0.255")
|
||||
dot10, dot245 = ParseIP("10.0.0.10"), ParseIP("10.0.0.245")
|
||||
dot250 = ParseIP("10.0.0.250")
|
||||
middle = ParseIP("10.0.0.128")
|
||||
)
|
||||
|
||||
func ParseIP(s string) address.Address {
|
||||
addr, _ := address.ParseIP(s)
|
||||
return addr
|
||||
}
|
||||
|
||||
func TestInvariants(t *testing.T) {
|
||||
ring := New(start, end, peer1name)
|
||||
|
||||
// Check ring is sorted
|
||||
ring.Entries = []*entry{{Token: dot245, Peer: peer1name}, {Token: dot10, Peer: peer2name}}
|
||||
require.True(t, ring.checkInvariants() == ErrNotSorted, "Expected error")
|
||||
|
||||
// Check tokens don't appear twice
|
||||
ring.Entries = []*entry{{Token: dot245, Peer: peer1name}, {Token: dot245, Peer: peer2name}}
|
||||
require.True(t, ring.checkInvariants() == ErrTokenRepeated, "Expected error")
|
||||
|
||||
// Check tokens are in bounds
|
||||
ring = New(dot10, dot245, peer1name)
|
||||
ring.Entries = []*entry{{Token: start, Peer: peer1name}}
|
||||
require.True(t, ring.checkInvariants() == ErrTokenOutOfRange, "Expected error")
|
||||
|
||||
ring.Entries = []*entry{{Token: end, Peer: peer1name}}
|
||||
require.True(t, ring.checkInvariants() == ErrTokenOutOfRange, "Expected error")
|
||||
}
|
||||
|
||||
func TestInsert(t *testing.T) {
|
||||
ring := New(start, end, peer1name)
|
||||
ring.Entries = []*entry{{Token: start, Peer: peer1name, Free: 255}}
|
||||
|
||||
require.Panics(t, func() {
|
||||
ring.Entries.insert(entry{Token: start, Peer: peer1name})
|
||||
})
|
||||
|
||||
ring.Entries.entry(0).Free = 0
|
||||
ring.Entries.insert(entry{Token: dot245, Peer: peer1name})
|
||||
ring2 := New(start, end, peer1name)
|
||||
ring2.Entries = []*entry{{Token: start, Peer: peer1name, Free: 0}, {Token: dot245, Peer: peer1name}}
|
||||
require.Equal(t, ring2, ring)
|
||||
|
||||
ring.Entries.insert(entry{Token: dot10, Peer: peer1name})
|
||||
ring2.Entries = []*entry{{Token: start, Peer: peer1name, Free: 0}, {Token: dot10, Peer: peer1name}, {Token: dot245, Peer: peer1name}}
|
||||
require.Equal(t, ring2, ring)
|
||||
}
|
||||
|
||||
func TestBetween(t *testing.T) {
|
||||
ring1 := New(start, end, peer1name)
|
||||
ring1.Entries = []*entry{{Token: start, Peer: peer1name, Free: 255}}
|
||||
|
||||
// First off, in a ring where everything is owned by the peer
|
||||
// between should return true for everything
|
||||
for i := 1; i <= 255; i++ {
|
||||
ip := ParseIP(fmt.Sprintf("10.0.0.%d", i))
|
||||
require.True(t, ring1.Entries.between(ip, 0, 1), "between should be true!")
|
||||
}
|
||||
|
||||
// Now, construct a ring with entries at +10 and -10
|
||||
// And check the correct behaviour
|
||||
|
||||
ring1.Entries = []*entry{{Token: dot10, Peer: peer1name}, {Token: dot245, Peer: peer2name}}
|
||||
ring1.assertInvariants()
|
||||
for i := 10; i <= 244; i++ {
|
||||
ipStr := fmt.Sprintf("10.0.0.%d", i)
|
||||
ip := ParseIP(ipStr)
|
||||
require.True(t, ring1.Entries.between(ip, 0, 1),
|
||||
fmt.Sprintf("Between should be true for %s!", ipStr))
|
||||
require.False(t, ring1.Entries.between(ip, 1, 2),
|
||||
fmt.Sprintf("Between should be false for %s!", ipStr))
|
||||
}
|
||||
for i := 0; i <= 9; i++ {
|
||||
ipStr := fmt.Sprintf("10.0.0.%d", i)
|
||||
ip := ParseIP(ipStr)
|
||||
require.False(t, ring1.Entries.between(ip, 0, 1),
|
||||
fmt.Sprintf("Between should be false for %s!", ipStr))
|
||||
require.True(t, ring1.Entries.between(ip, 1, 2),
|
||||
fmt.Sprintf("Between should be true for %s!", ipStr))
|
||||
}
|
||||
for i := 245; i <= 255; i++ {
|
||||
ipStr := fmt.Sprintf("10.0.0.%d", i)
|
||||
ip := ParseIP(ipStr)
|
||||
require.False(t, ring1.Entries.between(ip, 0, 1),
|
||||
fmt.Sprintf("Between should be false for %s!", ipStr))
|
||||
require.True(t, ring1.Entries.between(ip, 1, 2),
|
||||
fmt.Sprintf("Between should be true for %s!", ipStr))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGrantSimple(t *testing.T) {
|
||||
ring1 := New(start, end, peer1name)
|
||||
ring2 := New(start, end, peer2name)
|
||||
|
||||
// Claim everything for peer1
|
||||
ring1.ClaimItAll()
|
||||
require.Equal(t, entries{{Token: start, Peer: peer1name, Free: 255}}, ring1.Entries)
|
||||
|
||||
// Now grant everything to peer2
|
||||
ring1.GrantRangeToHost(start, end, peer2name)
|
||||
ring2.Entries = []*entry{{Token: start, Peer: peer2name, Free: 255, Version: 1}}
|
||||
require.Equal(t, ring2.Entries, ring1.Entries)
|
||||
|
||||
// Now spint back to peer 1
|
||||
ring2.GrantRangeToHost(dot10, end, peer1name)
|
||||
ring1.Entries = []*entry{{Token: start, Peer: peer2name, Free: 10, Version: 2},
|
||||
{Token: dot10, Peer: peer1name, Free: 245}}
|
||||
require.Equal(t, ring2.Entries, ring1.Entries)
|
||||
|
||||
// And spint back to peer 2 again
|
||||
ring1.GrantRangeToHost(dot245, end, peer2name)
|
||||
require.Equal(t, entries{{Token: start, Peer: peer2name, Free: 10, Version: 2},
|
||||
{Token: dot10, Peer: peer1name, Free: 235, Version: 1},
|
||||
{Token: dot245, Peer: peer2name, Free: 10}}, ring1.Entries)
|
||||
|
||||
// Grant range spanning a live token
|
||||
ring1.Entries = []*entry{{Token: start, Peer: peer1name, Free: 10, Version: 2},
|
||||
{Token: dot10, Peer: peer1name, Free: 235}, {Token: dot245, Peer: peer1name, Free: 10}}
|
||||
ring1.GrantRangeToHost(dot10, end, peer2name)
|
||||
require.Equal(t, entries{{Token: start, Peer: peer1name, Free: 10, Version: 2},
|
||||
{Token: dot10, Peer: peer2name, Free: 235, Version: 1},
|
||||
{Token: dot245, Peer: peer2name, Free: 10, Version: 1}}, ring1.Entries)
|
||||
|
||||
}
|
||||
|
||||
func TestGrantSplit(t *testing.T) {
|
||||
ring1 := New(start, end, peer1name)
|
||||
ring2 := New(start, end, peer2name)
|
||||
|
||||
// Claim everything for peer1
|
||||
ring1.Entries = []*entry{{Token: start, Peer: peer1name, Free: 255}}
|
||||
ring2.Merge(*ring1)
|
||||
require.Equal(t, ring2.Entries, ring1.Entries)
|
||||
|
||||
// Now grant a split range to peer2
|
||||
ring1.GrantRangeToHost(dot10, dot245, peer2name)
|
||||
require.Equal(t, entries{{Token: start, Peer: peer1name, Free: 10, Version: 1},
|
||||
{Token: dot10, Peer: peer2name, Free: 235},
|
||||
{Token: dot245, Peer: peer1name, Free: 10}}, ring1.Entries)
|
||||
|
||||
ring1.assertInvariants()
|
||||
|
||||
// Grant range spanning a live token, and inserting a new token
|
||||
ring1.Entries = []*entry{{Token: start, Peer: peer1name, Free: 10, Version: 2},
|
||||
{Token: dot10, Peer: peer1name, Free: 118}, {Token: middle, Peer: peer1name, Free: 127}}
|
||||
ring1.GrantRangeToHost(dot10, dot245, peer2name)
|
||||
require.Equal(t, entries{{Token: start, Peer: peer1name, Free: 10, Version: 2},
|
||||
{Token: dot10, Peer: peer2name, Free: 118, Version: 1},
|
||||
{Token: middle, Peer: peer2name, Free: 117, Version: 1},
|
||||
{Token: dot245, Peer: peer1name, Free: 10, Version: 0}}, ring1.Entries)
|
||||
|
||||
ring1.assertInvariants()
|
||||
}
|
||||
|
||||
func TestMergeSimple(t *testing.T) {
|
||||
ring1 := New(start, end, peer1name)
|
||||
ring2 := New(start, end, peer2name)
|
||||
|
||||
// Claim everything for peer1
|
||||
ring1.ClaimItAll()
|
||||
ring1.GrantRangeToHost(middle, end, peer2name)
|
||||
require.NoError(t, ring2.Merge(*ring1))
|
||||
|
||||
require.Equal(t, entries{{Token: start, Peer: peer1name, Free: 128, Version: 1},
|
||||
{Token: middle, Peer: peer2name, Free: 127}}, ring1.Entries)
|
||||
|
||||
require.Equal(t, ring2.Entries, ring1.Entries)
|
||||
|
||||
// Now to two different operations on either side,
|
||||
// check we can Merge again
|
||||
ring1.GrantRangeToHost(start, middle, peer2name)
|
||||
ring2.GrantRangeToHost(middle, end, peer1name)
|
||||
|
||||
require.NoError(t, ring2.Merge(*ring1))
|
||||
require.NoError(t, ring1.Merge(*ring2))
|
||||
|
||||
require.Equal(t, entries{{Token: start, Peer: peer2name, Free: 128, Version: 2},
|
||||
{Token: middle, Peer: peer1name, Free: 127, Version: 1}}, ring1.Entries)
|
||||
|
||||
require.Equal(t, ring2.Entries, ring1.Entries)
|
||||
}
|
||||
|
||||
func TestMergeErrors(t *testing.T) {
|
||||
// Cannot Merge in an invalid ring
|
||||
ring1 := New(start, end, peer1name)
|
||||
ring2 := New(start, end, peer2name)
|
||||
ring2.Entries = []*entry{{Token: middle, Peer: peer2name}, {Token: start, Peer: peer2name}}
|
||||
require.True(t, ring1.Merge(*ring2) == ErrNotSorted, "Expected ErrNotSorted")
|
||||
|
||||
// Should Merge two rings for different ranges
|
||||
ring2 = New(start, middle, peer2name)
|
||||
ring2.Entries = []*entry{}
|
||||
require.True(t, ring1.Merge(*ring2) == ErrDifferentRange, "Expected ErrDifferentRange")
|
||||
|
||||
// Cannot Merge newer version of entry I own
|
||||
ring2 = New(start, end, peer2name)
|
||||
ring1.Entries = []*entry{{Token: start, Peer: peer1name}}
|
||||
ring2.Entries = []*entry{{Token: start, Peer: peer1name, Version: 1}}
|
||||
require.True(t, ring1.Merge(*ring2) == ErrNewerVersion, "Expected ErrNewerVersion")
|
||||
|
||||
// Cannot Merge two entries with same version but different hosts
|
||||
ring1.Entries = []*entry{{Token: start, Peer: peer1name}}
|
||||
ring2.Entries = []*entry{{Token: start, Peer: peer2name}}
|
||||
require.True(t, ring1.Merge(*ring2) == ErrInvalidEntry, "Expected ErrInvalidEntry")
|
||||
|
||||
// Cannot Merge an entry into a range I own
|
||||
ring1.Entries = []*entry{{Token: start, Peer: peer1name}}
|
||||
ring2.Entries = []*entry{{Token: middle, Peer: peer2name}}
|
||||
require.True(t, ring1.Merge(*ring2) == ErrEntryInMyRange, "Expected ErrEntryInMyRange")
|
||||
}
|
||||
|
||||
func TestMergeMore(t *testing.T) {
|
||||
ring1 := New(start, end, peer1name)
|
||||
ring2 := New(start, end, peer2name)
|
||||
|
||||
assertRing := func(ring *Ring, entries entries) {
|
||||
require.Equal(t, entries, ring.Entries)
|
||||
}
|
||||
|
||||
assertRing(ring1, []*entry{})
|
||||
assertRing(ring2, []*entry{})
|
||||
|
||||
// Claim everything for peer1
|
||||
ring1.ClaimItAll()
|
||||
assertRing(ring1, []*entry{{Token: start, Peer: peer1name, Free: 255}})
|
||||
assertRing(ring2, []*entry{})
|
||||
|
||||
// Check the Merge sends it to the other ring
|
||||
require.NoError(t, ring2.Merge(*ring1))
|
||||
assertRing(ring1, []*entry{{Token: start, Peer: peer1name, Free: 255}})
|
||||
assertRing(ring2, []*entry{{Token: start, Peer: peer1name, Free: 255}})
|
||||
|
||||
// Give everything to peer2
|
||||
ring1.GrantRangeToHost(start, end, peer2name)
|
||||
assertRing(ring1, []*entry{{Token: start, Peer: peer2name, Free: 255, Version: 1}})
|
||||
assertRing(ring2, []*entry{{Token: start, Peer: peer1name, Free: 255}})
|
||||
|
||||
require.NoError(t, ring2.Merge(*ring1))
|
||||
assertRing(ring1, []*entry{{Token: start, Peer: peer2name, Free: 255, Version: 1}})
|
||||
assertRing(ring2, []*entry{{Token: start, Peer: peer2name, Free: 255, Version: 1}})
|
||||
|
||||
// And carve off some space
|
||||
ring2.GrantRangeToHost(middle, end, peer1name)
|
||||
assertRing(ring2, []*entry{{Token: start, Peer: peer2name, Free: 128, Version: 2},
|
||||
{Token: middle, Peer: peer1name, Free: 127}})
|
||||
assertRing(ring1, []*entry{{Token: start, Peer: peer2name, Free: 255, Version: 1}})
|
||||
|
||||
// And Merge back
|
||||
require.NoError(t, ring1.Merge(*ring2))
|
||||
assertRing(ring1, []*entry{{Token: start, Peer: peer2name, Free: 128, Version: 2},
|
||||
{Token: middle, Peer: peer1name, Free: 127}})
|
||||
assertRing(ring2, []*entry{{Token: start, Peer: peer2name, Free: 128, Version: 2},
|
||||
{Token: middle, Peer: peer1name, Free: 127}})
|
||||
|
||||
// This should be a no-op
|
||||
require.NoError(t, ring2.Merge(*ring1))
|
||||
assertRing(ring1, []*entry{{Token: start, Peer: peer2name, Free: 128, Version: 2},
|
||||
{Token: middle, Peer: peer1name, Free: 127}})
|
||||
assertRing(ring2, []*entry{{Token: start, Peer: peer2name, Free: 128, Version: 2},
|
||||
{Token: middle, Peer: peer1name, Free: 127}})
|
||||
}
|
||||
|
||||
func TestMergeSplit(t *testing.T) {
|
||||
ring1 := New(start, end, peer1name)
|
||||
ring2 := New(start, end, peer2name)
|
||||
|
||||
// Claim everything for peer2
|
||||
ring1.Entries = []*entry{{Token: start, Peer: peer2name, Free: 255}}
|
||||
require.NoError(t, ring2.Merge(*ring1))
|
||||
require.Equal(t, ring2.Entries, ring1.Entries)
|
||||
|
||||
// Now grant a split range to peer1
|
||||
ring2.GrantRangeToHost(dot10, dot245, peer1name)
|
||||
require.Equal(t, entries{{Token: start, Peer: peer2name, Free: 10, Version: 1},
|
||||
{Token: dot10, Peer: peer1name, Free: 235},
|
||||
{Token: dot245, Peer: peer2name, Free: 10}}, ring2.Entries)
|
||||
|
||||
require.NoError(t, ring1.Merge(*ring2))
|
||||
require.Equal(t, entries{{Token: start, Peer: peer2name, Free: 10, Version: 1},
|
||||
{Token: dot10, Peer: peer1name, Free: 235},
|
||||
{Token: dot245, Peer: peer2name, Free: 10}}, ring1.Entries)
|
||||
|
||||
require.Equal(t, ring2.Entries, ring1.Entries)
|
||||
}
|
||||
|
||||
func TestMergeSplit2(t *testing.T) {
|
||||
ring1 := New(start, end, peer1name)
|
||||
ring2 := New(start, end, peer2name)
|
||||
|
||||
// Claim everything for peer2
|
||||
ring1.Entries = []*entry{{Token: start, Peer: peer2name, Free: 250}, {Token: dot250, Peer: peer2name, Free: 5}}
|
||||
require.NoError(t, ring2.Merge(*ring1))
|
||||
require.Equal(t, ring2.Entries, ring1.Entries)
|
||||
|
||||
// Now grant a split range to peer1
|
||||
ring2.GrantRangeToHost(dot10, dot245, peer1name)
|
||||
require.Equal(t, entries{{Token: start, Peer: peer2name, Free: 10, Version: 1},
|
||||
{Token: dot10, Peer: peer1name, Free: 235},
|
||||
{Token: dot245, Peer: peer2name, Free: 5}, {Token: dot250, Peer: peer2name, Free: 5}}, ring2.Entries)
|
||||
|
||||
require.NoError(t, ring1.Merge(*ring2))
|
||||
require.Equal(t, entries{{Token: start, Peer: peer2name, Free: 10, Version: 1},
|
||||
{Token: dot10, Peer: peer1name, Free: 235},
|
||||
{Token: dot245, Peer: peer2name, Free: 5}, {Token: dot250, Peer: peer2name, Free: 5}}, ring1.Entries)
|
||||
|
||||
require.Equal(t, ring2.Entries, ring1.Entries)
|
||||
}
|
||||
|
||||
// A simple test, very similar to above, but using the marshalling to byte[]s
|
||||
func TestGossip(t *testing.T) {
|
||||
ring1 := New(start, end, peer1name)
|
||||
ring2 := New(start, end, peer2name)
|
||||
|
||||
assertRing := func(ring *Ring, entries entries) {
|
||||
require.Equal(t, entries, ring.Entries)
|
||||
}
|
||||
|
||||
assertRing(ring1, []*entry{})
|
||||
assertRing(ring2, []*entry{})
|
||||
|
||||
// Claim everything for peer1
|
||||
ring1.ClaimItAll()
|
||||
assertRing(ring1, []*entry{{Token: start, Peer: peer1name, Free: 255}})
|
||||
assertRing(ring2, []*entry{})
|
||||
|
||||
// Check the Merge sends it to the other ring
|
||||
require.NoError(t, ring2.Merge(*ring1))
|
||||
assertRing(ring1, []*entry{{Token: start, Peer: peer1name, Free: 255}})
|
||||
assertRing(ring2, []*entry{{Token: start, Peer: peer1name, Free: 255}})
|
||||
}
|
||||
|
||||
func assertPeersWithSpace(t *testing.T, ring *Ring, start, end address.Address, expected int) []mesh.PeerName {
|
||||
peers := ring.ChoosePeersToAskForSpace(start, end)
|
||||
require.Equal(t, expected, len(peers))
|
||||
return peers
|
||||
}
|
||||
|
||||
func TestFindFree(t *testing.T) {
|
||||
ring1 := New(start, end, peer1name)
|
||||
|
||||
assertPeersWithSpace(t, ring1, start, end, 0)
|
||||
|
||||
ring1.Entries = []*entry{{Token: start, Peer: peer1name}}
|
||||
assertPeersWithSpace(t, ring1, start, end, 0)
|
||||
|
||||
// We shouldn't return outselves
|
||||
ring1.ReportFree(map[address.Address]address.Offset{start: 10})
|
||||
assertPeersWithSpace(t, ring1, start, end, 0)
|
||||
|
||||
ring1.Entries = []*entry{{Token: start, Peer: peer1name, Free: 1},
|
||||
{Token: middle, Peer: peer1name, Free: 1}}
|
||||
assertPeersWithSpace(t, ring1, start, end, 0)
|
||||
ring1.assertInvariants()
|
||||
|
||||
// We should return others
|
||||
var peers []mesh.PeerName
|
||||
|
||||
ring1.Entries = []*entry{{Token: start, Peer: peer2name, Free: 1}}
|
||||
peers = assertPeersWithSpace(t, ring1, start, end, 1)
|
||||
require.Equal(t, peer2name, peers[0])
|
||||
|
||||
ring1.Entries = []*entry{{Token: start, Peer: peer2name, Free: 1},
|
||||
{Token: middle, Peer: peer3name, Free: 1}}
|
||||
peers = assertPeersWithSpace(t, ring1, start, middle, 1)
|
||||
require.Equal(t, peer2name, peers[0])
|
||||
|
||||
peers = assertPeersWithSpace(t, ring1, middle, end, 1)
|
||||
require.Equal(t, peer3name, peers[0])
|
||||
|
||||
assertPeersWithSpace(t, ring1, start, end, 2)
|
||||
ring1.assertInvariants()
|
||||
}
|
||||
|
||||
func TestReportFree(t *testing.T) {
|
||||
ring1 := New(start, end, peer1name)
|
||||
ring2 := New(start, end, peer2name)
|
||||
|
||||
ring1.ClaimItAll()
|
||||
ring1.GrantRangeToHost(middle, end, peer2name)
|
||||
require.NoError(t, ring2.Merge(*ring1))
|
||||
|
||||
freespace := make(map[address.Address]address.Offset)
|
||||
for _, r := range ring2.OwnedRanges() {
|
||||
freespace[r.Start] = 0
|
||||
}
|
||||
ring2.ReportFree(freespace)
|
||||
}
|
||||
|
||||
func TestMisc(t *testing.T) {
|
||||
ring := New(start, end, peer1name)
|
||||
|
||||
require.True(t, ring.Empty(), "empty")
|
||||
|
||||
ring.ClaimItAll()
|
||||
println(ring.String())
|
||||
}
|
||||
|
||||
func TestEmptyGossip(t *testing.T) {
|
||||
ring1 := New(start, end, peer1name)
|
||||
ring2 := New(start, end, peer2name)
|
||||
|
||||
ring1.ClaimItAll()
|
||||
// This used to panic, and it shouldn't
|
||||
require.NoError(t, ring1.Merge(*ring2))
|
||||
}
|
||||
|
||||
func TestMergeOldMessage(t *testing.T) {
|
||||
ring1 := New(start, end, peer1name)
|
||||
ring2 := New(start, end, peer2name)
|
||||
|
||||
ring1.ClaimItAll()
|
||||
require.NoError(t, ring2.Merge(*ring1))
|
||||
|
||||
ring1.GrantRangeToHost(middle, end, peer1name)
|
||||
require.NoError(t, ring1.Merge(*ring2))
|
||||
}
|
||||
|
||||
func TestSplitRangeAtBeginning(t *testing.T) {
|
||||
ring1 := New(start, end, peer1name)
|
||||
ring2 := New(start, end, peer2name)
|
||||
|
||||
ring1.ClaimItAll()
|
||||
require.NoError(t, ring2.Merge(*ring1))
|
||||
|
||||
ring1.GrantRangeToHost(start, middle, peer2name)
|
||||
require.NoError(t, ring2.Merge(*ring1))
|
||||
}
|
||||
|
||||
func TestOwnedRange(t *testing.T) {
|
||||
ring1 := New(start, end, peer1name)
|
||||
ring1.ClaimItAll()
|
||||
|
||||
require.Equal(t, []address.Range{{Start: start, End: end}}, ring1.OwnedRanges())
|
||||
|
||||
ring1.GrantRangeToHost(middle, end, peer2name)
|
||||
require.Equal(t, []address.Range{{Start: start, End: middle}}, ring1.OwnedRanges())
|
||||
|
||||
ring2 := New(start, end, peer2name)
|
||||
ring2.Merge(*ring1)
|
||||
require.Equal(t, []address.Range{{Start: middle, End: end}}, ring2.OwnedRanges())
|
||||
|
||||
ring2.Entries = []*entry{{Token: middle, Peer: peer2name}}
|
||||
require.Equal(t, []address.Range{{Start: start, End: middle}, {Start: middle, End: end}}, ring2.OwnedRanges())
|
||||
|
||||
ring2.Entries = []*entry{{Token: dot10, Peer: peer2name}, {Token: middle, Peer: peer2name}}
|
||||
require.Equal(t, []address.Range{{Start: start, End: dot10}, {Start: dot10, End: middle},
|
||||
{Start: middle, End: end}}, ring2.OwnedRanges())
|
||||
|
||||
}
|
||||
|
||||
func TestTransfer(t *testing.T) {
|
||||
// First test just checks if we can grant some range to a host, when we transfer it, we get it back
|
||||
ring1 := New(start, end, peer1name)
|
||||
ring1.ClaimItAll()
|
||||
ring1.GrantRangeToHost(middle, end, peer2name)
|
||||
ring1.Transfer(peer2name, peer1name)
|
||||
require.Equal(t, []address.Range{{start, middle}, {middle, end}}, ring1.OwnedRanges())
|
||||
|
||||
// Second test is what happens when a token exists at the end of a range but is transferred
|
||||
// - does it get resurrected correctly?
|
||||
ring1 = New(start, end, peer1name)
|
||||
ring1.ClaimItAll()
|
||||
ring1.GrantRangeToHost(middle, end, peer2name)
|
||||
ring1.Transfer(peer2name, peer1name)
|
||||
ring1.GrantRangeToHost(dot10, middle, peer2name)
|
||||
require.Equal(t, []address.Range{{start, dot10}, {middle, end}}, ring1.OwnedRanges())
|
||||
}
|
||||
|
||||
func TestOwner(t *testing.T) {
|
||||
ring1 := New(start, end, peer1name)
|
||||
require.True(t, ring1.Contains(start), "start should be in ring")
|
||||
require.False(t, ring1.Contains(end), "end should not be in ring")
|
||||
|
||||
require.Equal(t, mesh.UnknownPeerName, ring1.Owner(start))
|
||||
|
||||
ring1.ClaimItAll()
|
||||
ring1.GrantRangeToHost(middle, end, peer2name)
|
||||
require.Equal(t, peer1name, ring1.Owner(start))
|
||||
require.Equal(t, peer2name, ring1.Owner(middle))
|
||||
require.Panics(t, func() {
|
||||
ring1.Owner(end)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
type addressSlice []address.Address
|
||||
|
||||
func (s addressSlice) Len() int { return len(s) }
|
||||
func (s addressSlice) Less(i, j int) bool { return s[i] < s[j] }
|
||||
func (s addressSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
||||
|
||||
func TestFuzzRing(t *testing.T) {
|
||||
var (
|
||||
numPeers = 25
|
||||
iterations = 1000
|
||||
)
|
||||
|
||||
peers := make([]mesh.PeerName, numPeers)
|
||||
for i := 0; i < numPeers; i++ {
|
||||
peer, _ := mesh.PeerNameFromString(fmt.Sprintf("%02d:00:00:00:02:00", i))
|
||||
peers[i] = peer
|
||||
}
|
||||
|
||||
// Make a valid, random ring
|
||||
makeGoodRandomRing := func() *Ring {
|
||||
addressSpace := end - start
|
||||
numTokens := rand.Intn(int(addressSpace))
|
||||
|
||||
tokenMap := make(map[address.Address]bool)
|
||||
for i := 0; i < numTokens; i++ {
|
||||
tokenMap[address.Address(rand.Intn(int(addressSpace)))] = true
|
||||
}
|
||||
var tokens []address.Address
|
||||
for token := range tokenMap {
|
||||
tokens = append(tokens, token)
|
||||
}
|
||||
sort.Sort(addressSlice(tokens))
|
||||
|
||||
peer := peers[rand.Intn(len(peers))]
|
||||
ring := New(start, end, peer)
|
||||
for _, token := range tokens {
|
||||
peer = peers[rand.Intn(len(peers))]
|
||||
ring.Entries = append(ring.Entries, &entry{Token: start + token, Peer: peer})
|
||||
}
|
||||
|
||||
ring.assertInvariants()
|
||||
return ring
|
||||
}
|
||||
|
||||
for i := 0; i < iterations; i++ {
|
||||
// make 2 random rings
|
||||
ring1 := makeGoodRandomRing()
|
||||
ring2 := makeGoodRandomRing()
|
||||
|
||||
// Merge them - this might fail, we don't care
|
||||
// We just want to make sure it doesn't panic
|
||||
ring1.Merge(*ring2)
|
||||
|
||||
// Check whats left still passes assertions
|
||||
ring1.assertInvariants()
|
||||
ring2.assertInvariants()
|
||||
}
|
||||
|
||||
// Make an invalid, random ring
|
||||
makeBadRandomRing := func() *Ring {
|
||||
addressSpace := end - start
|
||||
numTokens := rand.Intn(int(addressSpace))
|
||||
tokens := make([]address.Address, numTokens)
|
||||
for i := 0; i < numTokens; i++ {
|
||||
tokens[i] = address.Address(rand.Intn(int(addressSpace)))
|
||||
}
|
||||
|
||||
peer := peers[rand.Intn(len(peers))]
|
||||
ring := New(start, end, peer)
|
||||
for _, token := range tokens {
|
||||
peer = peers[rand.Intn(len(peers))]
|
||||
ring.Entries = append(ring.Entries, &entry{Token: start + token, Peer: peer})
|
||||
}
|
||||
|
||||
return ring
|
||||
}
|
||||
|
||||
for i := 0; i < iterations; i++ {
|
||||
// make 2 random rings
|
||||
ring1 := makeGoodRandomRing()
|
||||
ring2 := makeBadRandomRing()
|
||||
|
||||
// Merge them - this might fail, we don't care
|
||||
// We just want to make sure it doesn't panic
|
||||
ring1.Merge(*ring2)
|
||||
|
||||
// Check whats left still passes assertions
|
||||
ring1.assertInvariants()
|
||||
}
|
||||
}
|
||||
|
||||
func TestFuzzRingHard(t *testing.T) {
|
||||
//common.SetLogLevel("debug")
|
||||
var (
|
||||
numPeers = 100
|
||||
iterations = 3000
|
||||
peers []mesh.PeerName
|
||||
rings []*Ring
|
||||
nextPeerID = 0
|
||||
)
|
||||
|
||||
addPeer := func() {
|
||||
peer, _ := mesh.PeerNameFromString(fmt.Sprintf("%02d:%02d:00:00:00:00", nextPeerID/10, nextPeerID%10))
|
||||
common.Log.Debugf("%s: Adding peer", peer)
|
||||
nextPeerID++
|
||||
peers = append(peers, peer)
|
||||
rings = append(rings, New(start, end, peer))
|
||||
}
|
||||
|
||||
for i := 0; i < numPeers; i++ {
|
||||
addPeer()
|
||||
}
|
||||
|
||||
rings[0].ClaimItAll()
|
||||
|
||||
randomPeer := func(exclude int) (int, mesh.PeerName, *Ring) {
|
||||
var peerIndex int
|
||||
if exclude >= 0 {
|
||||
peerIndex = rand.Intn(len(peers) - 1)
|
||||
if peerIndex == exclude {
|
||||
peerIndex++
|
||||
}
|
||||
} else {
|
||||
peerIndex = rand.Intn(len(peers))
|
||||
}
|
||||
return peerIndex, peers[peerIndex], rings[peerIndex]
|
||||
}
|
||||
|
||||
// Keep a map of index -> ranges, as these are a little expensive to
|
||||
// calculate for every ring on every iteration.
|
||||
var theRanges = make(map[int][]address.Range)
|
||||
theRanges[0] = rings[0].OwnedRanges()
|
||||
|
||||
addOrRmPeer := func() {
|
||||
if len(peers) < numPeers {
|
||||
addPeer()
|
||||
return
|
||||
}
|
||||
|
||||
// Pick one peer to remove, and a different one to transfer to
|
||||
peerIndex, peername, _ := randomPeer(-1)
|
||||
_, otherPeername, otherRing := randomPeer(peerIndex)
|
||||
|
||||
// We need to be in a ~converged ring to rmpeer
|
||||
for _, ring := range rings {
|
||||
require.NoError(t, otherRing.Merge(*ring))
|
||||
}
|
||||
|
||||
common.Log.Debugf("%s: transferring from peer %s", otherPeername, peername)
|
||||
otherRing.Transfer(peername, otherPeername)
|
||||
|
||||
// Remove peer from our state
|
||||
peers = append(peers[:peerIndex], peers[peerIndex+1:]...)
|
||||
rings = append(rings[:peerIndex], rings[peerIndex+1:]...)
|
||||
theRanges = make(map[int][]address.Range)
|
||||
|
||||
// And now tell everyone about the transfer - rmpeer is
|
||||
// not partition safe
|
||||
for i, ring := range rings {
|
||||
require.NoError(t, ring.Merge(*otherRing))
|
||||
theRanges[i] = ring.OwnedRanges()
|
||||
}
|
||||
}
|
||||
|
||||
doGrantOrGossip := func() {
|
||||
var ringsWithRanges = make([]int, 0, len(rings))
|
||||
for index, ranges := range theRanges {
|
||||
if len(ranges) > 0 {
|
||||
ringsWithRanges = append(ringsWithRanges, index)
|
||||
}
|
||||
}
|
||||
|
||||
if len(ringsWithRanges) > 0 {
|
||||
// Produce a random split in a random owned range, given to a random peer
|
||||
indexWithRanges := ringsWithRanges[rand.Intn(len(ringsWithRanges))]
|
||||
ownedRanges := theRanges[indexWithRanges]
|
||||
ring := rings[indexWithRanges]
|
||||
|
||||
rangeToSplit := ownedRanges[rand.Intn(len(ownedRanges))]
|
||||
size := address.Subtract(rangeToSplit.End, rangeToSplit.Start)
|
||||
ipInRange := address.Add(rangeToSplit.Start, address.Offset(rand.Intn(int(size))))
|
||||
_, peerToGiveTo, _ := randomPeer(-1)
|
||||
common.Log.Debugf("%s: Granting [%v, %v) to %s", ring.Peer, ipInRange, rangeToSplit.End, peerToGiveTo)
|
||||
ring.GrantRangeToHost(ipInRange, rangeToSplit.End, peerToGiveTo)
|
||||
|
||||
// Now 'gossip' this to a random host (note, note could be same host as above)
|
||||
otherIndex, _, otherRing := randomPeer(-1)
|
||||
common.Log.Debugf("%s: 'Gossiping' to %s", ring.Peer, otherRing.Peer)
|
||||
require.NoError(t, otherRing.Merge(*ring))
|
||||
|
||||
theRanges[indexWithRanges] = ring.OwnedRanges()
|
||||
theRanges[otherIndex] = otherRing.OwnedRanges()
|
||||
return
|
||||
}
|
||||
|
||||
// No rings think they own anything (as gossip might be behind)
|
||||
// We're going to pick a random host (which has entries) and gossip
|
||||
// it to a random host (which may or may not have entries).
|
||||
var ringsWithEntries = make([]*Ring, 0, len(rings))
|
||||
for _, ring := range rings {
|
||||
if len(ring.Entries) > 0 {
|
||||
ringsWithEntries = append(ringsWithEntries, ring)
|
||||
}
|
||||
}
|
||||
ring1 := ringsWithEntries[rand.Intn(len(ringsWithEntries))]
|
||||
ring2index, _, ring2 := randomPeer(-1)
|
||||
common.Log.Debugf("%s: 'Gossiping' to %s", ring1.Peer, ring2.Peer)
|
||||
require.NoError(t, ring2.Merge(*ring1))
|
||||
theRanges[ring2index] = ring2.OwnedRanges()
|
||||
}
|
||||
|
||||
for i := 0; i < iterations; i++ {
|
||||
// about 1 in 10 times, rmpeer or add host
|
||||
n := rand.Intn(10)
|
||||
switch {
|
||||
case n < 1:
|
||||
addOrRmPeer()
|
||||
default:
|
||||
doGrantOrGossip()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Ring) ClaimItAll() {
|
||||
r.ClaimForPeers([]mesh.PeerName{r.Peer})
|
||||
}
|
||||
|
||||
func (es entries) String() string {
|
||||
var buffer bytes.Buffer
|
||||
fmt.Fprintf(&buffer, "[")
|
||||
for i, entry := range es {
|
||||
fmt.Fprintf(&buffer, "%+v", *entry)
|
||||
if i+1 < len(es) {
|
||||
fmt.Fprintf(&buffer, " ")
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(&buffer, "]")
|
||||
return buffer.String()
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
package space
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/weaveworks/weave/common"
|
||||
"github.com/weaveworks/weave/net/address"
|
||||
)
|
||||
|
||||
type Space struct {
|
||||
// ours and free represent a set of addresses as a sorted
|
||||
// sequences of ranges. Even elements give the inclusive
|
||||
// starting points of ranges, and odd elements give the
|
||||
// exclusive ending points. Ranges in an array do not
|
||||
// overlap, and neighbouring ranges are always coalesced if
|
||||
// possible, so the arrays consist of sorted Addrs without
|
||||
// repetition.
|
||||
ours []address.Address
|
||||
free []address.Address
|
||||
}
|
||||
|
||||
func New() *Space {
|
||||
return &Space{}
|
||||
}
|
||||
|
||||
func (s *Space) Add(start address.Address, size address.Offset) {
|
||||
s.free = add(s.free, start, address.Add(start, size))
|
||||
}
|
||||
|
||||
// Clear removes all spaces from this space set. Used during node shutdown.
|
||||
func (s *Space) Clear() {
|
||||
s.free = s.free[:0]
|
||||
s.ours = s.ours[:0]
|
||||
}
|
||||
|
||||
// Walk down the free list calling f() on the in-range portions, until
|
||||
// f() returns true or we run out of free space. Return true iff f() returned true
|
||||
func (s *Space) walkFree(r address.Range, f func(address.Range) bool) bool {
|
||||
if r.Start >= r.End { // degenerate case
|
||||
return false
|
||||
}
|
||||
for i := 0; i < len(s.free); i += 2 {
|
||||
chunk := address.Range{Start: s.free[i], End: s.free[i+1]}
|
||||
if chunk.End <= r.Start { // this chunk comes before the range
|
||||
continue
|
||||
}
|
||||
if chunk.Start >= r.End {
|
||||
// all remaining free space is completely after range
|
||||
break
|
||||
}
|
||||
// at this point we know chunk.End>chunk.Start &&
|
||||
// chunk.End>r.Start && r.End>chunk.Start && r.End>r.Start
|
||||
// therefore max(start, r.Start) < min(end, r.End)
|
||||
// Restrict this block of free space to be in range
|
||||
if chunk.Start < r.Start {
|
||||
chunk.Start = r.Start
|
||||
}
|
||||
if chunk.End > r.End {
|
||||
chunk.End = r.End
|
||||
}
|
||||
// at this point we know start<end
|
||||
if f(chunk) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *Space) Allocate(r address.Range) (bool, address.Address) {
|
||||
var result address.Address
|
||||
return s.walkFree(r, func(chunk address.Range) bool {
|
||||
result = chunk.Start
|
||||
s.ours = add(s.ours, result, result+1)
|
||||
s.free = subtract(s.free, result, result+1)
|
||||
return true
|
||||
}), result
|
||||
}
|
||||
|
||||
func (s *Space) Claim(addr address.Address) error {
|
||||
if !contains(s.free, addr) {
|
||||
return fmt.Errorf("Address %v is not free to claim", addr)
|
||||
}
|
||||
|
||||
s.ours = add(s.ours, addr, addr+1)
|
||||
s.free = subtract(s.free, addr, addr+1)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Space) NumFreeAddressesInRange(r address.Range) address.Offset {
|
||||
res := address.Offset(0)
|
||||
s.walkFree(r, func(chunk address.Range) bool {
|
||||
res += chunk.Size()
|
||||
return false
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
func (s *Space) Free(addr address.Address) error {
|
||||
if !contains(s.ours, addr) {
|
||||
return fmt.Errorf("Address %v is not ours", addr)
|
||||
}
|
||||
if contains(s.free, addr) {
|
||||
return fmt.Errorf("Address %v is already free", addr)
|
||||
}
|
||||
|
||||
s.ours = subtract(s.ours, addr, addr+1)
|
||||
s.free = add(s.free, addr, addr+1)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Space) biggestFreeRange(r address.Range) (biggest address.Range) {
|
||||
biggestSize := address.Offset(0)
|
||||
s.walkFree(r, func(chunk address.Range) bool {
|
||||
if size := chunk.Size(); size >= biggestSize {
|
||||
biggest = chunk
|
||||
biggestSize = size
|
||||
}
|
||||
return false
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Space) Donate(r address.Range) (address.Range, bool) {
|
||||
biggest := s.biggestFreeRange(r)
|
||||
|
||||
if biggest.Size() == 0 {
|
||||
return address.Range{}, false
|
||||
}
|
||||
|
||||
// Donate half of that biggest free range. Note size/2 rounds down, so
|
||||
// the resulting donation size rounds up, and in particular can't be empty.
|
||||
biggest.Start = address.Add(biggest.Start, biggest.Size()/2)
|
||||
|
||||
s.ours = subtract(s.ours, biggest.Start, biggest.End)
|
||||
s.free = subtract(s.free, biggest.Start, biggest.End)
|
||||
return biggest, true
|
||||
}
|
||||
|
||||
func firstGreater(a []address.Address, x address.Address) int {
|
||||
return sort.Search(len(a), func(i int) bool { return a[i] > x })
|
||||
}
|
||||
|
||||
func firstGreaterOrEq(a []address.Address, x address.Address) int {
|
||||
return sort.Search(len(a), func(i int) bool { return a[i] >= x })
|
||||
}
|
||||
|
||||
// Do the ranges contain the given address?
|
||||
func contains(addrs []address.Address, addr address.Address) bool {
|
||||
return firstGreater(addrs, addr)&1 != 0
|
||||
}
|
||||
|
||||
// Take the union of the range [start, end) with the ranges in the array
|
||||
func add(addrs []address.Address, start address.Address, end address.Address) []address.Address {
|
||||
return addSub(addrs, start, end, 0)
|
||||
}
|
||||
|
||||
// Subtract the range [start, end) from the ranges in the array
|
||||
func subtract(addrs []address.Address, start address.Address, end address.Address) []address.Address {
|
||||
return addSub(addrs, start, end, 1)
|
||||
}
|
||||
|
||||
func addSub(addrs []address.Address, start address.Address, end address.Address, sense int) []address.Address {
|
||||
startPos := firstGreaterOrEq(addrs, start)
|
||||
endPos := firstGreater(addrs[startPos:], end) + startPos
|
||||
|
||||
// Boundaries up to startPos are unaffected
|
||||
res := make([]address.Address, startPos, len(addrs)+2)
|
||||
copy(res, addrs)
|
||||
|
||||
// Include start and end as new boundaries if they lie
|
||||
// outside/inside existing ranges (according to sense).
|
||||
if startPos&1 == sense {
|
||||
res = append(res, start)
|
||||
}
|
||||
|
||||
if endPos&1 == sense {
|
||||
res = append(res, end)
|
||||
}
|
||||
|
||||
// Boundaries after endPos are unaffected
|
||||
return append(res, addrs[endPos:]...)
|
||||
}
|
||||
|
||||
func (s *Space) String() string {
|
||||
var buf bytes.Buffer
|
||||
if len(s.ours) > 0 {
|
||||
fmt.Fprint(&buf, "owned:")
|
||||
for i := 0; i < len(s.ours); i += 2 {
|
||||
fmt.Fprintf(&buf, " %s+%d ", s.ours[i], s.ours[i+1]-s.ours[i])
|
||||
}
|
||||
}
|
||||
if len(s.free) > 0 {
|
||||
fmt.Fprintf(&buf, "free:")
|
||||
for i := 0; i < len(s.free); i += 2 {
|
||||
fmt.Fprintf(&buf, " %s+%d ", s.free[i], s.free[i+1]-s.free[i])
|
||||
}
|
||||
}
|
||||
if len(s.ours) == 0 && len(s.free) == 0 {
|
||||
fmt.Fprintf(&buf, "No address ranges owned")
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
type addressSlice []address.Address
|
||||
|
||||
func (p addressSlice) Len() int { return len(p) }
|
||||
func (p addressSlice) Less(i, j int) bool { return p[i] < p[j] }
|
||||
func (p addressSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
|
||||
|
||||
func (s *Space) assertInvariants() {
|
||||
common.Assert(sort.IsSorted(addressSlice(s.ours)))
|
||||
common.Assert(sort.IsSorted(addressSlice(s.free)))
|
||||
}
|
||||
|
||||
// Return a slice representing everything we own, whether it is free or not
|
||||
func (s *Space) everything() []address.Address {
|
||||
a := make([]address.Address, len(s.ours))
|
||||
copy(a, s.ours)
|
||||
for i := 0; i < len(s.free); i += 2 {
|
||||
a = add(a, s.free[i], s.free[i+1])
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
// OwnedRanges returns slice of Ranges, ordered by IP, gluing together
|
||||
// contiguous sequences of owned and free addresses
|
||||
func (s *Space) OwnedRanges() []address.Range {
|
||||
everything := s.everything()
|
||||
result := make([]address.Range, len(everything)/2)
|
||||
for i := 0; i < len(everything); i += 2 {
|
||||
result[i/2] = address.Range{Start: everything[i], End: everything[i+1]}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Create a Space that has free space in all the supplied Ranges.
|
||||
func (s *Space) AddRanges(ranges []address.Range) {
|
||||
for _, r := range ranges {
|
||||
s.free = add(s.free, r.Start, r.End)
|
||||
}
|
||||
}
|
||||
|
||||
// Taking ranges to be a set of all space we should own, add in any excess as free space
|
||||
func (s *Space) UpdateRanges(ranges []address.Range) {
|
||||
new := []address.Address{}
|
||||
for _, r := range ranges {
|
||||
new = add(new, r.Start, r.End)
|
||||
}
|
||||
current := s.everything()
|
||||
for i := 0; i < len(current); i += 2 {
|
||||
new = subtract(new, current[i], current[i+1])
|
||||
}
|
||||
for i := 0; i < len(new); i += 2 {
|
||||
s.free = add(s.free, new[i], new[i+1])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
package space
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/weaveworks/weave/net/address"
|
||||
wt "github.com/weaveworks/weave/testing"
|
||||
)
|
||||
|
||||
func makeSpace(start address.Address, size address.Offset) *Space {
|
||||
s := New()
|
||||
s.Add(start, size)
|
||||
return s
|
||||
}
|
||||
|
||||
func ip(s string) address.Address {
|
||||
addr, _ := address.ParseIP(s)
|
||||
return addr
|
||||
}
|
||||
|
||||
// Helper function to avoid 'NumFreeAddressesInRange(start, end)'
|
||||
// dozens of times in tests
|
||||
func (s *Space) NumFreeAddresses() address.Offset {
|
||||
res := address.Offset(0)
|
||||
for i := 0; i < len(s.free); i += 2 {
|
||||
res += address.Subtract(s.free[i+1], s.free[i])
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func TestLowlevel(t *testing.T) {
|
||||
a := []address.Address{}
|
||||
a = add(a, 100, 200)
|
||||
require.Equal(t, []address.Address{100, 200}, a)
|
||||
require.True(t, !contains(a, 99), "")
|
||||
require.True(t, contains(a, 100), "")
|
||||
require.True(t, contains(a, 199), "")
|
||||
require.True(t, !contains(a, 200), "")
|
||||
a = add(a, 700, 800)
|
||||
require.Equal(t, []address.Address{100, 200, 700, 800}, a)
|
||||
a = add(a, 300, 400)
|
||||
require.Equal(t, []address.Address{100, 200, 300, 400, 700, 800}, a)
|
||||
a = add(a, 400, 500)
|
||||
require.Equal(t, []address.Address{100, 200, 300, 500, 700, 800}, a)
|
||||
a = add(a, 600, 700)
|
||||
require.Equal(t, []address.Address{100, 200, 300, 500, 600, 800}, a)
|
||||
a = add(a, 500, 600)
|
||||
require.Equal(t, []address.Address{100, 200, 300, 800}, a)
|
||||
a = subtract(a, 500, 600)
|
||||
require.Equal(t, []address.Address{100, 200, 300, 500, 600, 800}, a)
|
||||
a = subtract(a, 600, 700)
|
||||
require.Equal(t, []address.Address{100, 200, 300, 500, 700, 800}, a)
|
||||
a = subtract(a, 400, 500)
|
||||
require.Equal(t, []address.Address{100, 200, 300, 400, 700, 800}, a)
|
||||
a = subtract(a, 300, 400)
|
||||
require.Equal(t, []address.Address{100, 200, 700, 800}, a)
|
||||
a = subtract(a, 700, 800)
|
||||
require.Equal(t, []address.Address{100, 200}, a)
|
||||
a = subtract(a, 100, 200)
|
||||
require.Equal(t, []address.Address{}, a)
|
||||
|
||||
s := New()
|
||||
require.Equal(t, address.Offset(0), s.NumFreeAddresses())
|
||||
ok, got := s.Allocate(address.NewRange(0, 1000))
|
||||
require.False(t, ok, "allocate in empty space should fail")
|
||||
|
||||
s.Add(100, 100)
|
||||
require.Equal(t, address.Offset(100), s.NumFreeAddresses())
|
||||
ok, got = s.Allocate(address.NewRange(0, 1000))
|
||||
require.True(t, ok && got == 100, "allocate")
|
||||
require.Equal(t, address.Offset(99), s.NumFreeAddresses())
|
||||
require.NoError(t, s.Claim(150))
|
||||
require.Equal(t, address.Offset(98), s.NumFreeAddresses())
|
||||
require.NoError(t, s.Free(100))
|
||||
require.Equal(t, address.Offset(99), s.NumFreeAddresses())
|
||||
wt.AssertErrorInterface(t, (*error)(nil), s.Free(0), "free not allocated")
|
||||
wt.AssertErrorInterface(t, (*error)(nil), s.Free(100), "double free")
|
||||
|
||||
r, ok := s.Donate(address.NewRange(0, 1000))
|
||||
require.True(t, ok && r.Start == 125 && r.Size() == 25, "donate")
|
||||
|
||||
// test Donate when addresses are scarce
|
||||
s = New()
|
||||
r, ok = s.Donate(address.NewRange(0, 1000))
|
||||
require.True(t, !ok, "donate on empty space should fail")
|
||||
s.Add(0, 3)
|
||||
require.NoError(t, s.Claim(0))
|
||||
require.NoError(t, s.Claim(2))
|
||||
r, ok = s.Donate(address.NewRange(0, 1000))
|
||||
require.True(t, ok && r.Start == 1 && r.End == 2, "donate")
|
||||
r, ok = s.Donate(address.NewRange(0, 1000))
|
||||
require.True(t, !ok, "donate should fail")
|
||||
}
|
||||
|
||||
func TestSpaceAllocate(t *testing.T) {
|
||||
const (
|
||||
testAddr1 = "10.0.3.4"
|
||||
testAddr2 = "10.0.3.5"
|
||||
testAddrx = "10.0.3.19"
|
||||
testAddry = "10.0.9.19"
|
||||
containerID = "deadbeef"
|
||||
size = 20
|
||||
)
|
||||
var (
|
||||
start = ip(testAddr1)
|
||||
)
|
||||
|
||||
space1 := makeSpace(start, size)
|
||||
require.Equal(t, address.Offset(20), space1.NumFreeAddresses())
|
||||
space1.assertInvariants()
|
||||
|
||||
_, addr1 := space1.Allocate(address.NewRange(start, size))
|
||||
require.Equal(t, testAddr1, addr1.String(), "address")
|
||||
require.Equal(t, address.Offset(19), space1.NumFreeAddresses())
|
||||
space1.assertInvariants()
|
||||
|
||||
_, addr2 := space1.Allocate(address.NewRange(start, size))
|
||||
require.False(t, addr2.String() == testAddr1, "address")
|
||||
require.Equal(t, address.Offset(18), space1.NumFreeAddresses())
|
||||
require.Equal(t, address.Offset(13), space1.NumFreeAddressesInRange(address.Range{Start: ip(testAddr1), End: ip(testAddrx)}))
|
||||
require.Equal(t, address.Offset(18), space1.NumFreeAddressesInRange(address.Range{Start: ip(testAddr1), End: ip(testAddry)}))
|
||||
space1.assertInvariants()
|
||||
|
||||
space1.Free(addr2)
|
||||
space1.assertInvariants()
|
||||
|
||||
wt.AssertErrorInterface(t, (*error)(nil), space1.Free(addr2), "double free")
|
||||
wt.AssertErrorInterface(t, (*error)(nil), space1.Free(ip(testAddrx)), "address not allocated")
|
||||
wt.AssertErrorInterface(t, (*error)(nil), space1.Free(ip(testAddry)), "wrong out of range")
|
||||
|
||||
space1.assertInvariants()
|
||||
}
|
||||
|
||||
func TestSpaceFree(t *testing.T) {
|
||||
const (
|
||||
testAddr1 = "10.0.3.4"
|
||||
testAddrx = "10.0.3.19"
|
||||
testAddry = "10.0.9.19"
|
||||
containerID = "deadbeef"
|
||||
)
|
||||
|
||||
entireRange := address.NewRange(ip(testAddr1), 20)
|
||||
space := makeSpace(ip(testAddr1), 20)
|
||||
|
||||
// Check we are prepared to give up the entire space
|
||||
r := space.biggestFreeRange(entireRange)
|
||||
require.True(t, r.Start == ip(testAddr1) && r.Size() == 20, "Wrong space")
|
||||
|
||||
for i := 0; i < 20; i++ {
|
||||
ok, _ := space.Allocate(entireRange)
|
||||
require.True(t, ok, "Failed to get address")
|
||||
}
|
||||
|
||||
// Check we are full
|
||||
ok, _ := space.Allocate(entireRange)
|
||||
require.True(t, !ok, "Should have failed to get address")
|
||||
r, ok = space.Donate(entireRange)
|
||||
require.True(t, r.Size() == 0, "Wrong space")
|
||||
|
||||
// Free in the middle
|
||||
require.NoError(t, space.Free(ip("10.0.3.13")))
|
||||
r = space.biggestFreeRange(entireRange)
|
||||
require.True(t, r.Start == ip("10.0.3.13") && r.Size() == 1, "Wrong space")
|
||||
|
||||
// Free one at the end
|
||||
require.NoError(t, space.Free(ip("10.0.3.23")))
|
||||
r = space.biggestFreeRange(entireRange)
|
||||
require.True(t, r.Start == ip("10.0.3.23") && r.Size() == 1, "Wrong space")
|
||||
|
||||
// Now free a few at the end
|
||||
require.NoError(t, space.Free(ip("10.0.3.22")))
|
||||
require.NoError(t, space.Free(ip("10.0.3.21")))
|
||||
|
||||
require.Equal(t, address.Offset(4), space.NumFreeAddresses())
|
||||
|
||||
// Now get the biggest free space; should be 3.21
|
||||
r = space.biggestFreeRange(entireRange)
|
||||
require.True(t, r.Start == ip("10.0.3.21") && r.Size() == 3, "Wrong space")
|
||||
|
||||
// Now free a few in the middle
|
||||
require.NoError(t, space.Free(ip("10.0.3.12")))
|
||||
require.NoError(t, space.Free(ip("10.0.3.11")))
|
||||
require.NoError(t, space.Free(ip("10.0.3.10")))
|
||||
|
||||
require.Equal(t, address.Offset(7), space.NumFreeAddresses())
|
||||
|
||||
// Now get the biggest free space; should be 3.21
|
||||
r = space.biggestFreeRange(entireRange)
|
||||
require.True(t, r.Start == ip("10.0.3.10") && r.Size() == 4, "Wrong space")
|
||||
|
||||
require.Equal(t, []address.Range{{Start: ip("10.0.3.4"), End: ip("10.0.3.24")}}, space.OwnedRanges())
|
||||
}
|
||||
|
||||
func TestDonateSimple(t *testing.T) {
|
||||
const (
|
||||
testAddr1 = "10.0.1.0"
|
||||
testAddr2 = "10.0.1.32"
|
||||
size = 48
|
||||
)
|
||||
|
||||
var (
|
||||
ipAddr1 = ip(testAddr1)
|
||||
)
|
||||
|
||||
ps1 := makeSpace(ipAddr1, size)
|
||||
|
||||
// Empty space set should split in two and give me the second half
|
||||
r, ok := ps1.Donate(address.NewRange(ip(testAddr1), size))
|
||||
numGivenUp := r.Size()
|
||||
require.True(t, ok, "Donate result")
|
||||
require.Equal(t, "10.0.1.24", r.Start.String(), "Invalid start")
|
||||
require.Equal(t, address.Offset(size/2), numGivenUp)
|
||||
require.Equal(t, address.Offset(size/2), ps1.NumFreeAddresses())
|
||||
|
||||
// Now check we can give the rest up.
|
||||
count := 0 // count to avoid infinite loop
|
||||
for ; count < 1000; count++ {
|
||||
r, ok := ps1.Donate(address.NewRange(ip(testAddr1), size))
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
numGivenUp += r.Size()
|
||||
}
|
||||
require.Equal(t, address.Offset(0), ps1.NumFreeAddresses())
|
||||
require.Equal(t, address.Offset(size), numGivenUp)
|
||||
}
|
||||
|
||||
func TestDonateHard(t *testing.T) {
|
||||
//common.InitDefaultLogging(true)
|
||||
var (
|
||||
start = ip("10.0.1.0")
|
||||
size address.Offset = 48
|
||||
)
|
||||
|
||||
// Fill a fresh space
|
||||
spaceset := makeSpace(start, size)
|
||||
for i := address.Offset(0); i < size; i++ {
|
||||
ok, _ := spaceset.Allocate(address.NewRange(start, size))
|
||||
require.True(t, ok, "Failed to get IP!")
|
||||
}
|
||||
|
||||
require.Equal(t, address.Offset(0), spaceset.NumFreeAddresses())
|
||||
|
||||
// Now free all but the last address
|
||||
// this will force us to split the free list
|
||||
for i := address.Offset(0); i < size-1; i++ {
|
||||
require.NoError(t, spaceset.Free(address.Add(start, i)))
|
||||
}
|
||||
|
||||
// Now split
|
||||
newRange, ok := spaceset.Donate(address.NewRange(start, size))
|
||||
require.True(t, ok, "GiveUpSpace result")
|
||||
require.Equal(t, ip("10.0.1.23"), newRange.Start)
|
||||
require.Equal(t, address.Offset(24), newRange.Size())
|
||||
require.Equal(t, address.Offset(23), spaceset.NumFreeAddresses())
|
||||
|
||||
//Space set should now have 2 spaces
|
||||
expected := New()
|
||||
expected.Add(start, 23)
|
||||
expected.ours = add(nil, ip("10.0.1.47"), ip("10.0.1.48"))
|
||||
require.Equal(t, expected, spaceset)
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package ipam
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/weaveworks/weave/ipam/paxos"
|
||||
"github.com/weaveworks/weave/net/address"
|
||||
)
|
||||
|
||||
type Status struct {
|
||||
Paxos *paxos.Status
|
||||
Range string
|
||||
RangeNumIPs int
|
||||
DefaultSubnet string
|
||||
Entries []EntryStatus
|
||||
PendingClaims []ClaimStatus
|
||||
PendingAllocates []string
|
||||
}
|
||||
|
||||
type EntryStatus struct {
|
||||
Token string
|
||||
Size uint32
|
||||
Peer string
|
||||
Nickname string
|
||||
IsKnownPeer bool
|
||||
Version uint32
|
||||
}
|
||||
|
||||
type ClaimStatus struct {
|
||||
Ident string
|
||||
Address address.Address
|
||||
}
|
||||
|
||||
func NewStatus(allocator *Allocator, defaultSubnet address.CIDR) *Status {
|
||||
if allocator == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var paxosStatus *paxos.Status
|
||||
if allocator.paxosTicker != nil {
|
||||
paxosStatus = paxos.NewStatus(allocator.paxos)
|
||||
}
|
||||
|
||||
resultChan := make(chan *Status)
|
||||
allocator.actionChan <- func() {
|
||||
resultChan <- &Status{
|
||||
paxosStatus,
|
||||
allocator.universe.String(),
|
||||
int(allocator.universe.Size()),
|
||||
defaultSubnet.String(),
|
||||
newEntryStatusSlice(allocator),
|
||||
newClaimStatusSlice(allocator),
|
||||
newAllocateIdentSlice(allocator)}
|
||||
}
|
||||
|
||||
return <-resultChan
|
||||
}
|
||||
|
||||
func newEntryStatusSlice(allocator *Allocator) []EntryStatus {
|
||||
var slice []EntryStatus
|
||||
|
||||
if allocator.ring.Empty() {
|
||||
return slice
|
||||
}
|
||||
|
||||
for _, r := range allocator.ring.AllRangeInfo() {
|
||||
slice = append(slice, EntryStatus{
|
||||
Token: r.Start.String(),
|
||||
Size: uint32(r.Size()),
|
||||
Peer: r.Peer.String(),
|
||||
Nickname: allocator.nicknames[r.Peer],
|
||||
IsKnownPeer: allocator.isKnownPeer(r.Peer),
|
||||
Version: r.Version,
|
||||
})
|
||||
}
|
||||
|
||||
return slice
|
||||
}
|
||||
|
||||
func newClaimStatusSlice(allocator *Allocator) []ClaimStatus {
|
||||
var slice []ClaimStatus
|
||||
for _, op := range allocator.pendingClaims {
|
||||
claim := op.(*claim)
|
||||
slice = append(slice, ClaimStatus{claim.ident, claim.addr})
|
||||
}
|
||||
return slice
|
||||
}
|
||||
|
||||
func newAllocateIdentSlice(allocator *Allocator) []string {
|
||||
var slice []string
|
||||
for _, op := range allocator.pendingAllocates {
|
||||
allocate := op.(*allocate)
|
||||
slice = append(slice, fmt.Sprintf("%s %s", allocate.ident, allocate.r.String()))
|
||||
}
|
||||
return slice
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
package ipam
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/weaveworks/weave/mesh"
|
||||
"github.com/weaveworks/weave/net/address"
|
||||
"github.com/weaveworks/weave/testing/gossip"
|
||||
)
|
||||
|
||||
type mockMessage struct {
|
||||
dst mesh.PeerName
|
||||
msgType byte
|
||||
buf []byte
|
||||
}
|
||||
|
||||
func (m *mockMessage) String() string {
|
||||
return fmt.Sprintf("-> %s [%x]", m.dst, m.buf)
|
||||
}
|
||||
|
||||
func toStringArray(messages []mockMessage) []string {
|
||||
out := make([]string, len(messages))
|
||||
for i := range out {
|
||||
out[i] = messages[i].String()
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
type mockGossipComms struct {
|
||||
sync.RWMutex
|
||||
*testing.T
|
||||
name string
|
||||
messages []mockMessage
|
||||
}
|
||||
|
||||
func (m *mockGossipComms) String() string {
|
||||
m.RLock()
|
||||
defer m.RUnlock()
|
||||
return fmt.Sprintf("[mockGossipComms %s]", m.name)
|
||||
}
|
||||
|
||||
// Note: this style of verification, using equalByteBuffer, requires
|
||||
// that the contents of messages are never re-ordered. Which, for instance,
|
||||
// requires they are not based off iterating through a map.
|
||||
|
||||
func (m *mockGossipComms) GossipBroadcast(update mesh.GossipData) error {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
buf := []byte{}
|
||||
if len(m.messages) == 0 {
|
||||
require.FailNow(m, fmt.Sprintf("%s: Gossip broadcast message unexpected: \n%x", m.name, buf))
|
||||
} else if msg := m.messages[0]; msg.dst != mesh.UnknownPeerName {
|
||||
require.FailNow(m, fmt.Sprintf("%s: Expected Gossip message to %s but got broadcast", m.name, msg.dst))
|
||||
} else if msg.buf != nil && !equalByteBuffer(msg.buf, buf) {
|
||||
require.FailNow(m, fmt.Sprintf("%s: Gossip message not sent as expected: \nwant: %x\ngot : %x", m.name, msg.buf, buf))
|
||||
} else {
|
||||
// Swallow this message
|
||||
m.messages = m.messages[1:]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func equalByteBuffer(a, b []byte) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i := 0; i < len(a); i++ {
|
||||
if a[i] != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *mockGossipComms) GossipUnicast(dstPeerName mesh.PeerName, buf []byte) error {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
if len(m.messages) == 0 {
|
||||
require.FailNow(m, fmt.Sprintf("%s: Gossip message to %s unexpected: \n%s", m.name, dstPeerName, buf))
|
||||
} else if msg := m.messages[0]; msg.dst == mesh.UnknownPeerName {
|
||||
require.FailNow(m, fmt.Sprintf("%s: Expected Gossip broadcast message but got dest %s", m.name, dstPeerName))
|
||||
} else if msg.dst != dstPeerName {
|
||||
require.FailNow(m, fmt.Sprintf("%s: Expected Gossip message to %s but got dest %s", m.name, msg.dst, dstPeerName))
|
||||
} else if buf[0] != msg.msgType {
|
||||
require.FailNow(m, fmt.Sprintf("%s: Expected Gossip message of type %d but got type %d", m.name, msg.msgType, buf[0]))
|
||||
} else if msg.buf != nil && !equalByteBuffer(msg.buf, buf[1:]) {
|
||||
require.FailNow(m, fmt.Sprintf("%s: Gossip message not sent as expected: \nwant: %x\ngot : %x", m.name, msg.buf, buf[1:]))
|
||||
} else {
|
||||
// Swallow this message
|
||||
m.messages = m.messages[1:]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ExpectMessage(alloc *Allocator, dst string, msgType byte, buf []byte) {
|
||||
m := alloc.gossip.(*mockGossipComms)
|
||||
dstPeerName, _ := mesh.PeerNameFromString(dst)
|
||||
m.Lock()
|
||||
m.messages = append(m.messages, mockMessage{dstPeerName, msgType, buf})
|
||||
m.Unlock()
|
||||
}
|
||||
|
||||
func ExpectBroadcastMessage(alloc *Allocator, buf []byte) {
|
||||
m := alloc.gossip.(*mockGossipComms)
|
||||
m.Lock()
|
||||
m.messages = append(m.messages, mockMessage{mesh.UnknownPeerName, 0, buf})
|
||||
m.Unlock()
|
||||
}
|
||||
|
||||
func CheckAllExpectedMessagesSent(allocs ...*Allocator) {
|
||||
for _, alloc := range allocs {
|
||||
m := alloc.gossip.(*mockGossipComms)
|
||||
m.RLock()
|
||||
if len(m.messages) > 0 {
|
||||
require.FailNow(m, fmt.Sprintf("%s: Gossip message(s) not sent as expected: \n%x", m.name, m.messages))
|
||||
}
|
||||
m.RUnlock()
|
||||
}
|
||||
}
|
||||
|
||||
func makeAllocator(name string, cidrStr string, quorum uint) (*Allocator, address.Range) {
|
||||
peername, err := mesh.PeerNameFromString(name)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
_, cidr, err := address.ParseCIDR(cidrStr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
alloc := NewAllocator(peername, mesh.PeerUID(rand.Int63()),
|
||||
"nick-"+name, cidr.Range(), quorum, func(mesh.PeerName) bool { return true })
|
||||
|
||||
return alloc, cidr.HostRange()
|
||||
}
|
||||
|
||||
func makeAllocatorWithMockGossip(t *testing.T, name string, universeCIDR string, quorum uint) (*Allocator, address.Range) {
|
||||
alloc, subnet := makeAllocator(name, universeCIDR, quorum)
|
||||
gossip := &mockGossipComms{T: t, name: name}
|
||||
alloc.SetInterfaces(gossip)
|
||||
alloc.Start()
|
||||
return alloc, subnet
|
||||
}
|
||||
|
||||
func (alloc *Allocator) claimRingForTesting(allocs ...*Allocator) {
|
||||
peers := []mesh.PeerName{alloc.ourName}
|
||||
for _, alloc2 := range allocs {
|
||||
peers = append(peers, alloc2.ourName)
|
||||
}
|
||||
alloc.ring.ClaimForPeers(normalizeConsensus(peers))
|
||||
alloc.space.AddRanges(alloc.ring.OwnedRanges())
|
||||
}
|
||||
|
||||
func (alloc *Allocator) NumFreeAddresses(r address.Range) address.Offset {
|
||||
resultChan := make(chan address.Offset)
|
||||
alloc.actionChan <- func() {
|
||||
resultChan <- alloc.space.NumFreeAddressesInRange(r)
|
||||
}
|
||||
return <-resultChan
|
||||
}
|
||||
|
||||
// Check whether or not something was sent on a channel
|
||||
func AssertSent(t *testing.T, ch <-chan bool) {
|
||||
timeout := time.After(10 * time.Second)
|
||||
select {
|
||||
case <-ch:
|
||||
// This case is ok
|
||||
case <-timeout:
|
||||
require.FailNow(t, "Nothing sent on channel")
|
||||
}
|
||||
}
|
||||
|
||||
func AssertNothingSent(t *testing.T, ch <-chan bool) {
|
||||
select {
|
||||
case val := <-ch:
|
||||
require.FailNow(t, fmt.Sprintf("Unexpected value on channel: %v", val))
|
||||
default:
|
||||
// no message received
|
||||
}
|
||||
}
|
||||
|
||||
func AssertNothingSentErr(t *testing.T, ch <-chan error) {
|
||||
select {
|
||||
case val := <-ch:
|
||||
require.FailNow(t, fmt.Sprintf("Unexpected value on channel: %v", val))
|
||||
default:
|
||||
// no message received
|
||||
}
|
||||
}
|
||||
|
||||
func makeNetworkOfAllocators(size int, cidr string) ([]*Allocator, *gossip.TestRouter, address.Range) {
|
||||
gossipRouter := gossip.NewTestRouter(0.0)
|
||||
allocs := make([]*Allocator, size)
|
||||
var subnet address.Range
|
||||
|
||||
for i := 0; i < size; i++ {
|
||||
var alloc *Allocator
|
||||
alloc, subnet = makeAllocator(fmt.Sprintf("%02d:00:00:02:00:00", i),
|
||||
cidr, uint(size/2+1))
|
||||
alloc.SetInterfaces(gossipRouter.Connect(alloc.ourName, alloc))
|
||||
alloc.Start()
|
||||
allocs[i] = alloc
|
||||
}
|
||||
|
||||
allocs[size-1].gossip.GossipBroadcast(allocs[size-1].Gossip())
|
||||
gossipRouter.Flush()
|
||||
return allocs, gossipRouter, subnet
|
||||
}
|
||||
|
||||
func stopNetworkOfAllocators(allocs []*Allocator) {
|
||||
for _, alloc := range allocs {
|
||||
alloc.Stop()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,460 @@
|
||||
package mesh
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Connection interface {
|
||||
Local() *Peer
|
||||
Remote() *Peer
|
||||
RemoteTCPAddr() string
|
||||
Outbound() bool
|
||||
Established() bool
|
||||
BreakTie(Connection) ConnectionTieBreak
|
||||
Shutdown(error)
|
||||
Log(args ...interface{})
|
||||
}
|
||||
|
||||
type ConnectionTieBreak int
|
||||
|
||||
const (
|
||||
TieBreakWon ConnectionTieBreak = iota
|
||||
TieBreakLost
|
||||
TieBreakTied
|
||||
)
|
||||
|
||||
var ErrConnectToSelf = fmt.Errorf("Cannot connect to ourself")
|
||||
|
||||
type RemoteConnection struct {
|
||||
local *Peer
|
||||
remote *Peer
|
||||
remoteTCPAddr string
|
||||
outbound bool
|
||||
established bool
|
||||
}
|
||||
|
||||
type LocalConnection struct {
|
||||
sync.RWMutex
|
||||
RemoteConnection
|
||||
TCPConn *net.TCPConn
|
||||
TrustRemote bool // is remote on a trusted subnet?
|
||||
TrustedByRemote bool // does remote trust us?
|
||||
version byte
|
||||
tcpSender TCPSender
|
||||
SessionKey *[32]byte
|
||||
heartbeatTCP *time.Ticker
|
||||
Router *Router
|
||||
uid uint64
|
||||
actionChan chan<- ConnectionAction
|
||||
errorChan chan<- error
|
||||
finished <-chan struct{} // closed to signal that actorLoop has finished
|
||||
OverlayConn OverlayConnection
|
||||
}
|
||||
|
||||
type ConnectionAction func() error
|
||||
|
||||
func NewRemoteConnection(from, to *Peer, tcpAddr string, outbound bool, established bool) *RemoteConnection {
|
||||
return &RemoteConnection{
|
||||
local: from,
|
||||
remote: to,
|
||||
remoteTCPAddr: tcpAddr,
|
||||
outbound: outbound,
|
||||
established: established,
|
||||
}
|
||||
}
|
||||
|
||||
func (conn *RemoteConnection) Local() *Peer { return conn.local }
|
||||
func (conn *RemoteConnection) Remote() *Peer { return conn.remote }
|
||||
func (conn *RemoteConnection) RemoteTCPAddr() string { return conn.remoteTCPAddr }
|
||||
func (conn *RemoteConnection) Outbound() bool { return conn.outbound }
|
||||
func (conn *RemoteConnection) Established() bool { return conn.established }
|
||||
func (conn *RemoteConnection) BreakTie(Connection) ConnectionTieBreak { return TieBreakTied }
|
||||
func (conn *RemoteConnection) Shutdown(error) {}
|
||||
|
||||
func (conn *RemoteConnection) Log(args ...interface{}) {
|
||||
log.Println(append(append([]interface{}{}, fmt.Sprintf("->[%s|%s]:", conn.remoteTCPAddr, conn.remote)), args...)...)
|
||||
}
|
||||
|
||||
func (conn *RemoteConnection) ErrorLog(args ...interface{}) {
|
||||
log.Errorln(append(append([]interface{}{}, fmt.Sprintf("->[%s|%s]:", conn.remoteTCPAddr, conn.remote)), args...)...)
|
||||
}
|
||||
|
||||
// Does not return anything. If the connection is successful, it will
|
||||
// end up in the local peer's connections map.
|
||||
func StartLocalConnection(connRemote *RemoteConnection, tcpConn *net.TCPConn, router *Router, acceptNewPeer bool) {
|
||||
if connRemote.local != router.Ourself.Peer {
|
||||
log.Fatal("Attempt to create local connection from a peer which is not ourself")
|
||||
}
|
||||
actionChan := make(chan ConnectionAction, ChannelSize)
|
||||
errorChan := make(chan error, 1)
|
||||
finished := make(chan struct{})
|
||||
conn := &LocalConnection{
|
||||
RemoteConnection: *connRemote, // NB, we're taking a copy of connRemote here.
|
||||
Router: router,
|
||||
TCPConn: tcpConn,
|
||||
TrustRemote: router.Trusts(connRemote),
|
||||
uid: randUint64(),
|
||||
actionChan: actionChan,
|
||||
errorChan: errorChan,
|
||||
finished: finished}
|
||||
go conn.run(actionChan, errorChan, finished, acceptNewPeer)
|
||||
}
|
||||
|
||||
func (conn *LocalConnection) BreakTie(dupConn Connection) ConnectionTieBreak {
|
||||
dupConnLocal := dupConn.(*LocalConnection)
|
||||
// conn.uid is used as the tie breaker here, in the knowledge that
|
||||
// both sides will make the same decision.
|
||||
if conn.uid < dupConnLocal.uid {
|
||||
return TieBreakWon
|
||||
} else if dupConnLocal.uid < conn.uid {
|
||||
return TieBreakLost
|
||||
} else {
|
||||
return TieBreakTied
|
||||
}
|
||||
}
|
||||
|
||||
func (conn *LocalConnection) Established() bool {
|
||||
return conn.established
|
||||
}
|
||||
|
||||
func (conn *LocalConnection) SendProtocolMsg(m ProtocolMsg) {
|
||||
if err := conn.sendProtocolMsg(m); err != nil {
|
||||
conn.Shutdown(err)
|
||||
}
|
||||
}
|
||||
|
||||
// ACTOR methods
|
||||
|
||||
// NB: The conn.* fields are only written by the connection actor
|
||||
// process, which is the caller of the ConnectionAction funs. Hence we
|
||||
// do not need locks for reading, and only need write locks for fields
|
||||
// read by other processes.
|
||||
|
||||
// Non-blocking.
|
||||
func (conn *LocalConnection) Shutdown(err error) {
|
||||
// err should always be a real error, even if only io.EOF
|
||||
if err == nil {
|
||||
panic("nil error")
|
||||
}
|
||||
|
||||
select {
|
||||
case conn.errorChan <- err:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
// Send an actor request to the actorLoop, but don't block if
|
||||
// actorLoop has exited - see http://blog.golang.org/pipelines for
|
||||
// pattern
|
||||
func (conn *LocalConnection) sendAction(action ConnectionAction) {
|
||||
select {
|
||||
case conn.actionChan <- action:
|
||||
case <-conn.finished:
|
||||
}
|
||||
}
|
||||
|
||||
// ACTOR server
|
||||
|
||||
func (conn *LocalConnection) run(actionChan <-chan ConnectionAction, errorChan <-chan error, finished chan<- struct{}, acceptNewPeer bool) {
|
||||
var err error // important to use this var and not create another one with 'err :='
|
||||
defer func() { conn.shutdown(err) }()
|
||||
defer close(finished)
|
||||
|
||||
conn.TCPConn.SetLinger(0)
|
||||
intro, err := ProtocolIntroParams{
|
||||
MinVersion: conn.Router.ProtocolMinVersion,
|
||||
MaxVersion: ProtocolMaxVersion,
|
||||
Features: conn.makeFeatures(),
|
||||
Conn: conn.TCPConn,
|
||||
Password: conn.Router.Password,
|
||||
Outbound: conn.outbound,
|
||||
}.DoIntro()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
conn.SessionKey = intro.SessionKey
|
||||
conn.tcpSender = intro.Sender
|
||||
conn.version = intro.Version
|
||||
|
||||
remote, err := conn.parseFeatures(intro.Features)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = conn.registerRemote(remote, acceptNewPeer); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
conn.Log("connection ready; using protocol version", conn.version)
|
||||
|
||||
// only use negotiated session key for untrusted connections
|
||||
var sessionKey *[32]byte
|
||||
if conn.Untrusted() {
|
||||
sessionKey = conn.SessionKey
|
||||
}
|
||||
|
||||
params := OverlayConnectionParams{
|
||||
RemotePeer: conn.remote,
|
||||
LocalAddr: conn.TCPConn.LocalAddr().(*net.TCPAddr),
|
||||
RemoteAddr: conn.TCPConn.RemoteAddr().(*net.TCPAddr),
|
||||
Outbound: conn.outbound,
|
||||
ConnUID: conn.uid,
|
||||
SessionKey: sessionKey,
|
||||
SendControlMessage: conn.sendOverlayControlMessage,
|
||||
Features: intro.Features,
|
||||
}
|
||||
if conn.OverlayConn, err = conn.Router.Overlay.PrepareConnection(params); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// As soon as we do AddConnection, the new connection becomes
|
||||
// visible to the packet routing logic. So AddConnection must
|
||||
// come after PrepareConnection
|
||||
if err = conn.Router.Ourself.AddConnection(conn); err != nil {
|
||||
return
|
||||
}
|
||||
conn.Router.ConnectionMaker.ConnectionCreated(conn)
|
||||
|
||||
// OverlayConnection confirmation comes after AddConnection,
|
||||
// because only after that completes do we know the connection is
|
||||
// valid: in particular that it is not a duplicate connection to
|
||||
// the same peer. Overlay communication on a duplicate connection
|
||||
// can cause problems such as tripping up overlay crypto at the
|
||||
// other end due to data being decoded by the other connection. It
|
||||
// is also generally wasteful to engage in any interaction with
|
||||
// the remote on a connection that turns out to be invalid.
|
||||
conn.OverlayConn.Confirm()
|
||||
|
||||
// receiveTCP must follow also AddConnection. In the absence
|
||||
// of any indirect connectivity to the remote peer, the first
|
||||
// we hear about it (and any peers reachable from it) is
|
||||
// through topology gossip it sends us on the connection. We
|
||||
// must ensure that the connection has been added to Ourself
|
||||
// prior to processing any such gossip, otherwise we risk
|
||||
// immediately gc'ing part of that newly received portion of
|
||||
// the topology (though not the remote peer itself, since that
|
||||
// will have a positive ref count), leaving behind dangling
|
||||
// references to peers. Hence we must invoke AddConnection,
|
||||
// which is *synchronous*, first.
|
||||
conn.heartbeatTCP = time.NewTicker(TCPHeartbeat)
|
||||
go conn.receiveTCP(intro.Receiver)
|
||||
|
||||
// AddConnection must precede actorLoop. More precisely, it
|
||||
// must precede shutdown, since that invokes DeleteConnection
|
||||
// and is invoked on termination of this entire
|
||||
// function. Essentially this boils down to a prohibition on
|
||||
// running AddConnection in a separate goroutine, at least not
|
||||
// without some synchronisation. Which in turn requires the
|
||||
// launching of the receiveTCP goroutine to precede actorLoop.
|
||||
err = conn.actorLoop(actionChan, errorChan)
|
||||
}
|
||||
|
||||
func (conn *LocalConnection) makeFeatures() map[string]string {
|
||||
features := map[string]string{
|
||||
"PeerNameFlavour": PeerNameFlavour,
|
||||
"Name": conn.local.Name.String(),
|
||||
"NickName": conn.local.NickName,
|
||||
"ShortID": fmt.Sprint(conn.local.ShortID),
|
||||
"UID": fmt.Sprint(conn.local.UID),
|
||||
"ConnID": fmt.Sprint(conn.uid),
|
||||
"Trusted": fmt.Sprint(conn.TrustRemote),
|
||||
}
|
||||
conn.Router.Overlay.AddFeaturesTo(features)
|
||||
return features
|
||||
}
|
||||
|
||||
type features map[string]string
|
||||
|
||||
func (features features) MustHave(keys []string) error {
|
||||
for _, key := range keys {
|
||||
if _, ok := features[key]; !ok {
|
||||
return fmt.Errorf("Field %s is missing", key)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (features features) Get(key string) string {
|
||||
return features[key]
|
||||
}
|
||||
|
||||
func (conn *LocalConnection) parseFeatures(features features) (*Peer, error) {
|
||||
if err := features.MustHave([]string{"PeerNameFlavour", "Name", "NickName", "UID", "ConnID"}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
remotePeerNameFlavour := features.Get("PeerNameFlavour")
|
||||
if remotePeerNameFlavour != PeerNameFlavour {
|
||||
return nil, fmt.Errorf("Peer name flavour mismatch (ours: '%s', theirs: '%s')", PeerNameFlavour, remotePeerNameFlavour)
|
||||
}
|
||||
|
||||
name, err := PeerNameFromString(features.Get("Name"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nickName := features.Get("NickName")
|
||||
|
||||
var shortID uint64
|
||||
var hasShortID bool
|
||||
if shortIDStr, present := features["ShortID"]; present {
|
||||
hasShortID = true
|
||||
shortID, err = strconv.ParseUint(shortIDStr, 10, PeerShortIDBits)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var trusted bool
|
||||
if trustedStr, present := features["Trusted"]; present {
|
||||
trusted, err = strconv.ParseBool(trustedStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
conn.TrustedByRemote = trusted
|
||||
|
||||
uid, err := ParsePeerUID(features.Get("UID"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
remoteConnID, err := strconv.ParseUint(features.Get("ConnID"), 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conn.uid ^= remoteConnID
|
||||
peer := NewPeer(name, nickName, uid, 0, PeerShortID(shortID))
|
||||
peer.HasShortID = hasShortID
|
||||
return peer, nil
|
||||
}
|
||||
|
||||
func (conn *LocalConnection) registerRemote(remote *Peer, acceptNewPeer bool) error {
|
||||
if acceptNewPeer {
|
||||
conn.remote = conn.Router.Peers.FetchWithDefault(remote)
|
||||
} else {
|
||||
conn.remote = conn.Router.Peers.FetchAndAddRef(remote.Name)
|
||||
if conn.remote == nil {
|
||||
return fmt.Errorf("Found unknown remote name: %s at %s", remote.Name, conn.remoteTCPAddr)
|
||||
}
|
||||
}
|
||||
|
||||
if conn.remote == conn.local {
|
||||
return ErrConnectToSelf
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (conn *LocalConnection) actorLoop(actionChan <-chan ConnectionAction, errorChan <-chan error) (err error) {
|
||||
fwdErrorChan := conn.OverlayConn.ErrorChannel()
|
||||
fwdEstablishedChan := conn.OverlayConn.EstablishedChannel()
|
||||
|
||||
for err == nil {
|
||||
select {
|
||||
case action := <-actionChan:
|
||||
err = action()
|
||||
|
||||
case err = <-errorChan:
|
||||
|
||||
case <-conn.heartbeatTCP.C:
|
||||
err = conn.sendSimpleProtocolMsg(ProtocolHeartbeat)
|
||||
|
||||
case <-fwdEstablishedChan:
|
||||
conn.established = true
|
||||
fwdEstablishedChan = nil
|
||||
conn.Router.Ourself.ConnectionEstablished(conn)
|
||||
|
||||
case err = <-fwdErrorChan:
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (conn *LocalConnection) shutdown(err error) {
|
||||
if conn.remote == nil {
|
||||
log.Errorf("->[%s] connection shutting down due to error during handshake: %v", conn.remoteTCPAddr, err)
|
||||
} else {
|
||||
conn.ErrorLog("connection shutting down due to error:", err)
|
||||
}
|
||||
|
||||
if conn.TCPConn != nil {
|
||||
checkWarn(conn.TCPConn.Close())
|
||||
}
|
||||
|
||||
if conn.remote != nil {
|
||||
conn.Router.Peers.Dereference(conn.remote)
|
||||
conn.Router.Ourself.DeleteConnection(conn)
|
||||
}
|
||||
|
||||
if conn.heartbeatTCP != nil {
|
||||
conn.heartbeatTCP.Stop()
|
||||
}
|
||||
|
||||
if conn.OverlayConn != nil {
|
||||
conn.OverlayConn.Stop()
|
||||
}
|
||||
|
||||
conn.Router.ConnectionMaker.ConnectionTerminated(conn, err)
|
||||
}
|
||||
|
||||
func (conn *LocalConnection) sendOverlayControlMessage(tag byte, msg []byte) error {
|
||||
return conn.sendProtocolMsg(ProtocolMsg{ProtocolTag(tag), msg})
|
||||
}
|
||||
|
||||
// Helpers
|
||||
|
||||
func (conn *LocalConnection) sendSimpleProtocolMsg(tag ProtocolTag) error {
|
||||
return conn.sendProtocolMsg(ProtocolMsg{tag: tag})
|
||||
}
|
||||
|
||||
func (conn *LocalConnection) sendProtocolMsg(m ProtocolMsg) error {
|
||||
return conn.tcpSender.Send(append([]byte{byte(m.tag)}, m.msg...))
|
||||
}
|
||||
|
||||
func (conn *LocalConnection) receiveTCP(receiver TCPReceiver) {
|
||||
var err error
|
||||
for {
|
||||
conn.extendReadDeadline()
|
||||
|
||||
var msg []byte
|
||||
if msg, err = receiver.Receive(); err != nil {
|
||||
break
|
||||
}
|
||||
if len(msg) < 1 {
|
||||
conn.Log("ignoring blank msg")
|
||||
continue
|
||||
}
|
||||
if err = conn.handleProtocolMsg(ProtocolTag(msg[0]), msg[1:]); err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
conn.Shutdown(err)
|
||||
}
|
||||
|
||||
func (conn *LocalConnection) handleProtocolMsg(tag ProtocolTag, payload []byte) error {
|
||||
switch tag {
|
||||
case ProtocolHeartbeat:
|
||||
case ProtocolReserved1, ProtocolReserved2, ProtocolReserved3, ProtocolOverlayControlMsg:
|
||||
conn.OverlayConn.ControlMessage(byte(tag), payload)
|
||||
case ProtocolGossipUnicast, ProtocolGossipBroadcast, ProtocolGossip:
|
||||
return conn.Router.handleGossip(tag, payload)
|
||||
default:
|
||||
conn.Log("ignoring unknown protocol tag:", tag)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (conn *LocalConnection) extendReadDeadline() {
|
||||
conn.TCPConn.SetReadDeadline(time.Now().Add(TCPHeartbeat * 2))
|
||||
}
|
||||
|
||||
func (conn *LocalConnection) Untrusted() bool {
|
||||
return !conn.TrustRemote || !conn.TrustedByRemote
|
||||
}
|
||||
@@ -0,0 +1,317 @@
|
||||
package mesh
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
InitialInterval = 2 * time.Second
|
||||
MaxInterval = 6 * time.Minute
|
||||
ResetAfter = 1 * time.Minute
|
||||
)
|
||||
|
||||
type peerAddrs map[string]*net.TCPAddr
|
||||
|
||||
type ConnectionMaker struct {
|
||||
ourself *LocalPeer
|
||||
peers *Peers
|
||||
port int
|
||||
discovery bool
|
||||
targets map[string]*Target
|
||||
connections map[Connection]struct{}
|
||||
directPeers peerAddrs
|
||||
actionChan chan<- ConnectionMakerAction
|
||||
}
|
||||
|
||||
type TargetState int
|
||||
|
||||
const (
|
||||
TargetWaiting TargetState = iota // we are waiting to connect there
|
||||
TargetAttempting // we are attempting to connect there
|
||||
TargetConnected // we are connected to there
|
||||
)
|
||||
|
||||
// Information about an address where we may find a peer
|
||||
type Target struct {
|
||||
state TargetState
|
||||
lastError error // reason for disconnection last time
|
||||
tryAfter time.Time // next time to try this address
|
||||
tryInterval time.Duration // retry delay on next failure
|
||||
}
|
||||
|
||||
type ConnectionMakerAction func() bool
|
||||
|
||||
func NewConnectionMaker(ourself *LocalPeer, peers *Peers, port int, discovery bool) *ConnectionMaker {
|
||||
actionChan := make(chan ConnectionMakerAction, ChannelSize)
|
||||
cm := &ConnectionMaker{
|
||||
ourself: ourself,
|
||||
peers: peers,
|
||||
port: port,
|
||||
discovery: discovery,
|
||||
directPeers: peerAddrs{},
|
||||
targets: make(map[string]*Target),
|
||||
connections: make(map[Connection]struct{}),
|
||||
actionChan: actionChan}
|
||||
go cm.queryLoop(actionChan)
|
||||
return cm
|
||||
}
|
||||
|
||||
func (cm *ConnectionMaker) InitiateConnections(peers []string, replace bool) []error {
|
||||
errors := []error{}
|
||||
addrs := peerAddrs{}
|
||||
for _, peer := range peers {
|
||||
host, port, err := net.SplitHostPort(peer)
|
||||
if err != nil {
|
||||
host = peer
|
||||
port = "0" // we use that as an indication that "no port was supplied"
|
||||
}
|
||||
if addr, err := net.ResolveTCPAddr("tcp4", fmt.Sprintf("%s:%s", host, port)); err != nil {
|
||||
errors = append(errors, err)
|
||||
} else {
|
||||
addrs[peer] = addr
|
||||
}
|
||||
}
|
||||
cm.actionChan <- func() bool {
|
||||
if replace {
|
||||
cm.directPeers = peerAddrs{}
|
||||
}
|
||||
for peer, addr := range addrs {
|
||||
cm.directPeers[peer] = addr
|
||||
// curtail any existing reconnect interval
|
||||
if target, found := cm.targets[cm.completeAddr(*addr)]; found {
|
||||
target.nextTryNow()
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
func (cm *ConnectionMaker) ForgetConnections(peers []string) {
|
||||
cm.actionChan <- func() bool {
|
||||
for _, peer := range peers {
|
||||
delete(cm.directPeers, peer)
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (cm *ConnectionMaker) ConnectionAborted(address string, err error) {
|
||||
cm.actionChan <- func() bool {
|
||||
target := cm.targets[address]
|
||||
target.state = TargetWaiting
|
||||
target.lastError = err
|
||||
target.nextTryLater()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func (cm *ConnectionMaker) ConnectionCreated(conn Connection) {
|
||||
cm.actionChan <- func() bool {
|
||||
cm.connections[conn] = void
|
||||
if conn.Outbound() {
|
||||
target := cm.targets[conn.RemoteTCPAddr()]
|
||||
target.state = TargetConnected
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (cm *ConnectionMaker) ConnectionTerminated(conn Connection, err error) {
|
||||
cm.actionChan <- func() bool {
|
||||
delete(cm.connections, conn)
|
||||
if conn.Outbound() {
|
||||
target := cm.targets[conn.RemoteTCPAddr()]
|
||||
target.state = TargetWaiting
|
||||
target.lastError = err
|
||||
switch {
|
||||
case err == ErrConnectToSelf:
|
||||
target.nextTryNever()
|
||||
case time.Now().After(target.tryAfter.Add(ResetAfter)):
|
||||
target.nextTryNow()
|
||||
default:
|
||||
target.nextTryLater()
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func (cm *ConnectionMaker) Refresh() {
|
||||
cm.actionChan <- func() bool { return true }
|
||||
}
|
||||
|
||||
func (cm *ConnectionMaker) queryLoop(actionChan <-chan ConnectionMakerAction) {
|
||||
timer := time.NewTimer(MaxDuration)
|
||||
run := func() { timer.Reset(cm.checkStateAndAttemptConnections()) }
|
||||
for {
|
||||
select {
|
||||
case action := <-actionChan:
|
||||
if action() {
|
||||
run()
|
||||
}
|
||||
case <-timer.C:
|
||||
run()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (cm *ConnectionMaker) completeAddr(addr net.TCPAddr) string {
|
||||
if addr.Port == 0 {
|
||||
addr.Port = cm.port
|
||||
}
|
||||
return addr.String()
|
||||
}
|
||||
|
||||
func (cm *ConnectionMaker) checkStateAndAttemptConnections() time.Duration {
|
||||
var (
|
||||
validTarget = make(map[string]struct{})
|
||||
directTarget = make(map[string]struct{})
|
||||
)
|
||||
ourConnectedPeers, ourConnectedTargets, ourInboundIPs := cm.ourConnections()
|
||||
|
||||
addTarget := func(address string) {
|
||||
if _, connected := ourConnectedTargets[address]; connected {
|
||||
return
|
||||
}
|
||||
validTarget[address] = void
|
||||
if _, found := cm.targets[address]; found {
|
||||
return
|
||||
}
|
||||
target := &Target{state: TargetWaiting}
|
||||
target.nextTryNow()
|
||||
cm.targets[address] = target
|
||||
}
|
||||
|
||||
// Add direct targets that are not connected
|
||||
for _, addr := range cm.directPeers {
|
||||
attempt := true
|
||||
if addr.Port == 0 {
|
||||
// If a peer was specified w/o a port, then we do not
|
||||
// attempt to connect to it if we have any inbound
|
||||
// connections from that IP.
|
||||
if _, connected := ourInboundIPs[addr.IP.String()]; connected {
|
||||
attempt = false
|
||||
}
|
||||
}
|
||||
address := cm.completeAddr(*addr)
|
||||
directTarget[address] = void
|
||||
if attempt {
|
||||
addTarget(address)
|
||||
}
|
||||
}
|
||||
|
||||
// Add targets for peers that someone else is connected to, but we
|
||||
// aren't
|
||||
if cm.discovery {
|
||||
cm.addPeerTargets(ourConnectedPeers, addTarget)
|
||||
}
|
||||
|
||||
return cm.connectToTargets(validTarget, directTarget)
|
||||
}
|
||||
|
||||
func (cm *ConnectionMaker) ourConnections() (PeerNameSet, map[string]struct{}, map[string]struct{}) {
|
||||
var (
|
||||
ourConnectedPeers = make(PeerNameSet)
|
||||
ourConnectedTargets = make(map[string]struct{})
|
||||
ourInboundIPs = make(map[string]struct{})
|
||||
)
|
||||
for conn := range cm.connections {
|
||||
address := conn.RemoteTCPAddr()
|
||||
ourConnectedPeers[conn.Remote().Name] = void
|
||||
ourConnectedTargets[address] = void
|
||||
if conn.Outbound() {
|
||||
continue
|
||||
}
|
||||
if ip, _, err := net.SplitHostPort(address); err == nil { // should always succeed
|
||||
ourInboundIPs[ip] = void
|
||||
}
|
||||
}
|
||||
return ourConnectedPeers, ourConnectedTargets, ourInboundIPs
|
||||
}
|
||||
|
||||
func (cm *ConnectionMaker) addPeerTargets(ourConnectedPeers PeerNameSet, addTarget func(string)) {
|
||||
cm.peers.ForEach(func(peer *Peer) {
|
||||
if peer == cm.ourself.Peer {
|
||||
return
|
||||
}
|
||||
// Modifying peer.connections requires a write lock on Peers,
|
||||
// and since we are holding a read lock (due to the ForEach),
|
||||
// access without locking the peer is safe.
|
||||
for otherPeer, conn := range peer.connections {
|
||||
if otherPeer == cm.ourself.Name {
|
||||
continue
|
||||
}
|
||||
if _, connected := ourConnectedPeers[otherPeer]; connected {
|
||||
continue
|
||||
}
|
||||
address := conn.RemoteTCPAddr()
|
||||
if conn.Outbound() {
|
||||
addTarget(address)
|
||||
} else if ip, _, err := net.SplitHostPort(address); err == nil {
|
||||
// There is no point connecting to the (likely
|
||||
// ephemeral) remote port of an inbound connection
|
||||
// that some peer has. Let's try to connect on the
|
||||
// weave port instead.
|
||||
addTarget(fmt.Sprintf("%s:%d", ip, cm.port))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (cm *ConnectionMaker) connectToTargets(validTarget map[string]struct{}, directTarget map[string]struct{}) time.Duration {
|
||||
now := time.Now() // make sure we catch items just added
|
||||
after := MaxDuration
|
||||
for address, target := range cm.targets {
|
||||
if target.state != TargetWaiting {
|
||||
continue
|
||||
}
|
||||
if _, valid := validTarget[address]; !valid {
|
||||
delete(cm.targets, address)
|
||||
continue
|
||||
}
|
||||
if target.tryAfter.IsZero() {
|
||||
continue
|
||||
}
|
||||
switch duration := target.tryAfter.Sub(now); {
|
||||
case duration <= 0:
|
||||
target.state = TargetAttempting
|
||||
_, isCmdLineTarget := directTarget[address]
|
||||
go cm.attemptConnection(address, isCmdLineTarget)
|
||||
case duration < after:
|
||||
after = duration
|
||||
}
|
||||
}
|
||||
return after
|
||||
}
|
||||
|
||||
func (cm *ConnectionMaker) attemptConnection(address string, acceptNewPeer bool) {
|
||||
log.Printf("->[%s] attempting connection", address)
|
||||
if err := cm.ourself.CreateConnection(address, acceptNewPeer); err != nil {
|
||||
log.Errorf("->[%s] error during connection attempt: %v", address, err)
|
||||
cm.ConnectionAborted(address, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Target) nextTryNever() {
|
||||
t.tryAfter = time.Time{}
|
||||
t.tryInterval = MaxInterval
|
||||
}
|
||||
|
||||
func (t *Target) nextTryNow() {
|
||||
t.tryAfter = time.Now()
|
||||
t.tryInterval = InitialInterval
|
||||
}
|
||||
|
||||
// The delay at the nth retry is a random value in the range
|
||||
// [i-i/2,i+i/2], where i = InitialInterval * 1.5^(n-1).
|
||||
func (t *Target) nextTryLater() {
|
||||
t.tryAfter = time.Now().Add(t.tryInterval/2 + time.Duration(rand.Int63n(int64(t.tryInterval))))
|
||||
t.tryInterval = t.tryInterval * 3 / 2
|
||||
if t.tryInterval > MaxInterval {
|
||||
t.tryInterval = MaxInterval
|
||||
}
|
||||
}
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
package mesh
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type GossipData interface {
|
||||
Encode() [][]byte
|
||||
Merge(GossipData) GossipData
|
||||
}
|
||||
|
||||
type Gossip interface {
|
||||
// specific message from one peer to another
|
||||
// intermediate peers relay it using unicast topology.
|
||||
GossipUnicast(dstPeerName PeerName, msg []byte) error
|
||||
// send gossip to every peer, relayed using broadcast topology.
|
||||
GossipBroadcast(update GossipData) error
|
||||
}
|
||||
|
||||
type Gossiper interface {
|
||||
OnGossipUnicast(sender PeerName, msg []byte) error
|
||||
// merge received data into state and return a representation of
|
||||
// the received data, for further propagation
|
||||
OnGossipBroadcast(sender PeerName, update []byte) (GossipData, error)
|
||||
// return state of everything we know; gets called periodically
|
||||
Gossip() GossipData
|
||||
// merge received data into state and return "everything new I've
|
||||
// just learnt", or nil if nothing in the received data was new
|
||||
OnGossip(update []byte) (GossipData, error)
|
||||
}
|
||||
|
||||
// Accumulates GossipData that needs to be sent to one destination,
|
||||
// and sends it when possible.
|
||||
type GossipSender struct {
|
||||
send func(GossipData)
|
||||
cell chan GossipData
|
||||
// for testing
|
||||
sent bool
|
||||
flushch chan chan bool
|
||||
}
|
||||
|
||||
func NewGossipSender(send func(GossipData)) *GossipSender {
|
||||
cell := make(chan GossipData, 1)
|
||||
flushch := make(chan chan bool)
|
||||
sender := &GossipSender{
|
||||
send: send,
|
||||
cell: cell,
|
||||
flushch: flushch,
|
||||
}
|
||||
go sender.run()
|
||||
return sender
|
||||
}
|
||||
|
||||
func (sender *GossipSender) run() {
|
||||
for {
|
||||
select {
|
||||
case pending := <-sender.cell:
|
||||
if pending == nil { // receive zero value when chan is closed
|
||||
return
|
||||
}
|
||||
sender.send(pending)
|
||||
sender.sent = true
|
||||
case ch := <-sender.flushch:
|
||||
// send anything pending, then reply back whether we sent
|
||||
// anything since previous flush
|
||||
select {
|
||||
case pending := <-sender.cell:
|
||||
sender.send(pending)
|
||||
sender.sent = true
|
||||
default:
|
||||
}
|
||||
ch <- sender.sent
|
||||
sender.sent = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (sender *GossipSender) Send(data GossipData) {
|
||||
// NB: this must not be invoked concurrently
|
||||
select {
|
||||
case pending := <-sender.cell:
|
||||
sender.cell <- pending.Merge(data)
|
||||
default:
|
||||
sender.cell <- data
|
||||
}
|
||||
}
|
||||
|
||||
func (sender *GossipSender) Stop() {
|
||||
close(sender.cell)
|
||||
}
|
||||
|
||||
type GossipChannels map[string]*GossipChannel
|
||||
|
||||
func (router *Router) NewGossip(channelName string, g Gossiper) Gossip {
|
||||
channel := NewGossipChannel(channelName, router.Ourself, router.Routes, g)
|
||||
router.gossipLock.Lock()
|
||||
defer router.gossipLock.Unlock()
|
||||
if _, found := router.gossipChannels[channelName]; found {
|
||||
checkFatal(fmt.Errorf("[gossip] duplicate channel %s", channelName))
|
||||
}
|
||||
router.gossipChannels[channelName] = channel
|
||||
return channel
|
||||
}
|
||||
|
||||
func (router *Router) gossipChannel(channelName string) *GossipChannel {
|
||||
router.gossipLock.RLock()
|
||||
channel, found := router.gossipChannels[channelName]
|
||||
router.gossipLock.RUnlock()
|
||||
if found {
|
||||
return channel
|
||||
}
|
||||
router.gossipLock.Lock()
|
||||
defer router.gossipLock.Unlock()
|
||||
if channel, found = router.gossipChannels[channelName]; found {
|
||||
return channel
|
||||
}
|
||||
channel = NewGossipChannel(channelName, router.Ourself, router.Routes, &surrogateGossiper)
|
||||
channel.log("created surrogate channel")
|
||||
router.gossipChannels[channelName] = channel
|
||||
return channel
|
||||
}
|
||||
|
||||
func (router *Router) gossipChannelSet() map[*GossipChannel]struct{} {
|
||||
channels := make(map[*GossipChannel]struct{})
|
||||
router.gossipLock.RLock()
|
||||
defer router.gossipLock.RUnlock()
|
||||
for _, channel := range router.gossipChannels {
|
||||
channels[channel] = void
|
||||
}
|
||||
return channels
|
||||
}
|
||||
|
||||
func (router *Router) SendAllGossip() {
|
||||
for channel := range router.gossipChannelSet() {
|
||||
if gossip := channel.gossiper.Gossip(); gossip != nil {
|
||||
channel.Send(router.Ourself.Name, gossip)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (router *Router) SendAllGossipDown(conn Connection) {
|
||||
for channel := range router.gossipChannelSet() {
|
||||
if gossip := channel.gossiper.Gossip(); gossip != nil {
|
||||
channel.SendDown(conn, gossip)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// for testing
|
||||
|
||||
func (router *Router) sendPendingGossip() bool {
|
||||
sentSomething := false
|
||||
for channel := range router.gossipChannelSet() {
|
||||
channel.Lock()
|
||||
for _, sender := range channel.senders {
|
||||
sentSomething = sender.flush() || sentSomething
|
||||
}
|
||||
for _, sender := range channel.broadcasters {
|
||||
sentSomething = sender.flush() || sentSomething
|
||||
}
|
||||
channel.Unlock()
|
||||
}
|
||||
return sentSomething
|
||||
}
|
||||
|
||||
func (sender *GossipSender) flush() bool {
|
||||
ch := make(chan bool)
|
||||
sender.flushch <- ch
|
||||
return <-ch
|
||||
}
|
||||
@@ -1,132 +1,42 @@
|
||||
package router
|
||||
package mesh
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const GossipInterval = 30 * time.Second
|
||||
|
||||
type GossipData interface {
|
||||
Encode() []byte
|
||||
Merge(GossipData)
|
||||
}
|
||||
|
||||
type Gossip interface {
|
||||
// specific message from one peer to another
|
||||
// intermediate peers relay it using unicast topology.
|
||||
GossipUnicast(dstPeerName PeerName, msg []byte) error
|
||||
// send gossip to every peer, relayed using broadcast topology.
|
||||
GossipBroadcast(update GossipData) error
|
||||
}
|
||||
|
||||
type Gossiper interface {
|
||||
OnGossipUnicast(sender PeerName, msg []byte) error
|
||||
// merge received data into state and return a representation of
|
||||
// the received data, for further propagation
|
||||
OnGossipBroadcast(update []byte) (GossipData, error)
|
||||
// return state of everything we know; gets called periodically
|
||||
Gossip() GossipData
|
||||
// merge received data into state and return "everything new I've
|
||||
// just learnt", or nil if nothing in the received data was new
|
||||
OnGossip(update []byte) (GossipData, error)
|
||||
}
|
||||
|
||||
// Accumulates GossipData that needs to be sent to one destination,
|
||||
// and sends it when possible.
|
||||
type GossipSender struct {
|
||||
send func(GossipData)
|
||||
cell chan GossipData
|
||||
}
|
||||
|
||||
func NewGossipSender(send func(GossipData)) *GossipSender {
|
||||
return &GossipSender{send: send}
|
||||
}
|
||||
|
||||
func (sender *GossipSender) Start() {
|
||||
sender.cell = make(chan GossipData, 1)
|
||||
go sender.run()
|
||||
}
|
||||
|
||||
func (sender *GossipSender) run() {
|
||||
for {
|
||||
if pending := <-sender.cell; pending == nil { // receive zero value when chan is closed
|
||||
break
|
||||
} else {
|
||||
sender.send(pending)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (sender *GossipSender) Send(data GossipData) {
|
||||
// NB: this must not be invoked concurrently
|
||||
select {
|
||||
case pending := <-sender.cell:
|
||||
pending.Merge(data)
|
||||
sender.cell <- pending
|
||||
default:
|
||||
sender.cell <- data
|
||||
}
|
||||
}
|
||||
|
||||
func (sender *GossipSender) Stop() {
|
||||
close(sender.cell)
|
||||
}
|
||||
|
||||
type connectionSenders map[Connection]*GossipSender
|
||||
type peerSenders map[PeerName]*GossipSender
|
||||
|
||||
type GossipChannel struct {
|
||||
sync.Mutex
|
||||
name string
|
||||
ourself *LocalPeer
|
||||
routes *Routes
|
||||
name string
|
||||
hash uint32
|
||||
gossiper Gossiper
|
||||
senders connectionSenders
|
||||
broadcasters peerSenders
|
||||
}
|
||||
|
||||
func (router *Router) NewGossip(channelName string, g Gossiper) Gossip {
|
||||
channelHash := hash(channelName)
|
||||
channel := &GossipChannel{
|
||||
ourself: router.Ourself,
|
||||
routes: router.Routes,
|
||||
type connectionSenders map[Connection]*GossipSender
|
||||
type peerSenders map[PeerName]*GossipSender
|
||||
|
||||
func NewGossipChannel(channelName string, ourself *LocalPeer, routes *Routes, g Gossiper) *GossipChannel {
|
||||
return &GossipChannel{
|
||||
name: channelName,
|
||||
hash: channelHash,
|
||||
ourself: ourself,
|
||||
routes: routes,
|
||||
gossiper: g,
|
||||
senders: make(connectionSenders),
|
||||
broadcasters: make(peerSenders)}
|
||||
router.GossipChannels[channelHash] = channel
|
||||
return channel
|
||||
}
|
||||
|
||||
func (router *Router) SendAllGossip() {
|
||||
for _, channel := range router.GossipChannels {
|
||||
channel.Send(router.Ourself.Name, channel.gossiper.Gossip())
|
||||
}
|
||||
}
|
||||
|
||||
func (router *Router) SendAllGossipDown(conn Connection) {
|
||||
for _, channel := range router.GossipChannels {
|
||||
channel.SendDown(conn, channel.gossiper.Gossip())
|
||||
}
|
||||
}
|
||||
|
||||
func (router *Router) handleGossip(tag ProtocolTag, payload []byte) error {
|
||||
decoder := gob.NewDecoder(bytes.NewReader(payload))
|
||||
var channelHash uint32
|
||||
if err := decoder.Decode(&channelHash); err != nil {
|
||||
var channelName string
|
||||
if err := decoder.Decode(&channelName); err != nil {
|
||||
return err
|
||||
}
|
||||
channel, found := router.GossipChannels[channelHash]
|
||||
if !found {
|
||||
return fmt.Errorf("[gossip] received unknown channel with hash %v", channelHash)
|
||||
}
|
||||
channel := router.gossipChannel(channelName)
|
||||
var srcName PeerName
|
||||
if err := decoder.Decode(&srcName); err != nil {
|
||||
return err
|
||||
@@ -148,7 +58,12 @@ func (c *GossipChannel) deliverUnicast(srcName PeerName, origPayload []byte, dec
|
||||
return err
|
||||
}
|
||||
if c.ourself.Name != destName {
|
||||
return c.relayUnicast(destName, origPayload)
|
||||
if err := c.relayUnicast(destName, origPayload); err != nil {
|
||||
// just log errors from relayUnicast; a problem between us and destination
|
||||
// is not enough reason to break the connection from the source
|
||||
c.log(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
var payload []byte
|
||||
if err := dec.Decode(&payload); err != nil {
|
||||
@@ -162,7 +77,7 @@ func (c *GossipChannel) deliverBroadcast(srcName PeerName, _ []byte, dec *gob.De
|
||||
if err := dec.Decode(&payload); err != nil {
|
||||
return err
|
||||
}
|
||||
data, err := c.gossiper.OnGossipBroadcast(payload)
|
||||
data, err := c.gossiper.OnGossipBroadcast(srcName, payload)
|
||||
if err != nil || data == nil {
|
||||
return err
|
||||
}
|
||||
@@ -182,81 +97,23 @@ func (c *GossipChannel) deliver(srcName PeerName, _ []byte, dec *gob.Decoder) er
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *GossipChannel) Send(srcName PeerName, data GossipData) {
|
||||
// do this outside the lock below so we avoid lock nesting
|
||||
c.routes.EnsureRecalculated()
|
||||
selectedConnections := make(ConnectionSet)
|
||||
for name := range c.routes.RandomNeighbours(srcName) {
|
||||
if conn, found := c.ourself.ConnectionTo(name); found {
|
||||
selectedConnections[conn] = void
|
||||
}
|
||||
}
|
||||
if len(selectedConnections) == 0 {
|
||||
return
|
||||
}
|
||||
connections := c.ourself.Connections()
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
// GC - randomly (courtesy of go's map iterator) pick some
|
||||
// existing entries and stop&remove them if the associated
|
||||
// connection is no longer active. We stop as soon as we
|
||||
// encounter a valid entry; the idea being that when there is
|
||||
// little or no garbage then this executes close to O(1)[1],
|
||||
// whereas when there is lots of garbage we remove it quickly.
|
||||
//
|
||||
// [1] TODO Unfortunately, due to the desire to avoid nested
|
||||
// locks, instead of simply invoking Peer.ConnectionTo(name)
|
||||
// below, we have that Peer.Connections() invocation above. That
|
||||
// is O(n_our_connections) at best.
|
||||
for conn, sender := range c.senders {
|
||||
if _, found := connections[conn]; !found {
|
||||
delete(c.senders, conn)
|
||||
sender.Stop()
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
for conn := range selectedConnections {
|
||||
c.sendDown(conn, data)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *GossipChannel) SendDown(conn Connection, data GossipData) {
|
||||
c.Lock()
|
||||
c.sendDown(conn, data)
|
||||
c.Unlock()
|
||||
}
|
||||
|
||||
func (c *GossipChannel) sendDown(conn Connection, data GossipData) {
|
||||
sender, found := c.senders[conn]
|
||||
if !found {
|
||||
sender = NewGossipSender(func(pending GossipData) {
|
||||
protocolMsg := ProtocolMsg{ProtocolGossip, GobEncode(c.hash, c.ourself.Name, pending.Encode())}
|
||||
conn.(ProtocolSender).SendProtocolMsg(protocolMsg)
|
||||
})
|
||||
c.senders[conn] = sender
|
||||
sender.Start()
|
||||
}
|
||||
sender.Send(data)
|
||||
}
|
||||
|
||||
func (c *GossipChannel) GossipUnicast(dstPeerName PeerName, msg []byte) error {
|
||||
return c.relayUnicast(dstPeerName, GobEncode(c.hash, c.ourself.Name, dstPeerName, msg))
|
||||
return c.relayUnicast(dstPeerName, GobEncode(c.name, c.ourself.Name, dstPeerName, msg))
|
||||
}
|
||||
|
||||
func (c *GossipChannel) GossipBroadcast(update GossipData) error {
|
||||
return c.relayBroadcast(c.ourself.Name, update)
|
||||
}
|
||||
|
||||
func (c *GossipChannel) relayUnicast(dstPeerName PeerName, buf []byte) error {
|
||||
func (c *GossipChannel) relayUnicast(dstPeerName PeerName, buf []byte) (err error) {
|
||||
if relayPeerName, found := c.routes.UnicastAll(dstPeerName); !found {
|
||||
c.log("unknown relay destination:", dstPeerName)
|
||||
err = fmt.Errorf("unknown relay destination: %s", dstPeerName)
|
||||
} else if conn, found := c.ourself.ConnectionTo(relayPeerName); !found {
|
||||
c.log("unable to find connection to relay peer", relayPeerName)
|
||||
err = fmt.Errorf("unable to find connection to relay peer %s", relayPeerName)
|
||||
} else {
|
||||
conn.(ProtocolSender).SendProtocolMsg(ProtocolMsg{ProtocolGossipUnicast, buf})
|
||||
}
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *GossipChannel) relayBroadcast(srcName PeerName, update GossipData) error {
|
||||
@@ -286,7 +143,6 @@ func (c *GossipChannel) relayBroadcast(srcName PeerName, update GossipData) erro
|
||||
if !found {
|
||||
broadcaster = NewGossipSender(func(pending GossipData) { c.sendBroadcast(srcName, pending) })
|
||||
c.broadcasters[srcName] = broadcaster
|
||||
broadcaster.Start()
|
||||
}
|
||||
broadcaster.Send(update)
|
||||
return nil
|
||||
@@ -298,40 +154,91 @@ func (c *GossipChannel) sendBroadcast(srcName PeerName, update GossipData) {
|
||||
if len(nextHops) == 0 {
|
||||
return
|
||||
}
|
||||
protocolMsg := ProtocolMsg{ProtocolGossipBroadcast, GobEncode(c.hash, srcName, update.Encode())}
|
||||
// FIXME a single blocked connection can stall us
|
||||
for _, conn := range c.ourself.ConnectionsTo(nextHops) {
|
||||
conn.(ProtocolSender).SendProtocolMsg(protocolMsg)
|
||||
connections := c.ourself.ConnectionsTo(nextHops)
|
||||
for _, msg := range update.Encode() {
|
||||
protocolMsg := ProtocolMsg{ProtocolGossipBroadcast, GobEncode(c.name, srcName, msg)}
|
||||
// FIXME a single blocked connection can stall us
|
||||
for _, conn := range connections {
|
||||
conn.(ProtocolSender).SendProtocolMsg(protocolMsg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *GossipChannel) Send(srcName PeerName, data GossipData) {
|
||||
c.routes.EnsureRecalculated()
|
||||
destinations := c.routes.RandomNeighbours(srcName)
|
||||
if len(destinations) == 0 {
|
||||
return
|
||||
}
|
||||
c.sendDown(c.ourself.ConnectionsTo(destinations), data)
|
||||
}
|
||||
|
||||
func (c *GossipChannel) SendDown(conn Connection, data GossipData) {
|
||||
c.sendDown([]Connection{conn}, data)
|
||||
}
|
||||
|
||||
func (c *GossipChannel) sendDown(selectedConnections []Connection, data GossipData) {
|
||||
if len(selectedConnections) == 0 {
|
||||
return
|
||||
}
|
||||
connections := c.ourself.Connections()
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
// GC - randomly (courtesy of go's map iterator) pick some
|
||||
// existing senders and stop&remove them if the associated
|
||||
// connection is no longer active. We stop as soon as we
|
||||
// encounter a valid entry; the idea being that when there is
|
||||
// little or no garbage then this executes close to O(1)[1],
|
||||
// whereas when there is lots of garbage we remove it quickly.
|
||||
//
|
||||
// [1] TODO Unfortunately, due to the desire to avoid nested
|
||||
// locks, instead of simply invoking LocalPeer.ConnectionTo(name),
|
||||
// we operate on LocalPeer.Connections(). That is
|
||||
// O(n_our_connections) at best.
|
||||
for conn, sender := range c.senders {
|
||||
if _, found := connections[conn]; !found {
|
||||
delete(c.senders, conn)
|
||||
sender.Stop()
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
// start senders, if necessary, and send.
|
||||
for _, conn := range selectedConnections {
|
||||
sender, found := c.senders[conn]
|
||||
if !found {
|
||||
sender = c.makeSender(conn)
|
||||
c.senders[conn] = sender
|
||||
}
|
||||
sender.Send(data)
|
||||
}
|
||||
}
|
||||
|
||||
// We have seen a couple of failures which suggest a >128GB slice was encountered.
|
||||
// 100MB should be enough for anyone.
|
||||
const maxFeasibleMessageLen = 100 * 1024 * 1024
|
||||
|
||||
func (c *GossipChannel) makeSender(conn Connection) *GossipSender {
|
||||
return NewGossipSender(func(pending GossipData) {
|
||||
for _, msg := range pending.Encode() {
|
||||
if len(msg) > maxFeasibleMessageLen {
|
||||
panic(fmt.Sprintf("Gossip message too large: len=%d bytes; on channel '%s' from %+v", len(msg), c.name, pending))
|
||||
}
|
||||
protocolMsg := ProtocolMsg{ProtocolGossip, GobEncode(c.name, c.ourself.Name, msg)}
|
||||
conn.(ProtocolSender).SendProtocolMsg(protocolMsg)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (c *GossipChannel) log(args ...interface{}) {
|
||||
log.Println(append(append([]interface{}{}, "[gossip "+c.name+"]:"), args...)...)
|
||||
}
|
||||
|
||||
// for testing
|
||||
|
||||
// FIXME this doesn't actually guarantee everything has been sent
|
||||
// since a GossipSender may be in the process of sending and there is
|
||||
// no easy way for us to know when that has completed.
|
||||
func (router *Router) sendPendingGossip() {
|
||||
for _, channel := range router.GossipChannels {
|
||||
for _, sender := range channel.senders {
|
||||
sender.flush()
|
||||
}
|
||||
for _, sender := range channel.broadcasters {
|
||||
sender.flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (sender *GossipSender) flush() {
|
||||
for {
|
||||
select {
|
||||
case pending := <-sender.cell:
|
||||
sender.send(pending)
|
||||
default:
|
||||
return
|
||||
}
|
||||
func GobEncode(items ...interface{}) []byte {
|
||||
buf := new(bytes.Buffer)
|
||||
enc := gob.NewEncoder(buf)
|
||||
for _, i := range items {
|
||||
checkFatal(enc.Encode(i))
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
package mesh
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TODO test gossip unicast; atm we only test topology gossip and
|
||||
// surrogates, neither of which employ unicast.
|
||||
|
||||
type mockGossipConnection struct {
|
||||
RemoteConnection
|
||||
dest *Router
|
||||
start chan struct{}
|
||||
}
|
||||
|
||||
func NewTestRouter(name string) *Router {
|
||||
peerName, _ := PeerNameFromString(name)
|
||||
router := NewRouter(Config{}, peerName, "nick", nil)
|
||||
router.Start()
|
||||
return router
|
||||
}
|
||||
|
||||
func (conn *mockGossipConnection) SendProtocolMsg(protocolMsg ProtocolMsg) {
|
||||
<-conn.start
|
||||
if err := conn.dest.handleGossip(protocolMsg.tag, protocolMsg.msg); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (conn *mockGossipConnection) Start() {
|
||||
close(conn.start)
|
||||
}
|
||||
|
||||
func sendPendingGossip(routers ...*Router) {
|
||||
// Loop until all routers report they didn't send anything
|
||||
for sentSomething := true; sentSomething; {
|
||||
sentSomething = false
|
||||
for _, router := range routers {
|
||||
sentSomething = router.sendPendingGossip() || sentSomething
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func AddTestGossipConnection(r1, r2 *Router) {
|
||||
c1 := r1.NewTestGossipConnection(r2)
|
||||
c2 := r2.NewTestGossipConnection(r1)
|
||||
c1.Start()
|
||||
c2.Start()
|
||||
}
|
||||
|
||||
func (router *Router) NewTestGossipConnection(r *Router) *mockGossipConnection {
|
||||
to := r.Ourself.Peer
|
||||
toPeer := NewPeer(to.Name, to.NickName, to.UID, 0, to.ShortID)
|
||||
toPeer = router.Peers.FetchWithDefault(toPeer) // Has side-effect of incrementing refcount
|
||||
|
||||
conn := &mockGossipConnection{
|
||||
RemoteConnection{router.Ourself.Peer, toPeer, "", false, true}, r, make(chan struct{})}
|
||||
router.Ourself.handleAddConnection(conn)
|
||||
router.Ourself.handleConnectionEstablished(conn)
|
||||
return conn
|
||||
}
|
||||
|
||||
func (router *Router) DeleteTestGossipConnection(r *Router) {
|
||||
toName := r.Ourself.Peer.Name
|
||||
conn, _ := router.Ourself.ConnectionTo(toName)
|
||||
router.Peers.Dereference(conn.Remote())
|
||||
router.Ourself.handleDeleteConnection(conn)
|
||||
}
|
||||
|
||||
// Create a Peer representing the receiver router, with connections to
|
||||
// the routers supplied as arguments, carrying across all UID and
|
||||
// version information.
|
||||
func (router *Router) tp(routers ...*Router) *Peer {
|
||||
peer := NewPeerFrom(router.Ourself.Peer)
|
||||
connections := make(map[PeerName]Connection)
|
||||
for _, r := range routers {
|
||||
p := NewPeerFrom(r.Ourself.Peer)
|
||||
connections[r.Ourself.Peer.Name] = newMockConnection(peer, p)
|
||||
}
|
||||
peer.Version = router.Ourself.Peer.Version
|
||||
peer.connections = connections
|
||||
return peer
|
||||
}
|
||||
|
||||
// Check that the topology of router matches the peers and all of their connections
|
||||
func checkTopology(t *testing.T, router *Router, wantedPeers ...*Peer) {
|
||||
router.Peers.RLock()
|
||||
checkTopologyPeers(t, true, router.Peers.allPeers(), wantedPeers...)
|
||||
router.Peers.RUnlock()
|
||||
}
|
||||
|
||||
func flushAndCheckTopology(t *testing.T, routers []*Router, wantedPeers ...*Peer) {
|
||||
sendPendingGossip(routers...)
|
||||
for _, r := range routers {
|
||||
checkTopology(t, r, wantedPeers...)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGossipTopology(t *testing.T) {
|
||||
// Create some peers that will talk to each other
|
||||
r1 := NewTestRouter("01:00:00:01:00:00")
|
||||
r2 := NewTestRouter("02:00:00:02:00:00")
|
||||
r3 := NewTestRouter("03:00:00:03:00:00")
|
||||
routers := []*Router{r1, r2, r3}
|
||||
// Check state when they have no connections
|
||||
checkTopology(t, r1, r1.tp())
|
||||
checkTopology(t, r2, r2.tp())
|
||||
|
||||
// Now try adding some connections
|
||||
AddTestGossipConnection(r1, r2)
|
||||
sendPendingGossip(r1, r2)
|
||||
checkTopology(t, r1, r1.tp(r2), r2.tp(r1))
|
||||
checkTopology(t, r2, r1.tp(r2), r2.tp(r1))
|
||||
|
||||
AddTestGossipConnection(r2, r3)
|
||||
flushAndCheckTopology(t, routers, r1.tp(r2), r2.tp(r1, r3), r3.tp(r2))
|
||||
|
||||
AddTestGossipConnection(r3, r1)
|
||||
flushAndCheckTopology(t, routers, r1.tp(r2, r3), r2.tp(r1, r3), r3.tp(r1, r2))
|
||||
|
||||
// Drop the connection from 2 to 3
|
||||
r2.DeleteTestGossipConnection(r3)
|
||||
flushAndCheckTopology(t, routers, r1.tp(r2, r3), r2.tp(r1), r3.tp(r1, r2))
|
||||
|
||||
// Drop the connection from 1 to 3
|
||||
r1.DeleteTestGossipConnection(r3)
|
||||
sendPendingGossip(r1, r2, r3)
|
||||
checkTopology(t, r1, r1.tp(r2), r2.tp(r1))
|
||||
checkTopology(t, r2, r1.tp(r2), r2.tp(r1))
|
||||
// r3 still thinks r1 has a connection to it
|
||||
checkTopology(t, r3, r1.tp(r2, r3), r2.tp(r1), r3.tp(r1, r2))
|
||||
}
|
||||
|
||||
func TestGossipSurrogate(t *testing.T) {
|
||||
// create the topology r1 <-> r2 <-> r3
|
||||
r1 := NewTestRouter("01:00:00:01:00:00")
|
||||
r2 := NewTestRouter("02:00:00:02:00:00")
|
||||
r3 := NewTestRouter("03:00:00:03:00:00")
|
||||
routers := []*Router{r1, r2, r3}
|
||||
AddTestGossipConnection(r1, r2)
|
||||
AddTestGossipConnection(r3, r2)
|
||||
flushAndCheckTopology(t, routers, r1.tp(r2), r2.tp(r1, r3), r3.tp(r2))
|
||||
|
||||
// create a gossiper at either end, but not the middle
|
||||
g1 := newTestGossiper()
|
||||
g3 := newTestGossiper()
|
||||
s1 := r1.NewGossip("Test", g1)
|
||||
s3 := r3.NewGossip("Test", g3)
|
||||
|
||||
// broadcast a message from each end, check it reaches the other
|
||||
broadcast(s1, 1)
|
||||
broadcast(s3, 2)
|
||||
sendPendingGossip(r1, r2, r3)
|
||||
g1.checkHas(t, 2)
|
||||
g3.checkHas(t, 1)
|
||||
|
||||
// check that each end gets their message back through periodic
|
||||
// gossip
|
||||
r1.SendAllGossip()
|
||||
r3.SendAllGossip()
|
||||
sendPendingGossip(r1, r2, r3)
|
||||
g1.checkHas(t, 1, 2)
|
||||
g3.checkHas(t, 1, 2)
|
||||
}
|
||||
|
||||
type testGossiper struct {
|
||||
sync.RWMutex
|
||||
state map[byte]struct{}
|
||||
}
|
||||
|
||||
func newTestGossiper() *testGossiper {
|
||||
return &testGossiper{state: make(map[byte]struct{})}
|
||||
}
|
||||
|
||||
func (g *testGossiper) OnGossipUnicast(sender PeerName, msg []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *testGossiper) OnGossipBroadcast(_ PeerName, update []byte) (GossipData, error) {
|
||||
g.Lock()
|
||||
defer g.Unlock()
|
||||
for _, v := range update {
|
||||
g.state[v] = void
|
||||
}
|
||||
return NewSurrogateGossipData(update), nil
|
||||
}
|
||||
|
||||
func (g *testGossiper) Gossip() GossipData {
|
||||
g.RLock()
|
||||
defer g.RUnlock()
|
||||
state := make([]byte, len(g.state))
|
||||
for v := range g.state {
|
||||
state = append(state, v)
|
||||
}
|
||||
return NewSurrogateGossipData(state)
|
||||
}
|
||||
|
||||
func (g *testGossiper) OnGossip(update []byte) (GossipData, error) {
|
||||
g.Lock()
|
||||
defer g.Unlock()
|
||||
var delta []byte
|
||||
for _, v := range update {
|
||||
if _, found := g.state[v]; !found {
|
||||
delta = append(delta, v)
|
||||
g.state[v] = void
|
||||
}
|
||||
}
|
||||
if len(delta) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return NewSurrogateGossipData(delta), nil
|
||||
}
|
||||
|
||||
func (g *testGossiper) checkHas(t *testing.T, vs ...byte) {
|
||||
g.RLock()
|
||||
defer g.RUnlock()
|
||||
for _, v := range vs {
|
||||
if _, found := g.state[v]; !found {
|
||||
require.FailNow(t, fmt.Sprintf("%d is missing", v))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func broadcast(s Gossip, v byte) {
|
||||
s.GossipBroadcast(NewSurrogateGossipData([]byte{v}))
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
package router
|
||||
package mesh
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type LocalPeer struct {
|
||||
sync.RWMutex
|
||||
*Peer
|
||||
router *Router
|
||||
actionChan chan<- LocalPeerAction
|
||||
@@ -16,63 +17,31 @@ type LocalPeer struct {
|
||||
type LocalPeerAction func()
|
||||
|
||||
func NewLocalPeer(name PeerName, nickName string, router *Router) *LocalPeer {
|
||||
return &LocalPeer{Peer: NewPeer(name, nickName, 0, 0), router: router}
|
||||
}
|
||||
|
||||
func (peer *LocalPeer) Start() {
|
||||
actionChan := make(chan LocalPeerAction, ChannelSize)
|
||||
peer.actionChan = actionChan
|
||||
peer := &LocalPeer{
|
||||
Peer: NewPeer(name, nickName, randomPeerUID(), 0, randomPeerShortID()),
|
||||
router: router,
|
||||
actionChan: actionChan,
|
||||
}
|
||||
go peer.actorLoop(actionChan)
|
||||
return peer
|
||||
}
|
||||
|
||||
func (peer *LocalPeer) Forward(dstPeer *Peer, df bool, frame []byte, dec *EthernetDecoder) error {
|
||||
return peer.Relay(peer.Peer, dstPeer, df, frame, dec)
|
||||
func (peer *LocalPeer) Connections() ConnectionSet {
|
||||
connections := make(ConnectionSet)
|
||||
peer.RLock()
|
||||
defer peer.RUnlock()
|
||||
for _, conn := range peer.connections {
|
||||
connections[conn] = void
|
||||
}
|
||||
return connections
|
||||
}
|
||||
|
||||
func (peer *LocalPeer) Broadcast(df bool, frame []byte, dec *EthernetDecoder) {
|
||||
peer.RelayBroadcast(peer.Peer, df, frame, dec)
|
||||
}
|
||||
|
||||
func (peer *LocalPeer) Relay(srcPeer, dstPeer *Peer, df bool, frame []byte, dec *EthernetDecoder) error {
|
||||
relayPeerName, found := peer.router.Routes.Unicast(dstPeer.Name)
|
||||
if !found {
|
||||
// Not necessarily an error as there could be a race with the
|
||||
// dst disappearing whilst the frame is in flight
|
||||
log.Println("Received packet for unknown destination:", dstPeer.FullName())
|
||||
return nil
|
||||
}
|
||||
conn, found := peer.ConnectionTo(relayPeerName)
|
||||
if !found {
|
||||
// Again, could just be a race, not necessarily an error
|
||||
log.Println("Unable to find connection to relay peer", relayPeerName)
|
||||
return nil
|
||||
}
|
||||
return conn.(*LocalConnection).Forward(df, &ForwardedFrame{
|
||||
srcPeer: srcPeer,
|
||||
dstPeer: dstPeer,
|
||||
frame: frame},
|
||||
dec)
|
||||
}
|
||||
|
||||
func (peer *LocalPeer) RelayBroadcast(srcPeer *Peer, df bool, frame []byte, dec *EthernetDecoder) {
|
||||
nextHops := peer.router.Routes.Broadcast(srcPeer.Name)
|
||||
if len(nextHops) == 0 {
|
||||
return
|
||||
}
|
||||
for _, conn := range peer.ConnectionsTo(nextHops) {
|
||||
err := conn.(*LocalConnection).Forward(df, &ForwardedFrame{
|
||||
srcPeer: srcPeer,
|
||||
dstPeer: conn.Remote(),
|
||||
frame: frame},
|
||||
dec)
|
||||
if err != nil {
|
||||
if ftbe, ok := err.(FrameTooBigError); ok {
|
||||
log.Printf("dropping too big DF broadcast frame (%v -> %v): PMTU= %v\n", dec.ip.DstIP, dec.ip.SrcIP, ftbe.EPMTU)
|
||||
} else {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
func (peer *LocalPeer) ConnectionTo(name PeerName) (Connection, bool) {
|
||||
peer.RLock()
|
||||
defer peer.RUnlock()
|
||||
conn, found := peer.connections[name]
|
||||
return conn, found // yes, you really can't inline that. FFS.
|
||||
}
|
||||
|
||||
func (peer *LocalPeer) ConnectionsTo(names []PeerName) []Connection {
|
||||
@@ -93,22 +62,16 @@ func (peer *LocalPeer) CreateConnection(peerAddr string, acceptNewPeer bool) err
|
||||
if err := peer.checkConnectionLimit(); err != nil {
|
||||
return err
|
||||
}
|
||||
tcpAddr, tcpErr := net.ResolveTCPAddr("tcp4", peerAddr)
|
||||
udpAddr, udpErr := net.ResolveUDPAddr("udp4", peerAddr)
|
||||
if tcpErr != nil || udpErr != nil {
|
||||
// they really should have the same value, but just in case...
|
||||
if tcpErr == nil {
|
||||
return udpErr
|
||||
}
|
||||
return tcpErr
|
||||
tcpAddr, err := net.ResolveTCPAddr("tcp4", peerAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tcpConn, err := net.DialTCP("tcp4", nil, tcpAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
connRemote := NewRemoteConnection(peer.Peer, nil, tcpConn.RemoteAddr().String(), true, false)
|
||||
connLocal := NewLocalConnection(connRemote, tcpConn, udpAddr, peer.router)
|
||||
connLocal.Start(acceptNewPeer)
|
||||
StartLocalConnection(connRemote, tcpConn, peer.router, acceptNewPeer)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -162,7 +125,7 @@ func (peer *LocalPeer) handleAddConnection(conn Connection) error {
|
||||
log.Fatal("Attempt made to add connection to peer with unknown remote peer")
|
||||
}
|
||||
toName := conn.Remote().Name
|
||||
dupErr := fmt.Errorf("Multiple connections to %s added to %s", conn.Remote().FullName(), peer.FullName())
|
||||
dupErr := fmt.Errorf("Multiple connections to %s added to %s", conn.Remote(), peer.String())
|
||||
// deliberately non symmetrical
|
||||
if dupConn, found := peer.connections[toName]; found {
|
||||
if dupConn == conn {
|
||||
@@ -192,7 +155,10 @@ func (peer *LocalPeer) handleAddConnection(conn Connection) error {
|
||||
conn.Log("connection added (new peer)")
|
||||
peer.router.SendAllGossipDown(conn)
|
||||
}
|
||||
|
||||
peer.router.Routes.Recalculate()
|
||||
peer.broadcastPeerUpdate(conn.Remote())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -206,6 +172,8 @@ func (peer *LocalPeer) handleConnectionEstablished(conn Connection) {
|
||||
}
|
||||
peer.connectionEstablished(conn)
|
||||
conn.Log("connection fully established")
|
||||
|
||||
peer.router.Routes.Recalculate()
|
||||
peer.broadcastPeerUpdate()
|
||||
}
|
||||
|
||||
@@ -225,18 +193,69 @@ func (peer *LocalPeer) handleDeleteConnection(conn Connection) {
|
||||
// Must do garbage collection first to ensure we don't send out an
|
||||
// update with unreachable peers (can cause looping)
|
||||
peer.router.Peers.GarbageCollect()
|
||||
peer.router.Routes.Recalculate()
|
||||
peer.broadcastPeerUpdate()
|
||||
}
|
||||
|
||||
// helpers
|
||||
|
||||
func (peer *LocalPeer) broadcastPeerUpdate(peers ...*Peer) {
|
||||
peer.router.Routes.Recalculate()
|
||||
peer.router.TopologyGossip.GossipBroadcast(NewTopologyGossipData(peer.router.Peers, append(peers, peer.Peer)...))
|
||||
// Some tests run without a router. This should be fixed so
|
||||
// that the relevant part of Router can be easily run in the
|
||||
// context of a test, but that will involve significant
|
||||
// reworking of tests.
|
||||
if peer.router != nil {
|
||||
peer.router.BroadcastTopologyUpdate(append(peers, peer.Peer))
|
||||
}
|
||||
}
|
||||
|
||||
func (peer *LocalPeer) checkConnectionLimit() error {
|
||||
limit := peer.router.ConnLimit
|
||||
if 0 != limit && peer.ConnectionCount() >= limit {
|
||||
if 0 != limit && peer.connectionCount() >= limit {
|
||||
return fmt.Errorf("Connection limit reached (%v)", limit)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (peer *LocalPeer) addConnection(conn Connection) {
|
||||
peer.Lock()
|
||||
defer peer.Unlock()
|
||||
peer.connections[conn.Remote().Name] = conn
|
||||
peer.Version++
|
||||
}
|
||||
|
||||
func (peer *LocalPeer) deleteConnection(conn Connection) {
|
||||
peer.Lock()
|
||||
defer peer.Unlock()
|
||||
delete(peer.connections, conn.Remote().Name)
|
||||
peer.Version++
|
||||
}
|
||||
|
||||
func (peer *LocalPeer) connectionEstablished(conn Connection) {
|
||||
peer.Lock()
|
||||
defer peer.Unlock()
|
||||
peer.Version++
|
||||
}
|
||||
|
||||
func (peer *LocalPeer) connectionCount() int {
|
||||
peer.RLock()
|
||||
defer peer.RUnlock()
|
||||
return len(peer.connections)
|
||||
}
|
||||
|
||||
func (peer *LocalPeer) setShortID(shortID PeerShortID) {
|
||||
peer.Lock()
|
||||
defer peer.Unlock()
|
||||
peer.ShortID = shortID
|
||||
peer.Version++
|
||||
}
|
||||
|
||||
func (peer *LocalPeer) setVersionBeyond(version uint64) bool {
|
||||
peer.Lock()
|
||||
defer peer.Unlock()
|
||||
if version >= peer.Version {
|
||||
peer.Version = version + 1
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -3,37 +3,39 @@
|
||||
// It supplies some mock implementations to other unit tests, and is
|
||||
// named "...test.go" so it is only compiled under `go test`.
|
||||
|
||||
package router
|
||||
package mesh
|
||||
|
||||
import (
|
||||
wt "github.com/weaveworks/weave/testing"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Add to peers a connection from peers.ourself to p
|
||||
func (peers *Peers) AddTestConnection(p *Peer) {
|
||||
toPeer := NewPeer(p.Name, "", p.UID, 0)
|
||||
peers.FetchWithDefault(toPeer) // Has side-effect of incrementing refcount
|
||||
conn := newMockConnection(peers.ourself, toPeer)
|
||||
summary := p.PeerSummary
|
||||
summary.Version = 0
|
||||
toPeer := NewPeerFromSummary(summary)
|
||||
toPeer = peers.FetchWithDefault(toPeer) // Has side-effect of incrementing refcount
|
||||
conn := newMockConnection(peers.ourself.Peer, toPeer)
|
||||
peers.ourself.addConnection(conn)
|
||||
peers.ourself.connectionEstablished(conn)
|
||||
}
|
||||
|
||||
// Add to peers a connection from p1 to p2
|
||||
func (peers *Peers) AddTestRemoteConnection(p1, p2 *Peer) {
|
||||
fromName := p1.Name
|
||||
fromPeer := NewPeer(fromName, "", p1.UID, 0)
|
||||
fromPeer := NewPeerFrom(p1)
|
||||
fromPeer = peers.FetchWithDefault(fromPeer)
|
||||
toName := p2.Name
|
||||
toPeer := NewPeer(toName, "", p2.UID, 0)
|
||||
toPeer := NewPeerFrom(p2)
|
||||
toPeer = peers.FetchWithDefault(toPeer)
|
||||
peers.ourself.addConnection(&RemoteConnection{fromPeer, toPeer, "", false, false})
|
||||
}
|
||||
|
||||
func (peers *Peers) DeleteTestConnection(p *Peer) {
|
||||
toName := p.Name
|
||||
toPeer, _ := peers.Fetch(toName)
|
||||
toPeer.DecrementLocalRefCount()
|
||||
toPeer := peers.Fetch(toName)
|
||||
peers.Dereference(toPeer)
|
||||
conn, _ := peers.ourself.ConnectionTo(toName)
|
||||
peers.ourself.deleteConnection(conn)
|
||||
}
|
||||
@@ -57,18 +59,18 @@ func checkEqualConns(t *testing.T, ourName PeerName, got, wanted map[PeerName]Co
|
||||
if _, found := checkConns[remoteName]; found {
|
||||
delete(checkConns, remoteName)
|
||||
} else {
|
||||
wt.Fatalf(t, "Unexpected connection from %s to %s", ourName, remoteName)
|
||||
require.FailNow(t, fmt.Sprintf("Unexpected connection from %s to %s", ourName, remoteName))
|
||||
}
|
||||
}
|
||||
if len(checkConns) > 0 {
|
||||
wt.Fatalf(t, "Expected connections not found: from %s to %v", ourName, checkConns)
|
||||
require.FailNow(t, fmt.Sprintf("Expected connections not found: from %s to %v", ourName, checkConns))
|
||||
}
|
||||
}
|
||||
|
||||
// Get all the peers from a Peers in a slice
|
||||
func (peers *Peers) allPeers() []*Peer {
|
||||
var res []*Peer
|
||||
for _, peer := range peers.table {
|
||||
for _, peer := range peers.byName {
|
||||
res = append(res, peer)
|
||||
}
|
||||
return res
|
||||
@@ -104,10 +106,10 @@ func checkTopologyPeers(t *testing.T, checkConns bool, peers []*Peer, wantedPeer
|
||||
}
|
||||
delete(check, name)
|
||||
} else {
|
||||
wt.Fatalf(t, "Unexpected peer: %s", name)
|
||||
require.FailNow(t, fmt.Sprintf("Unexpected peer: %s", name))
|
||||
}
|
||||
}
|
||||
if len(check) > 0 {
|
||||
wt.Fatalf(t, "Expected peers not found: %v", check)
|
||||
require.FailNow(t, fmt.Sprintf("Expected peers not found: %v", check))
|
||||
}
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
package mesh
|
||||
|
||||
import (
|
||||
"net"
|
||||
)
|
||||
|
||||
type Overlay interface {
|
||||
// Enhance a features map with overlay-related features
|
||||
AddFeaturesTo(map[string]string)
|
||||
|
||||
// Prepare on overlay connection. The connection should remain
|
||||
// passive until it has been Confirm()ed.
|
||||
PrepareConnection(OverlayConnectionParams) (OverlayConnection, error)
|
||||
|
||||
// Obtain diagnostic information specific to the overlay
|
||||
Diagnostics() interface{}
|
||||
}
|
||||
|
||||
type OverlayConnectionParams struct {
|
||||
RemotePeer *Peer
|
||||
|
||||
// The local address of the corresponding TCP connection. Used to
|
||||
// derive the local IP address for sending. May differ for
|
||||
// different overlay connections.
|
||||
LocalAddr *net.TCPAddr
|
||||
|
||||
// The remote address of the corresponding TCP connection. Used to
|
||||
// determine the address to send to, but only if the TCP
|
||||
// connection is outbound. Otherwise the Overlay needs to discover
|
||||
// it (e.g. from incoming datagrams).
|
||||
RemoteAddr *net.TCPAddr
|
||||
|
||||
// Is the corresponding TCP connection outbound?
|
||||
Outbound bool
|
||||
|
||||
// Unique identifier for this connection
|
||||
ConnUID uint64
|
||||
|
||||
// Session key, if connection is encrypted; nil otherwise.
|
||||
//
|
||||
// NB: overlay connections must take care not to use nonces which
|
||||
// may collide with those of the main connection. These nonces are
|
||||
// 192 bits, with the top most bit unspecified, the next bit set
|
||||
// to 1, followed by 126 zero bits, and a message sequence number
|
||||
// in the lowest 64 bits.
|
||||
SessionKey *[32]byte
|
||||
|
||||
// Function to send a control message to the counterpart
|
||||
// overlay connection.
|
||||
SendControlMessage func(tag byte, msg []byte) error
|
||||
|
||||
// Features passed at connection initiation
|
||||
Features map[string]string
|
||||
}
|
||||
|
||||
// All of the machinery to manage overlay connectivity to a particular
|
||||
// peer
|
||||
type OverlayConnection interface {
|
||||
// Confirm that the connection is really wanted, and so the
|
||||
// Overlay should begin heartbeats etc. to verify the operation of
|
||||
// the overlay connection.
|
||||
Confirm()
|
||||
|
||||
// A channel indicating that the overlay connection is
|
||||
// established, i.e. its operation has been confirmed.
|
||||
EstablishedChannel() <-chan struct{}
|
||||
|
||||
// A channel indicating an error from the overlay connection. The
|
||||
// overlay connection is not expected to be operational after the
|
||||
// first error, so the channel only needs to buffer a single
|
||||
// error.
|
||||
ErrorChannel() <-chan error
|
||||
|
||||
Stop()
|
||||
|
||||
// Handle a message from the peer. 'tag' exists for
|
||||
// compatibility, and should always be
|
||||
// ProtocolOverlayControlMessage for non-sleeve overlays.
|
||||
ControlMessage(tag byte, msg []byte)
|
||||
|
||||
// User facing overlay name
|
||||
DisplayName() string
|
||||
}
|
||||
|
||||
type NullOverlay struct{}
|
||||
|
||||
func (NullOverlay) AddFeaturesTo(map[string]string) {
|
||||
}
|
||||
|
||||
func (NullOverlay) PrepareConnection(OverlayConnectionParams) (OverlayConnection, error) {
|
||||
return NullOverlay{}, nil
|
||||
}
|
||||
|
||||
func (NullOverlay) Diagnostics() interface{} {
|
||||
return nil
|
||||
}
|
||||
func (NullOverlay) Confirm() {
|
||||
}
|
||||
|
||||
func (NullOverlay) EstablishedChannel() <-chan struct{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (NullOverlay) ErrorChannel() <-chan error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (NullOverlay) Stop() {
|
||||
}
|
||||
|
||||
func (NullOverlay) ControlMessage(byte, []byte) {
|
||||
}
|
||||
|
||||
func (NullOverlay) DisplayName() string {
|
||||
return "null"
|
||||
}
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
package mesh
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func randBytes(n int) []byte {
|
||||
buf := make([]byte, n)
|
||||
_, err := rand.Read(buf)
|
||||
checkFatal(err)
|
||||
return buf
|
||||
}
|
||||
|
||||
func randUint64() (r uint64) {
|
||||
return binary.LittleEndian.Uint64(randBytes(8))
|
||||
}
|
||||
|
||||
func randUint16() (r uint16) {
|
||||
return binary.LittleEndian.Uint16(randBytes(2))
|
||||
}
|
||||
|
||||
type PeerUID uint64
|
||||
|
||||
func randomPeerUID() PeerUID {
|
||||
for {
|
||||
uid := randUint64()
|
||||
if uid != 0 { // uid 0 is reserved for peer placeholder
|
||||
return PeerUID(uid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ParsePeerUID(s string) (PeerUID, error) {
|
||||
uid, err := strconv.ParseUint(s, 10, 64)
|
||||
return PeerUID(uid), err
|
||||
}
|
||||
|
||||
// Short IDs exist for the sake of fast datapath. They are 12 bits,
|
||||
// randomly assigned, but we detect and recover from collisions. This
|
||||
// does limit us to 4096 peers, but that should be sufficient for a
|
||||
// while.
|
||||
type PeerShortID uint16
|
||||
|
||||
const PeerShortIDBits = 12
|
||||
|
||||
func randomPeerShortID() PeerShortID {
|
||||
return PeerShortID(randUint16() & (1<<PeerShortIDBits - 1))
|
||||
}
|
||||
|
||||
type PeerSummary struct {
|
||||
NameByte []byte
|
||||
NickName string
|
||||
UID PeerUID
|
||||
Version uint64
|
||||
ShortID PeerShortID
|
||||
HasShortID bool
|
||||
}
|
||||
|
||||
type Peer struct {
|
||||
Name PeerName
|
||||
PeerSummary
|
||||
localRefCount uint64 // maintained by Peers
|
||||
connections map[PeerName]Connection
|
||||
}
|
||||
|
||||
type ListOfPeers []*Peer
|
||||
|
||||
func (lop ListOfPeers) Len() int {
|
||||
return len(lop)
|
||||
}
|
||||
func (lop ListOfPeers) Swap(i, j int) {
|
||||
lop[i], lop[j] = lop[j], lop[i]
|
||||
}
|
||||
func (lop ListOfPeers) Less(i, j int) bool {
|
||||
return lop[i].Name < lop[j].Name
|
||||
}
|
||||
|
||||
type ConnectionSet map[Connection]struct{}
|
||||
|
||||
func NewPeerFromSummary(summary PeerSummary) *Peer {
|
||||
return &Peer{
|
||||
Name: PeerNameFromBin(summary.NameByte),
|
||||
PeerSummary: summary,
|
||||
connections: make(map[PeerName]Connection),
|
||||
}
|
||||
}
|
||||
|
||||
func NewPeer(name PeerName, nickName string, uid PeerUID, version uint64, shortID PeerShortID) *Peer {
|
||||
return NewPeerFromSummary(PeerSummary{
|
||||
NameByte: name.Bin(),
|
||||
NickName: nickName,
|
||||
UID: uid,
|
||||
Version: version,
|
||||
ShortID: shortID,
|
||||
HasShortID: true,
|
||||
})
|
||||
}
|
||||
|
||||
func NewPeerPlaceholder(name PeerName) *Peer {
|
||||
return NewPeerFromSummary(PeerSummary{NameByte: name.Bin()})
|
||||
}
|
||||
|
||||
func NewPeerFrom(peer *Peer) *Peer {
|
||||
return NewPeerFromSummary(peer.PeerSummary)
|
||||
}
|
||||
|
||||
func (peer *Peer) String() string {
|
||||
return fmt.Sprint(peer.Name, "(", peer.NickName, ")")
|
||||
}
|
||||
|
||||
// Calculate the routing table from this peer to all peers reachable
|
||||
// from it, returning a "next hop" map of PeerNameX -> PeerNameY,
|
||||
// which says "in order to send a message to X, the peer should send
|
||||
// the message to its neighbour Y".
|
||||
//
|
||||
// Because currently we do not have weightings on the connections
|
||||
// between peers, there is no need to use a minimum spanning tree
|
||||
// algorithm. Instead we employ the simpler and cheaper breadth-first
|
||||
// widening. The computation is deterministic, which ensures that when
|
||||
// it is performed on the same data by different peers, they get the
|
||||
// same result. This is important since otherwise we risk message loss
|
||||
// or routing cycles.
|
||||
//
|
||||
// When the 'establishedAndSymmetric' flag is set, only connections
|
||||
// that are marked as 'established' and are symmetric (i.e. where both
|
||||
// sides indicate they have a connection to the other) are considered.
|
||||
//
|
||||
// When a non-nil stopAt peer is supplied, the widening stops when it
|
||||
// reaches that peer. The boolean return indicates whether that has
|
||||
// happened.
|
||||
//
|
||||
// NB: This function should generally be invoked while holding a read
|
||||
// lock on Peers and LocalPeer.
|
||||
func (peer *Peer) Routes(stopAt *Peer, establishedAndSymmetric bool) (bool, map[PeerName]PeerName) {
|
||||
routes := make(unicastRoutes)
|
||||
routes[peer.Name] = UnknownPeerName
|
||||
nextWorklist := []*Peer{peer}
|
||||
for len(nextWorklist) > 0 {
|
||||
worklist := nextWorklist
|
||||
sort.Sort(ListOfPeers(worklist))
|
||||
nextWorklist = []*Peer{}
|
||||
for _, curPeer := range worklist {
|
||||
if curPeer == stopAt {
|
||||
return true, routes
|
||||
}
|
||||
curPeer.ForEachConnectedPeer(establishedAndSymmetric, routes,
|
||||
func(remotePeer *Peer) {
|
||||
nextWorklist = append(nextWorklist, remotePeer)
|
||||
remoteName := remotePeer.Name
|
||||
// We now know how to get to remoteName: the same
|
||||
// way we get to curPeer. Except, if curPeer is
|
||||
// the starting peer in which case we know we can
|
||||
// reach remoteName directly.
|
||||
if curPeer == peer {
|
||||
routes[remoteName] = remoteName
|
||||
} else {
|
||||
routes[remoteName] = routes[curPeer.Name]
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
return false, routes
|
||||
}
|
||||
|
||||
func (peer *Peer) ForEachConnectedPeer(establishedAndSymmetric bool, exclude map[PeerName]PeerName, f func(*Peer)) {
|
||||
for remoteName, conn := range peer.connections {
|
||||
if establishedAndSymmetric && !conn.Established() {
|
||||
continue
|
||||
}
|
||||
if _, found := exclude[remoteName]; found {
|
||||
continue
|
||||
}
|
||||
remotePeer := conn.Remote()
|
||||
if remoteConn, found := remotePeer.connections[peer.Name]; !establishedAndSymmetric || (found && remoteConn.Established()) {
|
||||
f(remotePeer)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
// Let peer names be sha256 hashes...of anything (as long as it's
|
||||
// unique).
|
||||
package router
|
||||
|
||||
package mesh
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
@@ -14,7 +14,8 @@
|
||||
// Note that we do not mandate *what* MAC should be used as the peer
|
||||
// name. In particular it doesn't actually have to be the MAC of, say,
|
||||
// the network interface the peer is sniffing on.
|
||||
package router
|
||||
|
||||
package mesh
|
||||
|
||||
import (
|
||||
"net"
|
||||
@@ -55,3 +56,20 @@ func (name PeerName) Bin() []byte {
|
||||
func (name PeerName) String() string {
|
||||
return intmac(uint64(name)).String()
|
||||
}
|
||||
|
||||
func macint(mac net.HardwareAddr) (r uint64) {
|
||||
for _, b := range mac {
|
||||
r <<= 8
|
||||
r |= uint64(b)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func intmac(key uint64) (r net.HardwareAddr) {
|
||||
r = make([]byte, 6)
|
||||
for i := 5; i >= 0; i-- {
|
||||
r[i] = byte(key)
|
||||
key >>= 8
|
||||
}
|
||||
return
|
||||
}
|
||||
+530
@@ -0,0 +1,530 @@
|
||||
package mesh
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"io"
|
||||
"math/rand"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var void = struct{}{}
|
||||
|
||||
type Peers struct {
|
||||
sync.RWMutex
|
||||
ourself *LocalPeer
|
||||
byName map[PeerName]*Peer
|
||||
byShortID map[PeerShortID]ShortIDPeers
|
||||
onGC []func(*Peer)
|
||||
|
||||
// Called when the mapping from short ids to peers changes
|
||||
onInvalidateShortIDs []func()
|
||||
}
|
||||
|
||||
type ShortIDPeers struct {
|
||||
// If we know about a single peer with the short id, this is
|
||||
// that peer. If there is a collision, this is the peer with
|
||||
// the lowest Name.
|
||||
peer *Peer
|
||||
|
||||
// In case of a collision, this holds the other peers.
|
||||
others []*Peer
|
||||
}
|
||||
|
||||
type PeerNameSet map[PeerName]struct{}
|
||||
|
||||
type ConnectionSummary struct {
|
||||
NameByte []byte
|
||||
RemoteTCPAddr string
|
||||
Outbound bool
|
||||
Established bool
|
||||
}
|
||||
|
||||
// Pending notifications due to changes to Peers that need to be sent
|
||||
// out once the Peers is unlocked.
|
||||
type PeersPendingNotifications struct {
|
||||
// Peers that have been GCed
|
||||
removed []*Peer
|
||||
|
||||
// The mapping from shorts ids to peers changed
|
||||
invalidateShortIDs bool
|
||||
|
||||
// The local short ID needs reassigning due to a collision
|
||||
reassignLocalShortID bool
|
||||
|
||||
// The local peer was modified
|
||||
localPeerModified bool
|
||||
}
|
||||
|
||||
func NewPeers(ourself *LocalPeer) *Peers {
|
||||
peers := &Peers{
|
||||
ourself: ourself,
|
||||
byName: make(map[PeerName]*Peer),
|
||||
byShortID: make(map[PeerShortID]ShortIDPeers),
|
||||
}
|
||||
peers.FetchWithDefault(ourself.Peer)
|
||||
return peers
|
||||
}
|
||||
|
||||
func (peers *Peers) OnGC(callback func(*Peer)) {
|
||||
peers.Lock()
|
||||
defer peers.Unlock()
|
||||
|
||||
// Although the array underlying peers.onGC might be accessed
|
||||
// without holding the lock in unlockAndNotify, we don't
|
||||
// support removing callbacks, so a simple append here is
|
||||
// safe.
|
||||
peers.onGC = append(peers.onGC, callback)
|
||||
}
|
||||
|
||||
func (peers *Peers) OnInvalidateShortIDs(callback func()) {
|
||||
peers.Lock()
|
||||
defer peers.Unlock()
|
||||
|
||||
// Safe, as in OnGC
|
||||
peers.onInvalidateShortIDs = append(peers.onInvalidateShortIDs, callback)
|
||||
}
|
||||
|
||||
func (peers *Peers) unlockAndNotify(pending *PeersPendingNotifications) {
|
||||
broadcastLocalPeer := (pending.reassignLocalShortID && peers.reassignLocalShortID(pending)) ||
|
||||
pending.localPeerModified
|
||||
onGC := peers.onGC
|
||||
onInvalidateShortIDs := peers.onInvalidateShortIDs
|
||||
peers.Unlock()
|
||||
|
||||
if pending.removed != nil {
|
||||
for _, callback := range onGC {
|
||||
for _, peer := range pending.removed {
|
||||
callback(peer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if pending.invalidateShortIDs {
|
||||
for _, callback := range onInvalidateShortIDs {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
if broadcastLocalPeer {
|
||||
peers.ourself.broadcastPeerUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
func (peers *Peers) addByShortID(peer *Peer, pending *PeersPendingNotifications) {
|
||||
if !peer.HasShortID {
|
||||
return
|
||||
}
|
||||
|
||||
entry, ok := peers.byShortID[peer.ShortID]
|
||||
if !ok {
|
||||
entry = ShortIDPeers{peer: peer}
|
||||
} else if entry.peer == nil {
|
||||
// This short ID is free, but was used in the past.
|
||||
// Because we are reusing it, it's an invalidation
|
||||
// event.
|
||||
entry.peer = peer
|
||||
pending.invalidateShortIDs = true
|
||||
} else if peer.Name < entry.peer.Name {
|
||||
// Short ID collision, this peer becomes the principal
|
||||
// peer for the short ID, bumping the previous one
|
||||
// into others.
|
||||
|
||||
if entry.peer == peers.ourself.Peer {
|
||||
// The bumped peer is peers.ourself, so we
|
||||
// need to look for a new short id
|
||||
pending.reassignLocalShortID = true
|
||||
}
|
||||
|
||||
entry.others = append(entry.others, entry.peer)
|
||||
entry.peer = peer
|
||||
pending.invalidateShortIDs = true
|
||||
} else {
|
||||
// Short ID collision, this peer is secondary
|
||||
entry.others = append(entry.others, peer)
|
||||
}
|
||||
|
||||
peers.byShortID[peer.ShortID] = entry
|
||||
}
|
||||
|
||||
func (peers *Peers) deleteByShortID(peer *Peer, pending *PeersPendingNotifications) {
|
||||
if !peer.HasShortID {
|
||||
return
|
||||
}
|
||||
|
||||
entry := peers.byShortID[peer.ShortID]
|
||||
var otherIndex int
|
||||
|
||||
if peer != entry.peer {
|
||||
// peer is secondary, find its index in others
|
||||
otherIndex = -1
|
||||
|
||||
for i, other := range entry.others {
|
||||
if peer == other {
|
||||
otherIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if otherIndex < 0 {
|
||||
return
|
||||
}
|
||||
} else if len(entry.others) != 0 {
|
||||
// need to find the peer with the lowest name to
|
||||
// become the new principal one
|
||||
otherIndex = 0
|
||||
minName := entry.others[0].Name
|
||||
|
||||
for i := 1; i < len(entry.others); i++ {
|
||||
otherName := entry.others[i].Name
|
||||
if otherName < minName {
|
||||
minName = otherName
|
||||
otherIndex = i
|
||||
}
|
||||
}
|
||||
|
||||
entry.peer = entry.others[otherIndex]
|
||||
pending.invalidateShortIDs = true
|
||||
} else {
|
||||
// This is the last peer with the short id. We clear
|
||||
// the entry, don't delete it, in order to detect when
|
||||
// it gets re-used.
|
||||
peers.byShortID[peer.ShortID] = ShortIDPeers{}
|
||||
return
|
||||
}
|
||||
|
||||
entry.others[otherIndex] = entry.others[len(entry.others)-1]
|
||||
entry.others = entry.others[:len(entry.others)-1]
|
||||
peers.byShortID[peer.ShortID] = entry
|
||||
}
|
||||
|
||||
func (peers *Peers) reassignLocalShortID(pending *PeersPendingNotifications) bool {
|
||||
newShortID, ok := peers.chooseShortID()
|
||||
if ok {
|
||||
peers.setLocalShortID(newShortID, pending)
|
||||
return true
|
||||
}
|
||||
|
||||
// Otherwise we'll try again later on in garbageColleect
|
||||
return false
|
||||
}
|
||||
|
||||
func (peers *Peers) setLocalShortID(newShortID PeerShortID, pending *PeersPendingNotifications) {
|
||||
peers.deleteByShortID(peers.ourself.Peer, pending)
|
||||
peers.ourself.setShortID(newShortID)
|
||||
peers.addByShortID(peers.ourself.Peer, pending)
|
||||
}
|
||||
|
||||
// Choose an available short id at random
|
||||
func (peers *Peers) chooseShortID() (PeerShortID, bool) {
|
||||
rng := rand.New(rand.NewSource(int64(randUint64())))
|
||||
|
||||
// First, just try picking some short ids at random, and
|
||||
// seeing if they are available:
|
||||
for i := 0; i < 10; i++ {
|
||||
shortID := PeerShortID(rng.Intn(1 << PeerShortIDBits))
|
||||
if peers.byShortID[shortID].peer == nil {
|
||||
return shortID, true
|
||||
}
|
||||
}
|
||||
|
||||
// Looks like most short ids are used. So count the number of
|
||||
// unused ones, and pick one at random.
|
||||
available := int(1 << PeerShortIDBits)
|
||||
for _, entry := range peers.byShortID {
|
||||
if entry.peer != nil {
|
||||
available--
|
||||
}
|
||||
}
|
||||
|
||||
if available == 0 {
|
||||
// All short ids are used.
|
||||
return 0, false
|
||||
}
|
||||
|
||||
n := rng.Intn(available)
|
||||
var i PeerShortID
|
||||
for {
|
||||
if peers.byShortID[i].peer == nil {
|
||||
if n == 0 {
|
||||
return PeerShortID(i), true
|
||||
}
|
||||
|
||||
n--
|
||||
}
|
||||
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
func (peers *Peers) FetchWithDefault(peer *Peer) *Peer {
|
||||
peers.Lock()
|
||||
var pending PeersPendingNotifications
|
||||
defer peers.unlockAndNotify(&pending)
|
||||
|
||||
if existingPeer, found := peers.byName[peer.Name]; found {
|
||||
existingPeer.localRefCount++
|
||||
return existingPeer
|
||||
}
|
||||
|
||||
peers.byName[peer.Name] = peer
|
||||
peers.addByShortID(peer, &pending)
|
||||
peer.localRefCount++
|
||||
return peer
|
||||
}
|
||||
|
||||
func (peers *Peers) Fetch(name PeerName) *Peer {
|
||||
peers.RLock()
|
||||
defer peers.RUnlock()
|
||||
return peers.byName[name]
|
||||
}
|
||||
|
||||
func (peers *Peers) FetchAndAddRef(name PeerName) *Peer {
|
||||
peers.Lock()
|
||||
defer peers.Unlock()
|
||||
peer := peers.byName[name]
|
||||
if peer != nil {
|
||||
peer.localRefCount++
|
||||
}
|
||||
return peer
|
||||
}
|
||||
|
||||
func (peers *Peers) FetchByShortID(shortID PeerShortID) *Peer {
|
||||
peers.RLock()
|
||||
defer peers.RUnlock()
|
||||
return peers.byShortID[shortID].peer
|
||||
}
|
||||
|
||||
func (peers *Peers) Dereference(peer *Peer) {
|
||||
peers.Lock()
|
||||
defer peers.Unlock()
|
||||
peer.localRefCount--
|
||||
}
|
||||
|
||||
func (peers *Peers) ForEach(fun func(*Peer)) {
|
||||
peers.RLock()
|
||||
defer peers.RUnlock()
|
||||
for _, peer := range peers.byName {
|
||||
fun(peer)
|
||||
}
|
||||
}
|
||||
|
||||
// Merge an incoming update with our own topology.
|
||||
//
|
||||
// We add peers hitherto unknown to us, and update peers for which the
|
||||
// update contains a more recent version than known to us. The return
|
||||
// value is a) a representation of the received update, and b) an
|
||||
// "improved" update containing just these new/updated elements.
|
||||
func (peers *Peers) ApplyUpdate(update []byte) (PeerNameSet, PeerNameSet, error) {
|
||||
peers.Lock()
|
||||
var pending PeersPendingNotifications
|
||||
defer peers.unlockAndNotify(&pending)
|
||||
|
||||
newPeers, decodedUpdate, decodedConns, err := peers.decodeUpdate(update)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Add new peers
|
||||
for name, newPeer := range newPeers {
|
||||
peers.byName[name] = newPeer
|
||||
peers.addByShortID(newPeer, &pending)
|
||||
}
|
||||
|
||||
// Now apply the updates
|
||||
newUpdate := peers.applyUpdate(decodedUpdate, decodedConns, &pending)
|
||||
peers.garbageCollect(&pending)
|
||||
for _, peerRemoved := range pending.removed {
|
||||
delete(newUpdate, peerRemoved.Name)
|
||||
}
|
||||
|
||||
updateNames := make(PeerNameSet)
|
||||
for _, peer := range decodedUpdate {
|
||||
updateNames[peer.Name] = void
|
||||
}
|
||||
|
||||
return updateNames, newUpdate, nil
|
||||
}
|
||||
|
||||
func (peers *Peers) Names() PeerNameSet {
|
||||
peers.RLock()
|
||||
defer peers.RUnlock()
|
||||
|
||||
names := make(PeerNameSet)
|
||||
for name := range peers.byName {
|
||||
names[name] = void
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
func (peers *Peers) EncodePeers(names PeerNameSet) []byte {
|
||||
buf := new(bytes.Buffer)
|
||||
enc := gob.NewEncoder(buf)
|
||||
peers.RLock()
|
||||
defer peers.RUnlock()
|
||||
for name := range names {
|
||||
if peer, found := peers.byName[name]; found {
|
||||
if peer == peers.ourself.Peer {
|
||||
peers.ourself.Encode(enc)
|
||||
} else {
|
||||
peer.Encode(enc)
|
||||
}
|
||||
}
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
func (peers *Peers) GarbageCollect() {
|
||||
peers.Lock()
|
||||
var pending PeersPendingNotifications
|
||||
defer peers.unlockAndNotify(&pending)
|
||||
|
||||
peers.garbageCollect(&pending)
|
||||
}
|
||||
|
||||
func (peers *Peers) garbageCollect(pending *PeersPendingNotifications) {
|
||||
peers.ourself.RLock()
|
||||
_, reached := peers.ourself.Routes(nil, false)
|
||||
peers.ourself.RUnlock()
|
||||
|
||||
for name, peer := range peers.byName {
|
||||
if _, found := reached[peer.Name]; !found && peer.localRefCount == 0 {
|
||||
delete(peers.byName, name)
|
||||
peers.deleteByShortID(peer, pending)
|
||||
pending.removed = append(pending.removed, peer)
|
||||
}
|
||||
}
|
||||
|
||||
if peers.byShortID[peers.ourself.ShortID].peer != peers.ourself.Peer {
|
||||
// The local peer doesn't own its short id. Garbage
|
||||
// collection might have freed some up, so try to
|
||||
// reassign.
|
||||
pending.reassignLocalShortID = true
|
||||
}
|
||||
}
|
||||
|
||||
func (peers *Peers) decodeUpdate(update []byte) (newPeers map[PeerName]*Peer, decodedUpdate []*Peer, decodedConns [][]ConnectionSummary, err error) {
|
||||
newPeers = make(map[PeerName]*Peer)
|
||||
decodedUpdate = []*Peer{}
|
||||
decodedConns = [][]ConnectionSummary{}
|
||||
|
||||
decoder := gob.NewDecoder(bytes.NewReader(update))
|
||||
|
||||
for {
|
||||
peerSummary, connSummaries, decErr := decodePeer(decoder)
|
||||
if decErr == io.EOF {
|
||||
break
|
||||
} else if decErr != nil {
|
||||
err = decErr
|
||||
return
|
||||
}
|
||||
newPeer := NewPeerFromSummary(peerSummary)
|
||||
decodedUpdate = append(decodedUpdate, newPeer)
|
||||
decodedConns = append(decodedConns, connSummaries)
|
||||
if _, found := peers.byName[newPeer.Name]; !found {
|
||||
newPeers[newPeer.Name] = newPeer
|
||||
}
|
||||
}
|
||||
|
||||
for _, connSummaries := range decodedConns {
|
||||
for _, connSummary := range connSummaries {
|
||||
remoteName := PeerNameFromBin(connSummary.NameByte)
|
||||
if _, found := newPeers[remoteName]; found {
|
||||
continue
|
||||
}
|
||||
if _, found := peers.byName[remoteName]; found {
|
||||
continue
|
||||
}
|
||||
// Update refers to a peer which we have no knowledge of.
|
||||
newPeers[remoteName] = NewPeerPlaceholder(remoteName)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (peers *Peers) applyUpdate(decodedUpdate []*Peer, decodedConns [][]ConnectionSummary, pending *PeersPendingNotifications) PeerNameSet {
|
||||
newUpdate := make(PeerNameSet)
|
||||
for idx, newPeer := range decodedUpdate {
|
||||
connSummaries := decodedConns[idx]
|
||||
name := newPeer.Name
|
||||
// guaranteed to find peer in the peers.byName
|
||||
switch peer := peers.byName[name]; peer {
|
||||
case peers.ourself.Peer:
|
||||
if newPeer.UID != peer.UID {
|
||||
// The update contains information about an old
|
||||
// incarnation of ourselves. We increase our version
|
||||
// number beyond that which we received, so our
|
||||
// information supersedes the old one when it is
|
||||
// received by other peers.
|
||||
pending.localPeerModified = peers.ourself.setVersionBeyond(newPeer.Version)
|
||||
}
|
||||
case newPeer:
|
||||
peer.connections = makeConnsMap(peer, connSummaries, peers.byName)
|
||||
newUpdate[name] = void
|
||||
default: // existing peer
|
||||
if newPeer.Version < peer.Version ||
|
||||
(newPeer.Version == peer.Version &&
|
||||
(newPeer.UID < peer.UID ||
|
||||
(newPeer.UID == peer.UID &&
|
||||
(!newPeer.HasShortID || peer.HasShortID)))) {
|
||||
continue
|
||||
}
|
||||
peer.Version = newPeer.Version
|
||||
peer.UID = newPeer.UID
|
||||
peer.NickName = newPeer.NickName
|
||||
peer.connections = makeConnsMap(peer, connSummaries, peers.byName)
|
||||
|
||||
if newPeer.ShortID != peer.ShortID || newPeer.HasShortID != peer.HasShortID {
|
||||
peers.deleteByShortID(peer, pending)
|
||||
peer.ShortID = newPeer.ShortID
|
||||
peer.HasShortID = newPeer.HasShortID
|
||||
peers.addByShortID(peer, pending)
|
||||
}
|
||||
newUpdate[name] = void
|
||||
}
|
||||
}
|
||||
return newUpdate
|
||||
}
|
||||
|
||||
func (peer *Peer) Encode(enc *gob.Encoder) {
|
||||
checkFatal(enc.Encode(peer.PeerSummary))
|
||||
|
||||
connSummaries := []ConnectionSummary{}
|
||||
for _, conn := range peer.connections {
|
||||
connSummaries = append(connSummaries, ConnectionSummary{
|
||||
conn.Remote().NameByte,
|
||||
conn.RemoteTCPAddr(),
|
||||
conn.Outbound(),
|
||||
conn.Established(),
|
||||
})
|
||||
}
|
||||
|
||||
checkFatal(enc.Encode(connSummaries))
|
||||
}
|
||||
|
||||
func (peer *LocalPeer) Encode(enc *gob.Encoder) {
|
||||
peer.RLock()
|
||||
defer peer.RUnlock()
|
||||
peer.Peer.Encode(enc)
|
||||
}
|
||||
|
||||
func decodePeer(dec *gob.Decoder) (peerSummary PeerSummary, connSummaries []ConnectionSummary, err error) {
|
||||
if err = dec.Decode(&peerSummary); err != nil {
|
||||
return
|
||||
}
|
||||
if err = dec.Decode(&connSummaries); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func makeConnsMap(peer *Peer, connSummaries []ConnectionSummary, byName map[PeerName]*Peer) map[PeerName]Connection {
|
||||
conns := make(map[PeerName]Connection)
|
||||
for _, connSummary := range connSummaries {
|
||||
name := PeerNameFromBin(connSummary.NameByte)
|
||||
remotePeer := byName[name]
|
||||
conn := NewRemoteConnection(peer, remotePeer, connSummary.RemoteTCPAddr, connSummary.Outbound, connSummary.Established)
|
||||
conns[name] = conn
|
||||
}
|
||||
return conns
|
||||
}
|
||||
@@ -0,0 +1,376 @@
|
||||
package mesh
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TODO we should also test:
|
||||
//
|
||||
// - applying an incremental update, including the case where that
|
||||
// leads to an UnknownPeerError
|
||||
//
|
||||
// - the "improved update" calculation
|
||||
//
|
||||
// - non-gc of peers that are only referenced locally
|
||||
|
||||
func newNode(name PeerName) (*Peer, *Peers) {
|
||||
peer := NewLocalPeer(name, "", nil)
|
||||
peers := NewPeers(peer)
|
||||
return peer.Peer, peers
|
||||
}
|
||||
|
||||
// Check that ApplyUpdate copies the whole topology from peers
|
||||
func checkApplyUpdate(t *testing.T, peers *Peers) {
|
||||
dummyName, _ := PeerNameFromString("99:00:00:01:00:00")
|
||||
// We need a new node outside of the network, with a connection
|
||||
// into it.
|
||||
_, testBedPeers := newNode(dummyName)
|
||||
testBedPeers.AddTestConnection(peers.ourself.Peer)
|
||||
testBedPeers.ApplyUpdate(peers.EncodePeers(peers.Names()))
|
||||
|
||||
checkTopologyPeers(t, true, testBedPeers.allPeersExcept(dummyName), peers.allPeers()...)
|
||||
}
|
||||
|
||||
func TestPeersEncoding(t *testing.T) {
|
||||
const numNodes = 20
|
||||
const numIters = 1000
|
||||
var peer [numNodes]*Peer
|
||||
var ps [numNodes]*Peers
|
||||
for i := 0; i < numNodes; i++ {
|
||||
name, _ := PeerNameFromString(fmt.Sprintf("%02d:00:00:01:00:00", i))
|
||||
peer[i], ps[i] = newNode(name)
|
||||
}
|
||||
|
||||
var conns []struct{ from, to int }
|
||||
for i := 0; i < numIters; i++ {
|
||||
oper := rand.Intn(2)
|
||||
switch oper {
|
||||
case 0:
|
||||
from, to := rand.Intn(numNodes), rand.Intn(numNodes)
|
||||
if from != to {
|
||||
if _, found := peer[from].connections[peer[to].Name]; !found {
|
||||
ps[from].AddTestConnection(peer[to])
|
||||
conns = append(conns, struct{ from, to int }{from, to})
|
||||
checkApplyUpdate(t, ps[from])
|
||||
}
|
||||
}
|
||||
case 1:
|
||||
if len(conns) > 0 {
|
||||
n := rand.Intn(len(conns))
|
||||
c := conns[n]
|
||||
ps[c.from].DeleteTestConnection(peer[c.to])
|
||||
ps[c.from].GarbageCollect()
|
||||
checkApplyUpdate(t, ps[c.from])
|
||||
conns = append(conns[:n], conns[n+1:]...)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func garbageCollect(peers *Peers) []*Peer {
|
||||
var removed []*Peer
|
||||
peers.OnGC(func(peer *Peer) { removed = append(removed, peer) })
|
||||
peers.GarbageCollect()
|
||||
return removed
|
||||
}
|
||||
|
||||
func TestPeersGarbageCollection(t *testing.T) {
|
||||
const (
|
||||
peer1NameString = "01:00:00:01:00:00"
|
||||
peer2NameString = "02:00:00:02:00:00"
|
||||
peer3NameString = "03:00:00:03:00:00"
|
||||
)
|
||||
var (
|
||||
peer1Name, _ = PeerNameFromString(peer1NameString)
|
||||
peer2Name, _ = PeerNameFromString(peer2NameString)
|
||||
peer3Name, _ = PeerNameFromString(peer3NameString)
|
||||
)
|
||||
|
||||
// Create some peers with some connections to each other
|
||||
p1, ps1 := newNode(peer1Name)
|
||||
p2, ps2 := newNode(peer2Name)
|
||||
p3, ps3 := newNode(peer3Name)
|
||||
ps1.AddTestConnection(p2)
|
||||
ps2.AddTestRemoteConnection(p1, p2)
|
||||
ps2.AddTestConnection(p1)
|
||||
ps2.AddTestConnection(p3)
|
||||
ps3.AddTestConnection(p1)
|
||||
ps1.AddTestConnection(p3)
|
||||
ps2.AddTestRemoteConnection(p1, p3)
|
||||
ps2.AddTestRemoteConnection(p3, p1)
|
||||
|
||||
// Every peer is referenced, so nothing should be dropped
|
||||
require.Empty(t, garbageCollect(ps1), "peers removed")
|
||||
require.Empty(t, garbageCollect(ps2), "peers removed")
|
||||
require.Empty(t, garbageCollect(ps3), "peers removed")
|
||||
|
||||
// Drop the connection from 2 to 3, and 3 isn't garbage-collected
|
||||
// because 1 has a connection to 3
|
||||
ps2.DeleteTestConnection(p3)
|
||||
require.Empty(t, garbageCollect(ps2), "peers removed")
|
||||
|
||||
// Drop the connection from 1 to 3, and 3 will get removed by
|
||||
// garbage-collection
|
||||
ps1.DeleteTestConnection(p3)
|
||||
checkPeerArray(t, garbageCollect(ps1), p3)
|
||||
}
|
||||
|
||||
func TestShortIDCollisions(t *testing.T) {
|
||||
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
_, peers := newNode(PeerName(1 << PeerShortIDBits))
|
||||
|
||||
// Make enough peers that short id collisions are
|
||||
// overwhelmingly likely
|
||||
ps := make([]*Peer, 1<<PeerShortIDBits)
|
||||
for i := 0; i < 1<<PeerShortIDBits; i++ {
|
||||
ps[i] = NewPeer(PeerName(i), "", PeerUID(i), 0,
|
||||
PeerShortID(rng.Intn(1<<PeerShortIDBits)))
|
||||
}
|
||||
|
||||
shuffle := func() {
|
||||
for i := range ps {
|
||||
j := rng.Intn(i + 1)
|
||||
ps[i], ps[j] = ps[j], ps[i]
|
||||
}
|
||||
}
|
||||
|
||||
// Fill peers
|
||||
shuffle()
|
||||
var pending PeersPendingNotifications
|
||||
for _, p := range ps {
|
||||
peers.addByShortID(p, &pending)
|
||||
}
|
||||
|
||||
// Check invariants
|
||||
counts := make([]int, 1<<PeerShortIDBits)
|
||||
saw := func(p *Peer) {
|
||||
if p != peers.ourself.Peer {
|
||||
counts[p.UID]++
|
||||
}
|
||||
}
|
||||
|
||||
for shortID, entry := range peers.byShortID {
|
||||
if entry.peer == nil {
|
||||
// no principal peer for this short id, so
|
||||
// others must be empty
|
||||
require.Empty(t, entry.others)
|
||||
continue
|
||||
}
|
||||
|
||||
require.Equal(t, shortID, entry.peer.ShortID)
|
||||
saw(entry.peer)
|
||||
|
||||
for _, p := range entry.others {
|
||||
saw(p)
|
||||
require.Equal(t, shortID, p.ShortID)
|
||||
|
||||
// the principal peer should have the lowest name
|
||||
require.True(t, p.Name > entry.peer.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// Check that every peer was seen
|
||||
for _, n := range counts {
|
||||
require.Equal(t, 1, n)
|
||||
}
|
||||
|
||||
// Delete all the peers
|
||||
shuffle()
|
||||
for _, p := range ps {
|
||||
peers.deleteByShortID(p, &pending)
|
||||
}
|
||||
|
||||
for _, entry := range peers.byShortID {
|
||||
if entry.peer != peers.ourself.Peer {
|
||||
require.Nil(t, entry.peer)
|
||||
}
|
||||
|
||||
require.Empty(t, entry.others)
|
||||
}
|
||||
}
|
||||
|
||||
// Test the easy case of short id reassignment, when few short ids are taken
|
||||
func TestShortIDReassignmentEasy(t *testing.T) {
|
||||
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
_, peers := newNode(PeerName(0))
|
||||
|
||||
for i := 1; i <= 10; i++ {
|
||||
peers.FetchWithDefault(NewPeer(PeerName(i), "", PeerUID(i), 0,
|
||||
PeerShortID(rng.Intn(1<<PeerShortIDBits))))
|
||||
}
|
||||
|
||||
checkShortIDReassignment(t, peers)
|
||||
}
|
||||
|
||||
// Test the hard case of short id reassignment, when most short ids are taken
|
||||
func TestShortIDReassignmentHard(t *testing.T) {
|
||||
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
_, peers := newNode(PeerName(1 << PeerShortIDBits))
|
||||
|
||||
// Take all short ids
|
||||
ps := make([]*Peer, 1<<PeerShortIDBits)
|
||||
var pending PeersPendingNotifications
|
||||
for i := 0; i < 1<<PeerShortIDBits; i++ {
|
||||
ps[i] = NewPeer(PeerName(i), "", PeerUID(i), 0,
|
||||
PeerShortID(i))
|
||||
peers.addByShortID(ps[i], &pending)
|
||||
}
|
||||
|
||||
// As all short ids are taken, an attempted reassigment won't
|
||||
// do anything
|
||||
oldShortID := peers.ourself.ShortID
|
||||
require.False(t, peers.reassignLocalShortID(&pending))
|
||||
require.Equal(t, oldShortID, peers.ourself.ShortID)
|
||||
|
||||
// Free up a few ids
|
||||
for i := 0; i < 10; i++ {
|
||||
x := rng.Intn(len(ps))
|
||||
if ps[x] != nil {
|
||||
peers.deleteByShortID(ps[x], &pending)
|
||||
ps[x] = nil
|
||||
}
|
||||
}
|
||||
|
||||
checkShortIDReassignment(t, peers)
|
||||
}
|
||||
|
||||
func checkShortIDReassignment(t *testing.T, peers *Peers) {
|
||||
oldShortID := peers.ourself.ShortID
|
||||
peers.reassignLocalShortID(&PeersPendingNotifications{})
|
||||
require.NotEqual(t, oldShortID, peers.ourself.ShortID)
|
||||
require.Equal(t, peers.ourself.Peer, peers.byShortID[peers.ourself.ShortID].peer)
|
||||
}
|
||||
|
||||
func TestShortIDInvalidation(t *testing.T) {
|
||||
_, peers := newNode(PeerName(1 << PeerShortIDBits))
|
||||
|
||||
// need to use a short id that is not the local peer's
|
||||
shortID := peers.ourself.ShortID + 1
|
||||
|
||||
var pending PeersPendingNotifications
|
||||
|
||||
requireInvalidateShortIDs := func(expect bool) {
|
||||
require.Equal(t, expect, pending.invalidateShortIDs)
|
||||
pending.invalidateShortIDs = false
|
||||
}
|
||||
|
||||
// The use of a fresh short id does not cause invalidation
|
||||
a := NewPeer(PeerName(1), "", PeerUID(1), 0, shortID)
|
||||
peers.addByShortID(a, &pending)
|
||||
requireInvalidateShortIDs(false)
|
||||
|
||||
// An addition which does not change the mapping
|
||||
b := NewPeer(PeerName(2), "", PeerUID(2), 0, shortID)
|
||||
peers.addByShortID(b, &pending)
|
||||
requireInvalidateShortIDs(false)
|
||||
|
||||
// An addition which does change the mapping
|
||||
c := NewPeer(PeerName(0), "", PeerUID(0), 0, shortID)
|
||||
peers.addByShortID(c, &pending)
|
||||
requireInvalidateShortIDs(true)
|
||||
|
||||
// A deletion which does not change the mapping
|
||||
peers.deleteByShortID(b, &pending)
|
||||
requireInvalidateShortIDs(false)
|
||||
|
||||
// A deletion which does change the mapping
|
||||
peers.deleteByShortID(c, &pending)
|
||||
requireInvalidateShortIDs(true)
|
||||
|
||||
// Deleting the last peer with a short id does not cause invalidation
|
||||
peers.deleteByShortID(a, &pending)
|
||||
requireInvalidateShortIDs(false)
|
||||
|
||||
// .. but subsequent reuse of that short id does cause invalidation
|
||||
peers.addByShortID(a, &pending)
|
||||
requireInvalidateShortIDs(true)
|
||||
}
|
||||
|
||||
func TestShortIDPropagation(t *testing.T) {
|
||||
_, peers1 := newNode(PeerName(1))
|
||||
_, peers2 := newNode(PeerName(2))
|
||||
|
||||
peers1.AddTestConnection(peers2.ourself.Peer)
|
||||
peers1.ApplyUpdate(peers2.EncodePeers(peers2.Names()))
|
||||
peers12 := peers1.Fetch(PeerName(2))
|
||||
old := peers12.PeerSummary
|
||||
|
||||
require.True(t,
|
||||
peers2.reassignLocalShortID(&PeersPendingNotifications{}))
|
||||
peers1.ApplyUpdate(peers2.EncodePeers(peers2.Names()))
|
||||
require.NotEqual(t, old.Version, peers12.Version)
|
||||
require.NotEqual(t, old.ShortID, peers12.ShortID)
|
||||
}
|
||||
|
||||
func TestShortIDCollision(t *testing.T) {
|
||||
// Create 3 peers
|
||||
_, peers1 := newNode(PeerName(1))
|
||||
_, peers2 := newNode(PeerName(2))
|
||||
_, peers3 := newNode(PeerName(3))
|
||||
|
||||
var pending PeersPendingNotifications
|
||||
peers1.setLocalShortID(1, &pending)
|
||||
peers2.setLocalShortID(2, &pending)
|
||||
peers3.setLocalShortID(3, &pending)
|
||||
|
||||
peers2.AddTestConnection(peers1.ourself.Peer)
|
||||
peers3.AddTestConnection(peers2.ourself.Peer)
|
||||
|
||||
// Propogate from 1 to 2 to 3
|
||||
peers2.ApplyUpdate(peers1.EncodePeers(peers1.Names()))
|
||||
peers3.ApplyUpdate(peers2.EncodePeers(peers2.Names()))
|
||||
|
||||
// Force the short id of peer 1 to collide with peer 2. Peer
|
||||
// 1 has the lowest name, so it gets to keep the short id
|
||||
peers1.setLocalShortID(2, &pending)
|
||||
|
||||
oldShortID := peers2.ourself.ShortID
|
||||
_, updated, _ := peers2.ApplyUpdate(peers1.EncodePeers(peers1.Names()))
|
||||
|
||||
// peer 2 should have noticed the collision and resolved it
|
||||
require.NotEqual(t, oldShortID, peers2.ourself.ShortID)
|
||||
|
||||
// The Peers do not have a Router, so broadcastPeerUpdate does
|
||||
// nothing in the context of this test. So we fake what it
|
||||
// would do.
|
||||
updated[PeerName(2)] = void
|
||||
|
||||
// the update from peer 2 should include its short id change
|
||||
peers3.ApplyUpdate(peers2.EncodePeers(updated))
|
||||
require.Equal(t, peers2.ourself.ShortID,
|
||||
peers3.Fetch(PeerName(2)).ShortID)
|
||||
}
|
||||
|
||||
// Test the case where all short ids are taken, but then some peers go
|
||||
// away, so the local peer reassigns
|
||||
func TestDeferredShortIDReassignment(t *testing.T) {
|
||||
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
_, us := newNode(PeerName(1 << PeerShortIDBits))
|
||||
|
||||
// Connect us to other peers occupying all short ids
|
||||
others := make([]*Peers, 1<<PeerShortIDBits)
|
||||
var pending PeersPendingNotifications
|
||||
for i := range others {
|
||||
_, others[i] = newNode(PeerName(i))
|
||||
others[i].setLocalShortID(PeerShortID(i), &pending)
|
||||
us.AddTestConnection(others[i].ourself.Peer)
|
||||
}
|
||||
|
||||
// Check that, as expected, the local peer does not own its
|
||||
// short id
|
||||
require.NotEqual(t, us.ourself.Peer,
|
||||
us.byShortID[us.ourself.ShortID].peer)
|
||||
|
||||
// Disconnect one peer, and we should now be able to claim its
|
||||
// short id
|
||||
other := others[rng.Intn(1<<PeerShortIDBits)]
|
||||
us.DeleteTestConnection(other.ourself.Peer)
|
||||
us.GarbageCollect()
|
||||
|
||||
require.Equal(t, us.ourself.Peer, us.byShortID[us.ourself.ShortID].peer)
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
package mesh
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
Protocol = "weave"
|
||||
ProtocolMinVersion = 1
|
||||
ProtocolMaxVersion = 2
|
||||
)
|
||||
|
||||
var (
|
||||
ProtocolBytes = []byte(Protocol)
|
||||
|
||||
HeaderTimeout = 10 * time.Second
|
||||
|
||||
ProtocolV1Features = []string{
|
||||
"ConnID",
|
||||
"Name",
|
||||
"NickName",
|
||||
"PeerNameFlavour",
|
||||
"UID",
|
||||
}
|
||||
|
||||
ErrExpectedCrypto = fmt.Errorf("Password specified, but peer requested an unencrypted connection")
|
||||
ErrExpectedNoCrypto = fmt.Errorf("No password specificed, but peer requested an encrypted connection")
|
||||
)
|
||||
|
||||
// We don't need the full net.TCPConn to do the protocol intro. This
|
||||
// interface contains just the parts we do need, to support testing
|
||||
type ProtocolIntroConn interface {
|
||||
// io.Reader
|
||||
Read(b []byte) (n int, err error)
|
||||
|
||||
// io.Writer
|
||||
Write(b []byte) (n int, err error)
|
||||
|
||||
// net.Conn's deadline methods
|
||||
SetDeadline(t time.Time) error
|
||||
SetReadDeadline(t time.Time) error
|
||||
SetWriteDeadline(t time.Time) error
|
||||
}
|
||||
|
||||
type ProtocolIntroParams struct {
|
||||
MinVersion byte
|
||||
MaxVersion byte
|
||||
Features map[string]string
|
||||
Conn ProtocolIntroConn
|
||||
Password []byte
|
||||
Outbound bool
|
||||
}
|
||||
|
||||
type ProtocolIntroResults struct {
|
||||
Features map[string]string
|
||||
Receiver TCPReceiver
|
||||
Sender TCPSender
|
||||
SessionKey *[32]byte
|
||||
Version byte
|
||||
}
|
||||
|
||||
func (params ProtocolIntroParams) DoIntro() (res ProtocolIntroResults, err error) {
|
||||
if err = params.Conn.SetDeadline(time.Now().Add(HeaderTimeout)); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if res.Version, err = params.exchangeProtocolHeader(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var pubKey, privKey *[32]byte
|
||||
if params.Password != nil {
|
||||
if pubKey, privKey, err = GenerateKeyPair(); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err = params.Conn.SetWriteDeadline(time.Time{}); err != nil {
|
||||
return
|
||||
}
|
||||
if err = params.Conn.SetReadDeadline(time.Now().Add(TCPHeartbeat * 2)); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch res.Version {
|
||||
case 1:
|
||||
err = res.doIntroV1(params, pubKey, privKey)
|
||||
case 2:
|
||||
err = res.doIntroV2(params, pubKey, privKey)
|
||||
default:
|
||||
panic("unhandled protocol version")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (params ProtocolIntroParams) exchangeProtocolHeader() (byte, error) {
|
||||
// Write in a separate goroutine to avoid the possibility of
|
||||
// deadlock. The result channel is of size 1 so that the
|
||||
// goroutine does not linger even if we encounter an error on
|
||||
// the read side.
|
||||
sendHeader := append(ProtocolBytes, params.MinVersion, params.MaxVersion)
|
||||
writeDone := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := params.Conn.Write(sendHeader)
|
||||
writeDone <- err
|
||||
}()
|
||||
|
||||
header := make([]byte, len(ProtocolBytes)+2)
|
||||
if n, err := io.ReadFull(params.Conn, header); err != nil && n == 0 {
|
||||
return 0, fmt.Errorf("failed to receive remote protocol header: %s", err)
|
||||
} else if err != nil {
|
||||
return 0, fmt.Errorf("received incomplete remote protocol header (%d octets instead of %d): %v; error: %s",
|
||||
n, len(header), header[:n], err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(ProtocolBytes, header[:len(ProtocolBytes)]) {
|
||||
return 0, fmt.Errorf("remote protocol header not recognised: %v", header[:len(ProtocolBytes)])
|
||||
}
|
||||
|
||||
theirMinVersion := header[len(ProtocolBytes)]
|
||||
minVersion := theirMinVersion
|
||||
if params.MinVersion > minVersion {
|
||||
minVersion = params.MinVersion
|
||||
}
|
||||
|
||||
theirMaxVersion := header[len(ProtocolBytes)+1]
|
||||
maxVersion := theirMaxVersion
|
||||
if maxVersion > params.MaxVersion {
|
||||
maxVersion = params.MaxVersion
|
||||
}
|
||||
|
||||
if minVersion > maxVersion {
|
||||
return 0, fmt.Errorf("remote version range [%d,%d] is incompatible with ours [%d,%d]",
|
||||
theirMinVersion, theirMaxVersion,
|
||||
params.MinVersion, params.MaxVersion)
|
||||
}
|
||||
|
||||
if err := <-writeDone; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return maxVersion, nil
|
||||
}
|
||||
|
||||
// The V1 procotol consists of the protocol identification/version
|
||||
// header, followed by a stream of gobified values. The first value
|
||||
// is the encoded features map (never encrypted). The subsequent
|
||||
// values are the messages on the connection (encrypted for an
|
||||
// encrypted connection). For an encrypted connection, the public key
|
||||
// is passed in the "PublicKey" feature as a string of hex digits.
|
||||
func (res *ProtocolIntroResults) doIntroV1(params ProtocolIntroParams, pubKey, privKey *[32]byte) error {
|
||||
features := filterV1Features(params.Features)
|
||||
if pubKey != nil {
|
||||
features["PublicKey"] = hex.EncodeToString(pubKey[:])
|
||||
}
|
||||
|
||||
enc := gob.NewEncoder(params.Conn)
|
||||
dec := gob.NewDecoder(params.Conn)
|
||||
|
||||
// Encode in a separate goroutine to avoid the possibility of
|
||||
// deadlock. The result channel is of size 1 so that the
|
||||
// goroutine does not linger even if we encounter an error on
|
||||
// the read side.
|
||||
encodeDone := make(chan error, 1)
|
||||
go func() {
|
||||
encodeDone <- enc.Encode(features)
|
||||
}()
|
||||
|
||||
if err := dec.Decode(&res.Features); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := <-encodeDone; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res.Sender = NewGobTCPSender(enc)
|
||||
res.Receiver = NewGobTCPReceiver(dec)
|
||||
|
||||
if pubKey == nil {
|
||||
if _, present := res.Features["PublicKey"]; present {
|
||||
return ErrExpectedNoCrypto
|
||||
}
|
||||
} else {
|
||||
remotePubKeyStr, ok := res.Features["PublicKey"]
|
||||
if !ok {
|
||||
return ErrExpectedCrypto
|
||||
}
|
||||
|
||||
remotePubKey, err := hex.DecodeString(remotePubKeyStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res.setupCrypto(params, remotePubKey, privKey)
|
||||
}
|
||||
|
||||
res.Features = filterV1Features(res.Features)
|
||||
return nil
|
||||
}
|
||||
|
||||
// In the V1 protocol, the intro fields are sent unencrypted. So we
|
||||
// restrict them to an established subset of fields that are assumed
|
||||
// to be safe.
|
||||
func filterV1Features(intro map[string]string) map[string]string {
|
||||
safe := make(map[string]string)
|
||||
for _, k := range ProtocolV1Features {
|
||||
if val, ok := intro[k]; ok {
|
||||
safe[k] = val
|
||||
}
|
||||
}
|
||||
|
||||
return safe
|
||||
}
|
||||
|
||||
// The V2 procotol consists of the protocol identification/version
|
||||
// header, followed by:
|
||||
//
|
||||
// - A single "encryption flag" byte: 0 for no encryption, 1 for
|
||||
// encryption.
|
||||
//
|
||||
// - When the connection is encrypted, 32 bytes follow containing the
|
||||
// public key.
|
||||
//
|
||||
// - Then a stream of length-prefixed messages, which are encrypted
|
||||
// for an encrypted connection.
|
||||
//
|
||||
// The first message contains the encoded features map (so in contrast
|
||||
// to V1, it will be encrypted on an encrypted connection).
|
||||
func (res *ProtocolIntroResults) doIntroV2(params ProtocolIntroParams, pubKey, privKey *[32]byte) error {
|
||||
// Public key exchange
|
||||
var wbuf []byte
|
||||
if pubKey == nil {
|
||||
wbuf = []byte{0}
|
||||
} else {
|
||||
wbuf = make([]byte, 1+len(*pubKey))
|
||||
wbuf[0] = 1
|
||||
copy(wbuf[1:], (*pubKey)[:])
|
||||
}
|
||||
|
||||
// Write in a separate goroutine to avoid the possibility of
|
||||
// deadlock. The result channel is of size 1 so that the
|
||||
// goroutine does not linger even if we encounter an error on
|
||||
// the read side.
|
||||
writeDone := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := params.Conn.Write(wbuf)
|
||||
writeDone <- err
|
||||
}()
|
||||
|
||||
rbuf := make([]byte, 1)
|
||||
if _, err := io.ReadFull(params.Conn, rbuf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch rbuf[0] {
|
||||
case 0:
|
||||
if pubKey != nil {
|
||||
return ErrExpectedCrypto
|
||||
}
|
||||
|
||||
res.Sender = NewLengthPrefixTCPSender(params.Conn)
|
||||
res.Receiver = NewLengthPrefixTCPReceiver(params.Conn)
|
||||
|
||||
case 1:
|
||||
if pubKey == nil {
|
||||
return ErrExpectedNoCrypto
|
||||
}
|
||||
|
||||
rbuf = make([]byte, len(pubKey))
|
||||
if _, err := io.ReadFull(params.Conn, rbuf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res.Sender = NewLengthPrefixTCPSender(params.Conn)
|
||||
res.Receiver = NewLengthPrefixTCPReceiver(params.Conn)
|
||||
res.setupCrypto(params, rbuf, privKey)
|
||||
|
||||
default:
|
||||
return fmt.Errorf("Bad encryption flag %d", rbuf[0])
|
||||
}
|
||||
|
||||
if err := <-writeDone; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Features exchange
|
||||
go func() {
|
||||
buf := new(bytes.Buffer)
|
||||
if err := gob.NewEncoder(buf).Encode(¶ms.Features); err != nil {
|
||||
writeDone <- err
|
||||
return
|
||||
}
|
||||
|
||||
writeDone <- res.Sender.Send(buf.Bytes())
|
||||
}()
|
||||
|
||||
rbuf, err := res.Receiver.Receive()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := gob.NewDecoder(bytes.NewReader(rbuf)).Decode(&res.Features); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := <-writeDone; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (res *ProtocolIntroResults) setupCrypto(params ProtocolIntroParams, remotePubKey []byte, privKey *[32]byte) {
|
||||
var remotePubKeyArr [32]byte
|
||||
copy(remotePubKeyArr[:], remotePubKey)
|
||||
res.SessionKey = FormSessionKey(&remotePubKeyArr, privKey, params.Password)
|
||||
res.Sender = NewEncryptedTCPSender(res.Sender, res.SessionKey, params.Outbound)
|
||||
res.Receiver = NewEncryptedTCPReceiver(res.Receiver, res.SessionKey, params.Outbound)
|
||||
}
|
||||
|
||||
type ProtocolTag byte
|
||||
|
||||
const (
|
||||
ProtocolHeartbeat = iota
|
||||
ProtocolReserved1 // legacy overlay control messages
|
||||
ProtocolReserved2 //
|
||||
ProtocolReserved3 //
|
||||
ProtocolGossip
|
||||
ProtocolGossipUnicast
|
||||
ProtocolGossipBroadcast
|
||||
ProtocolOverlayControlMsg
|
||||
)
|
||||
|
||||
type ProtocolMsg struct {
|
||||
tag ProtocolTag
|
||||
msg []byte
|
||||
}
|
||||
|
||||
type ProtocolSender interface {
|
||||
SendProtocolMsg(m ProtocolMsg)
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package mesh
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
"golang.org/x/crypto/nacl/secretbox"
|
||||
)
|
||||
|
||||
const MaxTCPMsgSize = 10 * 1024 * 1024
|
||||
|
||||
func GenerateKeyPair() (publicKey, privateKey *[32]byte, err error) {
|
||||
return box.GenerateKey(rand.Reader)
|
||||
}
|
||||
|
||||
func FormSessionKey(remotePublicKey, localPrivateKey *[32]byte, secretKey []byte) *[32]byte {
|
||||
var sharedKey [32]byte
|
||||
box.Precompute(&sharedKey, remotePublicKey, localPrivateKey)
|
||||
sharedKeySlice := sharedKey[:]
|
||||
sharedKeySlice = append(sharedKeySlice, secretKey...)
|
||||
sessionKey := sha256.Sum256(sharedKeySlice)
|
||||
return &sessionKey
|
||||
}
|
||||
|
||||
// TCP Senders/Receivers
|
||||
|
||||
// The lowest 64 bits of the nonce contain the message sequence
|
||||
// number. The top most bit indicates the connection polarity at the
|
||||
// sender - '1' for outbound; the next indicates protocol type - '1'
|
||||
// for TCP. The remaining 126 bits are zero. The polarity is needed so
|
||||
// that the two ends of a connection do not use the same nonces; the
|
||||
// protocol type so that the TCP connection nonces are distinct from
|
||||
// nonces used by overlay connections, if they share the session key.
|
||||
// This is a requirement of the NaCl Security Model; see
|
||||
// http://nacl.cr.yp.to/box.html.
|
||||
type TCPCryptoState struct {
|
||||
sessionKey *[32]byte
|
||||
nonce [24]byte
|
||||
seqNo uint64
|
||||
}
|
||||
|
||||
func NewTCPCryptoState(sessionKey *[32]byte, outbound bool) *TCPCryptoState {
|
||||
s := &TCPCryptoState{sessionKey: sessionKey}
|
||||
if outbound {
|
||||
s.nonce[0] |= (1 << 7)
|
||||
}
|
||||
s.nonce[0] |= (1 << 6)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *TCPCryptoState) advance() {
|
||||
s.seqNo++
|
||||
binary.BigEndian.PutUint64(s.nonce[16:24], s.seqNo)
|
||||
}
|
||||
|
||||
type TCPSender interface {
|
||||
Send([]byte) error
|
||||
}
|
||||
|
||||
type GobTCPSender struct {
|
||||
encoder *gob.Encoder
|
||||
}
|
||||
|
||||
type LengthPrefixTCPSender struct {
|
||||
writer io.Writer
|
||||
}
|
||||
|
||||
type EncryptedTCPSender struct {
|
||||
sync.RWMutex
|
||||
sender TCPSender
|
||||
state *TCPCryptoState
|
||||
}
|
||||
|
||||
func NewGobTCPSender(encoder *gob.Encoder) *GobTCPSender {
|
||||
return &GobTCPSender{encoder: encoder}
|
||||
}
|
||||
|
||||
func (sender *GobTCPSender) Send(msg []byte) error {
|
||||
return sender.encoder.Encode(msg)
|
||||
}
|
||||
|
||||
func NewLengthPrefixTCPSender(writer io.Writer) *LengthPrefixTCPSender {
|
||||
return &LengthPrefixTCPSender{writer: writer}
|
||||
}
|
||||
|
||||
func (sender *LengthPrefixTCPSender) Send(msg []byte) error {
|
||||
l := len(msg)
|
||||
if l > MaxTCPMsgSize {
|
||||
return fmt.Errorf("outgoing message exceeds maximum size: %d > %d", l, MaxTCPMsgSize)
|
||||
}
|
||||
// We copy the message so we can send it in a single Write
|
||||
// operation, thus making this thread-safe without locking.
|
||||
prefixedMsg := make([]byte, 4+l)
|
||||
binary.BigEndian.PutUint32(prefixedMsg, uint32(l))
|
||||
copy(prefixedMsg[4:], msg)
|
||||
_, err := sender.writer.Write(prefixedMsg)
|
||||
return err
|
||||
}
|
||||
|
||||
func NewEncryptedTCPSender(sender TCPSender, sessionKey *[32]byte, outbound bool) *EncryptedTCPSender {
|
||||
return &EncryptedTCPSender{sender: sender, state: NewTCPCryptoState(sessionKey, outbound)}
|
||||
}
|
||||
|
||||
func (sender *EncryptedTCPSender) Send(msg []byte) error {
|
||||
sender.Lock()
|
||||
defer sender.Unlock()
|
||||
encodedMsg := secretbox.Seal(nil, msg, &sender.state.nonce, sender.state.sessionKey)
|
||||
sender.state.advance()
|
||||
return sender.sender.Send(encodedMsg)
|
||||
}
|
||||
|
||||
type TCPReceiver interface {
|
||||
Receive() ([]byte, error)
|
||||
}
|
||||
|
||||
type GobTCPReceiver struct {
|
||||
decoder *gob.Decoder
|
||||
}
|
||||
|
||||
type LengthPrefixTCPReceiver struct {
|
||||
reader io.Reader
|
||||
}
|
||||
|
||||
type EncryptedTCPReceiver struct {
|
||||
receiver TCPReceiver
|
||||
state *TCPCryptoState
|
||||
}
|
||||
|
||||
func NewGobTCPReceiver(decoder *gob.Decoder) *GobTCPReceiver {
|
||||
return &GobTCPReceiver{decoder: decoder}
|
||||
}
|
||||
|
||||
func (receiver *GobTCPReceiver) Receive() ([]byte, error) {
|
||||
var msg []byte
|
||||
err := receiver.decoder.Decode(&msg)
|
||||
return msg, err
|
||||
}
|
||||
|
||||
func NewLengthPrefixTCPReceiver(reader io.Reader) *LengthPrefixTCPReceiver {
|
||||
return &LengthPrefixTCPReceiver{reader: reader}
|
||||
}
|
||||
|
||||
func (receiver *LengthPrefixTCPReceiver) Receive() ([]byte, error) {
|
||||
lenPrefix := make([]byte, 4)
|
||||
if _, err := io.ReadFull(receiver.reader, lenPrefix); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l := binary.BigEndian.Uint32(lenPrefix)
|
||||
if l > MaxTCPMsgSize {
|
||||
return nil, fmt.Errorf("incoming message exceeds maximum size: %d > %d", l, MaxTCPMsgSize)
|
||||
}
|
||||
msg := make([]byte, l)
|
||||
_, err := io.ReadFull(receiver.reader, msg)
|
||||
return msg, err
|
||||
}
|
||||
|
||||
func NewEncryptedTCPReceiver(receiver TCPReceiver, sessionKey *[32]byte, outbound bool) *EncryptedTCPReceiver {
|
||||
return &EncryptedTCPReceiver{receiver: receiver, state: NewTCPCryptoState(sessionKey, !outbound)}
|
||||
}
|
||||
|
||||
func (receiver *EncryptedTCPReceiver) Receive() ([]byte, error) {
|
||||
msg, err := receiver.receiver.Receive()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
decodedMsg, success := secretbox.Open(nil, msg, &receiver.state.nonce, receiver.state.sessionKey)
|
||||
if !success {
|
||||
return nil, fmt.Errorf("Unable to decrypt TCP msg")
|
||||
}
|
||||
|
||||
receiver.state.advance()
|
||||
return decodedMsg, nil
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package mesh
|
||||
|
||||
import (
|
||||
"io"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type testConn struct {
|
||||
io.Writer
|
||||
io.Reader
|
||||
}
|
||||
|
||||
func (testConn) SetDeadline(t time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (testConn) SetReadDeadline(t time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (testConn) SetWriteDeadline(t time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func connPair() (ProtocolIntroConn, ProtocolIntroConn) {
|
||||
a := testConn{}
|
||||
b := testConn{}
|
||||
a.Reader, b.Writer = io.Pipe()
|
||||
b.Reader, a.Writer = io.Pipe()
|
||||
return &a, &b
|
||||
}
|
||||
|
||||
func doIntro(t *testing.T, params ProtocolIntroParams) <-chan ProtocolIntroResults {
|
||||
ch := make(chan ProtocolIntroResults, 1)
|
||||
go func() {
|
||||
res, err := params.DoIntro()
|
||||
require.Nil(t, err)
|
||||
ch <- res
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
||||
func doProtocolIntro(t *testing.T, aver, bver byte, password []byte) byte {
|
||||
aconn, bconn := connPair()
|
||||
aresch := doIntro(t, ProtocolIntroParams{
|
||||
MinVersion: ProtocolMinVersion,
|
||||
MaxVersion: aver,
|
||||
Features: map[string]string{"Name": "A"},
|
||||
Conn: aconn,
|
||||
Outbound: true,
|
||||
Password: password,
|
||||
})
|
||||
bresch := doIntro(t, ProtocolIntroParams{
|
||||
MinVersion: ProtocolMinVersion,
|
||||
MaxVersion: bver,
|
||||
Features: map[string]string{"Name": "B"},
|
||||
Conn: bconn,
|
||||
Outbound: false,
|
||||
Password: password,
|
||||
})
|
||||
ares := <-aresch
|
||||
bres := <-bresch
|
||||
|
||||
// Check that features were conveyed
|
||||
require.Equal(t, "B", ares.Features["Name"])
|
||||
require.Equal(t, "A", bres.Features["Name"])
|
||||
|
||||
// Check that Senders and Receivers work
|
||||
go func() {
|
||||
require.Nil(t, ares.Sender.Send([]byte("Hello from A")))
|
||||
require.Nil(t, bres.Sender.Send([]byte("Hello from B")))
|
||||
}()
|
||||
|
||||
data, err := bres.Receiver.Receive()
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, "Hello from A", string(data))
|
||||
|
||||
data, err = ares.Receiver.Receive()
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, "Hello from B", string(data))
|
||||
|
||||
require.Equal(t, ares.Version, bres.Version)
|
||||
return ares.Version
|
||||
}
|
||||
|
||||
func TestProtocolIntro(t *testing.T) {
|
||||
require.Equal(t, 2, int(doProtocolIntro(t, 2, 2, nil)))
|
||||
require.Equal(t, 2, int(doProtocolIntro(t, 2, 2, []byte("sekr1t"))))
|
||||
require.Equal(t, 1, int(doProtocolIntro(t, 1, 2, nil)))
|
||||
require.Equal(t, 1, int(doProtocolIntro(t, 1, 2, []byte("pa55"))))
|
||||
require.Equal(t, 1, int(doProtocolIntro(t, 2, 1, nil)))
|
||||
require.Equal(t, 1, int(doProtocolIntro(t, 2, 1, []byte("w0rd"))))
|
||||
}
|
||||
+198
@@ -0,0 +1,198 @@
|
||||
package mesh
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/weaveworks/weave/common"
|
||||
)
|
||||
|
||||
const (
|
||||
Port = 6783
|
||||
ChannelSize = 16
|
||||
TCPHeartbeat = 30 * time.Second
|
||||
GossipInterval = 30 * time.Second
|
||||
MaxDuration = time.Duration(math.MaxInt64)
|
||||
|
||||
acceptMaxTokens = 100 // [1]
|
||||
acceptTokenDelay = 100 * time.Millisecond // [2]
|
||||
)
|
||||
|
||||
// [1] capacity of token bucket for rate limiting accepts
|
||||
|
||||
// [2] control rate at which new tokens are added to the bucket
|
||||
|
||||
var (
|
||||
log = common.Log
|
||||
checkFatal = common.CheckFatal
|
||||
checkWarn = common.CheckWarn
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Port int
|
||||
ProtocolMinVersion byte
|
||||
Password []byte
|
||||
ConnLimit int
|
||||
PeerDiscovery bool
|
||||
TrustedSubnets []*net.IPNet
|
||||
}
|
||||
|
||||
type Router struct {
|
||||
Config
|
||||
Overlay Overlay
|
||||
Ourself *LocalPeer
|
||||
Peers *Peers
|
||||
Routes *Routes
|
||||
ConnectionMaker *ConnectionMaker
|
||||
gossipLock sync.RWMutex
|
||||
gossipChannels GossipChannels
|
||||
TopologyGossip Gossip
|
||||
acceptLimiter *TokenBucket
|
||||
}
|
||||
|
||||
func NewRouter(config Config, name PeerName, nickName string, overlay Overlay) *Router {
|
||||
router := &Router{Config: config, gossipChannels: make(GossipChannels)}
|
||||
|
||||
if overlay == nil {
|
||||
overlay = NullOverlay{}
|
||||
}
|
||||
|
||||
router.Overlay = overlay
|
||||
router.Ourself = NewLocalPeer(name, nickName, router)
|
||||
router.Peers = NewPeers(router.Ourself)
|
||||
router.Peers.OnGC(func(peer *Peer) {
|
||||
log.Println("Removed unreachable peer", peer)
|
||||
})
|
||||
router.Routes = NewRoutes(router.Ourself, router.Peers)
|
||||
router.ConnectionMaker = NewConnectionMaker(router.Ourself, router.Peers, router.Port, router.PeerDiscovery)
|
||||
router.TopologyGossip = router.NewGossip("topology", router)
|
||||
router.acceptLimiter = NewTokenBucket(acceptMaxTokens, acceptTokenDelay)
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
// Start listening for TCP connections. This is separate from
|
||||
// NewRouter so that gossipers can register before we start forming
|
||||
// connections.
|
||||
func (router *Router) Start() {
|
||||
router.listenTCP(router.Port)
|
||||
}
|
||||
|
||||
func (router *Router) Stop() error {
|
||||
// TODO: perform graceful shutdown...
|
||||
return nil
|
||||
}
|
||||
|
||||
func (router *Router) UsingPassword() bool {
|
||||
return router.Password != nil
|
||||
}
|
||||
|
||||
func (router *Router) listenTCP(localPort int) {
|
||||
localAddr, err := net.ResolveTCPAddr("tcp4", fmt.Sprint(":", localPort))
|
||||
checkFatal(err)
|
||||
ln, err := net.ListenTCP("tcp4", localAddr)
|
||||
checkFatal(err)
|
||||
go func() {
|
||||
defer ln.Close()
|
||||
for {
|
||||
tcpConn, err := ln.AcceptTCP()
|
||||
if err != nil {
|
||||
log.Errorln(err)
|
||||
continue
|
||||
}
|
||||
router.acceptTCP(tcpConn)
|
||||
router.acceptLimiter.Wait()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (router *Router) acceptTCP(tcpConn *net.TCPConn) {
|
||||
remoteAddrStr := tcpConn.RemoteAddr().String()
|
||||
log.Printf("->[%s] connection accepted", remoteAddrStr)
|
||||
connRemote := NewRemoteConnection(router.Ourself.Peer, nil, remoteAddrStr, false, false)
|
||||
StartLocalConnection(connRemote, tcpConn, router, true)
|
||||
}
|
||||
|
||||
// Gossiper methods - the Router is the topology Gossiper
|
||||
|
||||
type TopologyGossipData struct {
|
||||
peers *Peers
|
||||
update PeerNameSet
|
||||
}
|
||||
|
||||
func (d *TopologyGossipData) Merge(other GossipData) GossipData {
|
||||
names := make(PeerNameSet)
|
||||
for name := range d.update {
|
||||
names[name] = void
|
||||
}
|
||||
for name := range other.(*TopologyGossipData).update {
|
||||
names[name] = void
|
||||
}
|
||||
return &TopologyGossipData{peers: d.peers, update: names}
|
||||
}
|
||||
|
||||
func (d *TopologyGossipData) Encode() [][]byte {
|
||||
return [][]byte{d.peers.EncodePeers(d.update)}
|
||||
}
|
||||
|
||||
func (router *Router) BroadcastTopologyUpdate(update []*Peer) {
|
||||
names := make(PeerNameSet)
|
||||
for _, p := range update {
|
||||
names[p.Name] = void
|
||||
}
|
||||
router.TopologyGossip.GossipBroadcast(
|
||||
&TopologyGossipData{peers: router.Peers, update: names})
|
||||
}
|
||||
|
||||
func (router *Router) OnGossipUnicast(sender PeerName, msg []byte) error {
|
||||
return fmt.Errorf("unexpected topology gossip unicast: %v", msg)
|
||||
}
|
||||
|
||||
func (router *Router) OnGossipBroadcast(_ PeerName, update []byte) (GossipData, error) {
|
||||
origUpdate, _, err := router.applyTopologyUpdate(update)
|
||||
if err != nil || len(origUpdate) == 0 {
|
||||
return nil, err
|
||||
}
|
||||
return &TopologyGossipData{peers: router.Peers, update: origUpdate}, nil
|
||||
}
|
||||
|
||||
func (router *Router) Gossip() GossipData {
|
||||
return &TopologyGossipData{peers: router.Peers, update: router.Peers.Names()}
|
||||
}
|
||||
|
||||
func (router *Router) OnGossip(update []byte) (GossipData, error) {
|
||||
_, newUpdate, err := router.applyTopologyUpdate(update)
|
||||
if err != nil || len(newUpdate) == 0 {
|
||||
return nil, err
|
||||
}
|
||||
return &TopologyGossipData{peers: router.Peers, update: newUpdate}, nil
|
||||
}
|
||||
|
||||
func (router *Router) applyTopologyUpdate(update []byte) (PeerNameSet, PeerNameSet, error) {
|
||||
origUpdate, newUpdate, err := router.Peers.ApplyUpdate(update)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if len(newUpdate) > 0 {
|
||||
router.ConnectionMaker.Refresh()
|
||||
router.Routes.Recalculate()
|
||||
}
|
||||
return origUpdate, newUpdate, nil
|
||||
}
|
||||
|
||||
func (router *Router) Trusts(remote *RemoteConnection) bool {
|
||||
if tcpAddr, err := net.ResolveTCPAddr("tcp4", remote.remoteTCPAddr); err == nil {
|
||||
for _, trustedSubnet := range router.TrustedSubnets {
|
||||
if trustedSubnet.Contains(tcpAddr.IP) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Should not happen as remoteTCPAddr was obtained from TCPConn
|
||||
log.Errorf("Unable to parse remote TCP addr: %s", err)
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,47 +1,52 @@
|
||||
package router
|
||||
package mesh
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"math"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type unicastRoutes map[PeerName]PeerName
|
||||
type broadcastRoutes map[PeerName][]PeerName
|
||||
|
||||
type Routes struct {
|
||||
sync.RWMutex
|
||||
ourself *Peer
|
||||
ourself *LocalPeer
|
||||
peers *Peers
|
||||
unicast map[PeerName]PeerName
|
||||
unicastAll map[PeerName]PeerName // [1]
|
||||
broadcast map[PeerName][]PeerName
|
||||
broadcastAll map[PeerName][]PeerName // [1]
|
||||
onChange []func()
|
||||
unicast unicastRoutes
|
||||
unicastAll unicastRoutes // [1]
|
||||
broadcast broadcastRoutes
|
||||
broadcastAll broadcastRoutes // [1]
|
||||
recalculate chan<- *struct{}
|
||||
wait chan<- chan struct{}
|
||||
// [1] based on *all* connections, not just established &
|
||||
// symmetric ones
|
||||
}
|
||||
|
||||
func NewRoutes(ourself *Peer, peers *Peers) *Routes {
|
||||
func NewRoutes(ourself *LocalPeer, peers *Peers) *Routes {
|
||||
recalculate := make(chan *struct{}, 1)
|
||||
wait := make(chan chan struct{})
|
||||
routes := &Routes{
|
||||
ourself: ourself,
|
||||
peers: peers,
|
||||
unicast: make(map[PeerName]PeerName),
|
||||
unicastAll: make(map[PeerName]PeerName),
|
||||
broadcast: make(map[PeerName][]PeerName),
|
||||
broadcastAll: make(map[PeerName][]PeerName)}
|
||||
unicast: make(unicastRoutes),
|
||||
unicastAll: make(unicastRoutes),
|
||||
broadcast: make(broadcastRoutes),
|
||||
broadcastAll: make(broadcastRoutes),
|
||||
recalculate: recalculate,
|
||||
wait: wait}
|
||||
routes.unicast[ourself.Name] = UnknownPeerName
|
||||
routes.unicastAll[ourself.Name] = UnknownPeerName
|
||||
routes.broadcast[ourself.Name] = []PeerName{}
|
||||
routes.broadcastAll[ourself.Name] = []PeerName{}
|
||||
go routes.run(recalculate, wait)
|
||||
return routes
|
||||
}
|
||||
|
||||
func (routes *Routes) Start() {
|
||||
recalculate := make(chan *struct{}, 1)
|
||||
wait := make(chan chan struct{})
|
||||
routes.recalculate = recalculate
|
||||
routes.wait = wait
|
||||
go routes.run(recalculate, wait)
|
||||
func (routes *Routes) OnChange(callback func()) {
|
||||
routes.Lock()
|
||||
defer routes.Unlock()
|
||||
routes.onChange = append(routes.onChange, callback)
|
||||
}
|
||||
|
||||
func (routes *Routes) PeerNames() PeerNameSet {
|
||||
@@ -65,21 +70,13 @@ func (routes *Routes) UnicastAll(name PeerName) (PeerName, bool) {
|
||||
func (routes *Routes) Broadcast(name PeerName) []PeerName {
|
||||
routes.RLock()
|
||||
defer routes.RUnlock()
|
||||
hops, found := routes.broadcast[name]
|
||||
if !found {
|
||||
return []PeerName{}
|
||||
}
|
||||
return hops
|
||||
return routes.broadcast[name]
|
||||
}
|
||||
|
||||
func (routes *Routes) BroadcastAll(name PeerName) []PeerName {
|
||||
routes.RLock()
|
||||
defer routes.RUnlock()
|
||||
hops, found := routes.broadcastAll[name]
|
||||
if !found {
|
||||
return []PeerName{}
|
||||
}
|
||||
return hops
|
||||
return routes.broadcastAll[name]
|
||||
}
|
||||
|
||||
// Choose min(log2(n_peers), n_neighbouring_peers) neighbours, with a
|
||||
@@ -95,40 +92,27 @@ func (routes *Routes) BroadcastAll(name PeerName) []PeerName {
|
||||
// returns a higher proportion of neighbours than elsewhere. In
|
||||
// extremis, on peers with fewer than log2(n_peers) neighbours, all
|
||||
// neighbours are returned.
|
||||
func (routes *Routes) RandomNeighbours(except PeerName) PeerNameSet {
|
||||
res := make(PeerNameSet)
|
||||
func (routes *Routes) RandomNeighbours(except PeerName) []PeerName {
|
||||
destinations := make(PeerNameSet)
|
||||
routes.RLock()
|
||||
defer routes.RUnlock()
|
||||
count := int(math.Log2(float64(len(routes.unicastAll))))
|
||||
// depends on go's random map iteration
|
||||
for _, dst := range routes.unicastAll {
|
||||
if dst != UnknownPeerName && dst != except {
|
||||
res[dst] = void
|
||||
if len(res) >= count {
|
||||
destinations[dst] = void
|
||||
if len(destinations) >= count {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
res := make([]PeerName, 0, len(destinations))
|
||||
for dst := range destinations {
|
||||
res = append(res, dst)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (routes *Routes) String() string {
|
||||
var buf bytes.Buffer
|
||||
routes.RLock()
|
||||
defer routes.RUnlock()
|
||||
fmt.Fprintln(&buf, "unicast:")
|
||||
for name, hop := range routes.unicast {
|
||||
fmt.Fprintf(&buf, "%s -> %s\n", name, hop)
|
||||
}
|
||||
fmt.Fprintln(&buf, "broadcast:")
|
||||
for name, hops := range routes.broadcast {
|
||||
fmt.Fprintf(&buf, "%s -> %v\n", name, hops)
|
||||
}
|
||||
// We don't include the 'all' routes here since they are of
|
||||
// limited utility in troubleshooting
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// Request recalculation of the routing table. This is async but can
|
||||
// effectively be made synchronous with a subsequent call to
|
||||
// EnsureRecalculated.
|
||||
@@ -166,18 +150,32 @@ func (routes *Routes) run(recalculate <-chan *struct{}, wait <-chan chan struct{
|
||||
}
|
||||
|
||||
func (routes *Routes) calculate() {
|
||||
routes.peers.RLock()
|
||||
routes.ourself.RLock()
|
||||
var (
|
||||
oldUnicast = routes.unicast
|
||||
oldBroadcast = routes.broadcast
|
||||
unicast = routes.calculateUnicast(true)
|
||||
unicastAll = routes.calculateUnicast(false)
|
||||
broadcast = routes.calculateBroadcast(true)
|
||||
broadcastAll = routes.calculateBroadcast(false)
|
||||
)
|
||||
routes.ourself.RUnlock()
|
||||
routes.peers.RUnlock()
|
||||
|
||||
routes.Lock()
|
||||
routes.unicast = unicast
|
||||
routes.unicastAll = unicastAll
|
||||
routes.broadcast = broadcast
|
||||
routes.broadcastAll = broadcastAll
|
||||
onChange := routes.onChange
|
||||
routes.Unlock()
|
||||
|
||||
if !unicast.equals(oldUnicast) || !broadcast.equals(oldBroadcast) {
|
||||
for _, callback := range onChange {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate all the routes for the question: if *we* want to send a
|
||||
@@ -189,7 +187,7 @@ func (routes *Routes) calculate() {
|
||||
// any knowledge of the MAC address at all. Thus there's no need
|
||||
// to exchange knowledge of MAC addresses, nor any constraints on
|
||||
// the routes that we construct.
|
||||
func (routes *Routes) calculateUnicast(establishedAndSymmetric bool) map[PeerName]PeerName {
|
||||
func (routes *Routes) calculateUnicast(establishedAndSymmetric bool) unicastRoutes {
|
||||
_, unicast := routes.ourself.Routes(nil, establishedAndSymmetric)
|
||||
return unicast
|
||||
}
|
||||
@@ -212,31 +210,67 @@ func (routes *Routes) calculateUnicast(establishedAndSymmetric bool) map[PeerNam
|
||||
// Y =/= Z /\ X.Routes(Y) <= X.Routes(Z) =>
|
||||
// X.Routes(Y) u [P | Y.HasSymmetricConnectionTo(P)] <= X.Routes(Z)
|
||||
// where <= is the subset relationship on keys of the returned map.
|
||||
func (routes *Routes) calculateBroadcast(establishedAndSymmetric bool) map[PeerName][]PeerName {
|
||||
broadcast := make(map[PeerName][]PeerName)
|
||||
ourself := routes.ourself
|
||||
ourConnections := ourself.Connections()
|
||||
|
||||
routes.peers.ForEach(func(peer *Peer) {
|
||||
func (routes *Routes) calculateBroadcast(establishedAndSymmetric bool) broadcastRoutes {
|
||||
broadcast := make(broadcastRoutes)
|
||||
for _, peer := range routes.peers.byName {
|
||||
hops := []PeerName{}
|
||||
if found, reached := peer.Routes(ourself, establishedAndSymmetric); found {
|
||||
// This is rather similar to the inner loop on
|
||||
// peer.Routes(...); the main difference is in the
|
||||
// locking.
|
||||
for conn := range ourConnections {
|
||||
if establishedAndSymmetric && !conn.Established() {
|
||||
continue
|
||||
}
|
||||
remoteName := conn.Remote().Name
|
||||
if _, found := reached[remoteName]; found {
|
||||
continue
|
||||
}
|
||||
if remoteConn, found := conn.Remote().ConnectionTo(ourself.Name); !establishedAndSymmetric || (found && remoteConn.Established()) {
|
||||
hops = append(hops, remoteName)
|
||||
}
|
||||
}
|
||||
if found, reached := peer.Routes(routes.ourself.Peer, establishedAndSymmetric); found {
|
||||
routes.ourself.ForEachConnectedPeer(establishedAndSymmetric, reached,
|
||||
func(remotePeer *Peer) { hops = append(hops, remotePeer.Name) })
|
||||
}
|
||||
broadcast[peer.Name] = hops
|
||||
})
|
||||
}
|
||||
return broadcast
|
||||
}
|
||||
|
||||
func (a unicastRoutes) equals(b unicastRoutes) bool {
|
||||
for key, aval := range a {
|
||||
if bval, ok := b[key]; !ok || bval != aval {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
for key := range b {
|
||||
if _, ok := a[key]; !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (a broadcastRoutes) equals(b broadcastRoutes) bool {
|
||||
set := make(map[PeerName]struct{})
|
||||
|
||||
for key, aval := range a {
|
||||
bval, ok := b[key]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, peer := range aval {
|
||||
set[peer] = struct{}{}
|
||||
}
|
||||
|
||||
for _, peer := range bval {
|
||||
if _, ok := set[peer]; !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
delete(set, peer)
|
||||
}
|
||||
|
||||
if len(set) != 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
for key := range b {
|
||||
if _, ok := a[key]; !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
+211
@@ -0,0 +1,211 @@
|
||||
package mesh
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
)
|
||||
|
||||
type Status struct {
|
||||
Protocol string
|
||||
ProtocolMinVersion int
|
||||
ProtocolMaxVersion int
|
||||
Encryption bool
|
||||
PeerDiscovery bool
|
||||
Name string
|
||||
NickName string
|
||||
Port int
|
||||
Peers []PeerStatus
|
||||
UnicastRoutes []UnicastRouteStatus
|
||||
BroadcastRoutes []BroadcastRouteStatus
|
||||
Connections []LocalConnectionStatus
|
||||
Targets []string
|
||||
OverlayDiagnostics interface{}
|
||||
TrustedSubnets []string
|
||||
}
|
||||
|
||||
type PeerStatus struct {
|
||||
Name string
|
||||
NickName string
|
||||
UID PeerUID
|
||||
ShortID PeerShortID
|
||||
Version uint64
|
||||
Connections []ConnectionStatus
|
||||
}
|
||||
|
||||
type ConnectionStatus struct {
|
||||
Name string
|
||||
NickName string
|
||||
Address string
|
||||
Outbound bool
|
||||
Established bool
|
||||
}
|
||||
|
||||
type UnicastRouteStatus struct {
|
||||
Dest, Via string
|
||||
}
|
||||
|
||||
type BroadcastRouteStatus struct {
|
||||
Source string
|
||||
Via []string
|
||||
}
|
||||
|
||||
type LocalConnectionStatus struct {
|
||||
Address string
|
||||
Outbound bool
|
||||
State string
|
||||
Info string
|
||||
}
|
||||
|
||||
func NewStatus(router *Router) *Status {
|
||||
return &Status{
|
||||
Protocol,
|
||||
ProtocolMinVersion,
|
||||
ProtocolMaxVersion,
|
||||
router.UsingPassword(),
|
||||
router.PeerDiscovery,
|
||||
router.Ourself.Name.String(),
|
||||
router.Ourself.NickName,
|
||||
router.Port,
|
||||
NewPeerStatusSlice(router.Peers),
|
||||
NewUnicastRouteStatusSlice(router.Routes),
|
||||
NewBroadcastRouteStatusSlice(router.Routes),
|
||||
NewLocalConnectionStatusSlice(router.ConnectionMaker),
|
||||
NewTargetSlice(router.ConnectionMaker),
|
||||
router.Overlay.Diagnostics(),
|
||||
NewTrustedSubnetsSlice(router.TrustedSubnets)}
|
||||
}
|
||||
|
||||
func NewPeerStatusSlice(peers *Peers) []PeerStatus {
|
||||
var slice []PeerStatus
|
||||
|
||||
peers.ForEach(func(peer *Peer) {
|
||||
var connections []ConnectionStatus
|
||||
if peer == peers.ourself.Peer {
|
||||
for conn := range peers.ourself.Connections() {
|
||||
connections = append(connections, newConnectionStatus(conn))
|
||||
}
|
||||
} else {
|
||||
// Modifying peer.connections requires a write lock on
|
||||
// Peers, and since we are holding a read lock (due to the
|
||||
// ForEach), access without locking the peer is safe.
|
||||
for _, conn := range peer.connections {
|
||||
connections = append(connections, newConnectionStatus(conn))
|
||||
}
|
||||
}
|
||||
slice = append(slice, PeerStatus{
|
||||
peer.Name.String(),
|
||||
peer.NickName,
|
||||
peer.UID,
|
||||
peer.ShortID,
|
||||
peer.Version,
|
||||
connections})
|
||||
})
|
||||
|
||||
return slice
|
||||
}
|
||||
|
||||
func newConnectionStatus(c Connection) ConnectionStatus {
|
||||
return ConnectionStatus{
|
||||
c.Remote().Name.String(),
|
||||
c.Remote().NickName,
|
||||
c.RemoteTCPAddr(),
|
||||
c.Outbound(),
|
||||
c.Established()}
|
||||
}
|
||||
|
||||
func NewUnicastRouteStatusSlice(routes *Routes) []UnicastRouteStatus {
|
||||
routes.RLock()
|
||||
defer routes.RUnlock()
|
||||
|
||||
var slice []UnicastRouteStatus
|
||||
for dest, via := range routes.unicast {
|
||||
slice = append(slice, UnicastRouteStatus{dest.String(), via.String()})
|
||||
}
|
||||
return slice
|
||||
}
|
||||
|
||||
func NewBroadcastRouteStatusSlice(routes *Routes) []BroadcastRouteStatus {
|
||||
routes.RLock()
|
||||
defer routes.RUnlock()
|
||||
|
||||
var slice []BroadcastRouteStatus
|
||||
for source, via := range routes.broadcast {
|
||||
var hops []string
|
||||
for _, hop := range via {
|
||||
hops = append(hops, hop.String())
|
||||
}
|
||||
slice = append(slice, BroadcastRouteStatus{source.String(), hops})
|
||||
}
|
||||
return slice
|
||||
}
|
||||
|
||||
func NewLocalConnectionStatusSlice(cm *ConnectionMaker) []LocalConnectionStatus {
|
||||
resultChan := make(chan []LocalConnectionStatus, 0)
|
||||
cm.actionChan <- func() bool {
|
||||
var slice []LocalConnectionStatus
|
||||
for conn := range cm.connections {
|
||||
state := "pending"
|
||||
if conn.Established() {
|
||||
state = "established"
|
||||
}
|
||||
lc, _ := conn.(*LocalConnection)
|
||||
info := fmt.Sprintf("%-6v %v", lc.OverlayConn.DisplayName(), conn.Remote())
|
||||
if lc.Router.UsingPassword() {
|
||||
if lc.Untrusted() {
|
||||
info = fmt.Sprintf("%-11v %v", "encrypted", info)
|
||||
} else {
|
||||
info = fmt.Sprintf("%-11v %v", "unencrypted", info)
|
||||
}
|
||||
}
|
||||
slice = append(slice, LocalConnectionStatus{conn.RemoteTCPAddr(), conn.Outbound(), state, info})
|
||||
}
|
||||
for address, target := range cm.targets {
|
||||
add := func(state, info string) {
|
||||
slice = append(slice, LocalConnectionStatus{address, true, state, info})
|
||||
}
|
||||
switch target.state {
|
||||
case TargetWaiting:
|
||||
until := "never"
|
||||
if !target.tryAfter.IsZero() {
|
||||
until = target.tryAfter.String()
|
||||
}
|
||||
if target.lastError == nil { // shouldn't happen
|
||||
add("waiting", "until: "+until)
|
||||
} else {
|
||||
add("failed", target.lastError.Error()+", retry: "+until)
|
||||
}
|
||||
case TargetAttempting:
|
||||
if target.lastError == nil {
|
||||
add("connecting", "")
|
||||
} else {
|
||||
add("retrying", target.lastError.Error())
|
||||
}
|
||||
case TargetConnected:
|
||||
}
|
||||
}
|
||||
resultChan <- slice
|
||||
return false
|
||||
}
|
||||
return <-resultChan
|
||||
}
|
||||
|
||||
func NewTargetSlice(cm *ConnectionMaker) []string {
|
||||
resultChan := make(chan []string, 0)
|
||||
cm.actionChan <- func() bool {
|
||||
var slice []string
|
||||
for peer := range cm.directPeers {
|
||||
slice = append(slice, peer)
|
||||
}
|
||||
resultChan <- slice
|
||||
return false
|
||||
}
|
||||
return <-resultChan
|
||||
}
|
||||
|
||||
func NewTrustedSubnetsSlice(trustedSubnets []*net.IPNet) []string {
|
||||
trustedSubnetStrs := []string{}
|
||||
for _, trustedSubnet := range trustedSubnets {
|
||||
trustedSubnetStrs = append(trustedSubnetStrs, trustedSubnet.String())
|
||||
}
|
||||
return trustedSubnetStrs
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package mesh
|
||||
|
||||
type SurrogateGossipData struct {
|
||||
messages [][]byte
|
||||
}
|
||||
|
||||
func NewSurrogateGossipData(msg []byte) *SurrogateGossipData {
|
||||
return &SurrogateGossipData{messages: [][]byte{msg}}
|
||||
}
|
||||
|
||||
func (d *SurrogateGossipData) Encode() [][]byte {
|
||||
return d.messages
|
||||
}
|
||||
|
||||
func (d *SurrogateGossipData) Merge(other GossipData) GossipData {
|
||||
o := other.(*SurrogateGossipData)
|
||||
messages := make([][]byte, 0, len(d.messages)+len(o.messages))
|
||||
messages = append(messages, d.messages...)
|
||||
messages = append(messages, o.messages...)
|
||||
return &SurrogateGossipData{messages: messages}
|
||||
}
|
||||
|
||||
// SurrogateGossiper ignores unicasts and relays broadcasts and gossips.
|
||||
|
||||
type SurrogateGossiper struct{}
|
||||
|
||||
func (*SurrogateGossiper) OnGossipUnicast(sender PeerName, msg []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*SurrogateGossiper) OnGossipBroadcast(_ PeerName, update []byte) (GossipData, error) {
|
||||
return NewSurrogateGossipData(update), nil
|
||||
}
|
||||
|
||||
func (*SurrogateGossiper) Gossip() GossipData {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*SurrogateGossiper) OnGossip(update []byte) (GossipData, error) {
|
||||
return NewSurrogateGossipData(update), nil
|
||||
}
|
||||
|
||||
var (
|
||||
surrogateGossiper SurrogateGossiper
|
||||
)
|
||||
@@ -0,0 +1,42 @@
|
||||
package mesh
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type TokenBucket struct {
|
||||
capacity int64 // Maximum capacity of bucket
|
||||
tokenInterval time.Duration // Token replenishment rate
|
||||
refillDuration time.Duration // Time to refill from empty
|
||||
earliestUnspentToken time.Time
|
||||
}
|
||||
|
||||
func NewTokenBucket(capacity int64, tokenInterval time.Duration) *TokenBucket {
|
||||
tb := TokenBucket{
|
||||
capacity: capacity,
|
||||
tokenInterval: tokenInterval,
|
||||
refillDuration: tokenInterval * time.Duration(capacity)}
|
||||
|
||||
tb.earliestUnspentToken = tb.capacityToken()
|
||||
|
||||
return &tb
|
||||
}
|
||||
|
||||
func (tb *TokenBucket) Wait() {
|
||||
// If earliest unspent token is in the future, sleep until then
|
||||
time.Sleep(tb.earliestUnspentToken.Sub(time.Now()))
|
||||
|
||||
// Alternatively, enforce bucket capacity if necessary
|
||||
capacityToken := tb.capacityToken()
|
||||
if tb.earliestUnspentToken.Before(capacityToken) {
|
||||
tb.earliestUnspentToken = capacityToken
|
||||
}
|
||||
|
||||
// 'Remove' a token from the bucket
|
||||
tb.earliestUnspentToken = tb.earliestUnspentToken.Add(tb.tokenInterval)
|
||||
}
|
||||
|
||||
// Determine the historic token timestamp representing a full bucket
|
||||
func (tb *TokenBucket) capacityToken() time.Time {
|
||||
return time.Now().Add(-tb.refillDuration).Truncate(tb.tokenInterval)
|
||||
}
|
||||
@@ -1,323 +0,0 @@
|
||||
package nameserver
|
||||
|
||||
import (
|
||||
"container/heap"
|
||||
"errors"
|
||||
"github.com/miekg/dns"
|
||||
. "github.com/weaveworks/weave/common"
|
||||
"math"
|
||||
"math/rand"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
errInvalidCapacity = errors.New("Invalid cache capacity")
|
||||
errCouldNotResolve = errors.New("Could not resolve")
|
||||
errTimeout = errors.New("Timeout while waiting for resolution")
|
||||
errNoLocalReplies = errors.New("No local replies")
|
||||
)
|
||||
|
||||
const (
|
||||
defPendingTimeout int = 5 // timeout for a resolution
|
||||
)
|
||||
|
||||
const nullTTL = 0 // a null TTL
|
||||
|
||||
type entryStatus uint8
|
||||
|
||||
const (
|
||||
stPending entryStatus = iota // someone is waiting for the resolution
|
||||
stResolved entryStatus = iota // resolved
|
||||
)
|
||||
|
||||
const (
|
||||
CacheNoLocalReplies uint8 = 1 << iota // not found in local network (stored in the cache so we skip another local lookup or some time)
|
||||
)
|
||||
|
||||
// shuffleAnswers reorders answers for very basic load balancing
|
||||
func shuffleAnswers(answers []dns.RR) []dns.RR {
|
||||
n := len(answers)
|
||||
if n > 1 {
|
||||
rand.Seed(time.Now().UTC().UnixNano())
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
r := i + rand.Intn(n-i)
|
||||
answers[r], answers[i] = answers[i], answers[r]
|
||||
}
|
||||
}
|
||||
|
||||
return answers
|
||||
}
|
||||
|
||||
// a cache entry
|
||||
type cacheEntry struct {
|
||||
Status entryStatus // status of the entry
|
||||
Flags uint8 // some extra flags
|
||||
ReplyLen int
|
||||
|
||||
question dns.Question
|
||||
protocol dnsProtocol
|
||||
reply dns.Msg
|
||||
|
||||
validUntil time.Time // obtained from the reply and stored here for convenience/speed
|
||||
putTime time.Time
|
||||
|
||||
index int // for fast lookups in the heap
|
||||
}
|
||||
|
||||
func newCacheEntry(question *dns.Question, now time.Time) *cacheEntry {
|
||||
e := &cacheEntry{
|
||||
Status: stPending,
|
||||
validUntil: now.Add(time.Second * time.Duration(defPendingTimeout)),
|
||||
question: *question,
|
||||
index: -1,
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
// Get a copy of the reply stored in the entry, but with some values adjusted like the TTL
|
||||
func (e *cacheEntry) getReply(request *dns.Msg, maxLen int, now time.Time) (*dns.Msg, error) {
|
||||
if e.Status != stResolved {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// if the reply has expired or is invalid, force the caller to start a new resolution
|
||||
if e.hasExpired(now) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if e.Flags&CacheNoLocalReplies != 0 {
|
||||
return nil, errNoLocalReplies
|
||||
}
|
||||
|
||||
if e.ReplyLen >= maxLen {
|
||||
Debug.Printf("[cache msgid %d] returning truncated reponse: %d > %d", request.MsgHdr.Id, e.ReplyLen, maxLen)
|
||||
return makeTruncatedReply(request), nil
|
||||
}
|
||||
|
||||
// create a copy of the reply, with values for this particular query
|
||||
reply := e.reply.Copy()
|
||||
reply.SetReply(request)
|
||||
|
||||
// adjust the TTLs
|
||||
passedSecs := uint32(now.Sub(e.putTime).Seconds())
|
||||
for _, rr := range reply.Answer {
|
||||
hdr := rr.Header()
|
||||
ttl := hdr.Ttl
|
||||
if passedSecs < ttl {
|
||||
hdr.Ttl = ttl - passedSecs
|
||||
} else {
|
||||
return nil, nil // it is expired: do not spend more time and return nil...
|
||||
}
|
||||
}
|
||||
|
||||
reply.Rcode = e.reply.Rcode
|
||||
reply.Authoritative = true
|
||||
|
||||
// shuffle the values, etc...
|
||||
reply.Answer = shuffleAnswers(reply.Answer)
|
||||
|
||||
return reply, nil
|
||||
}
|
||||
|
||||
func (e cacheEntry) hasExpired(now time.Time) bool {
|
||||
return e.validUntil.Before(now) || e.validUntil == now
|
||||
}
|
||||
|
||||
// set the reply for the entry
|
||||
// returns True if the entry has changed the validUntil time
|
||||
func (e *cacheEntry) setReply(reply *dns.Msg, ttl int, flags uint8, now time.Time) bool {
|
||||
var prevValidUntil time.Time
|
||||
if e.Status == stResolved {
|
||||
if reply != nil {
|
||||
Debug.Printf("[cache msgid %d] replacing response in cache", reply.MsgHdr.Id)
|
||||
}
|
||||
prevValidUntil = e.validUntil
|
||||
}
|
||||
|
||||
e.Status = stResolved
|
||||
e.Flags = flags
|
||||
e.putTime = now
|
||||
|
||||
if ttl != nullTTL {
|
||||
e.validUntil = now.Add(time.Second * time.Duration(ttl))
|
||||
} else if reply != nil {
|
||||
// calculate the validUntil from the reply TTL
|
||||
var minTTL uint32 = math.MaxUint32
|
||||
for _, rr := range reply.Answer {
|
||||
ttl := rr.Header().Ttl
|
||||
if ttl < minTTL {
|
||||
minTTL = ttl // TODO: improve the minTTL calculation (maybe we should skip some RRs)
|
||||
}
|
||||
}
|
||||
e.validUntil = now.Add(time.Second * time.Duration(minTTL))
|
||||
}
|
||||
|
||||
if reply != nil {
|
||||
e.reply = *reply
|
||||
e.ReplyLen = reply.Len()
|
||||
}
|
||||
|
||||
return (prevValidUntil != e.validUntil)
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// An entriesPtrHeap is a min-heap of cache entries.
|
||||
type entriesPtrsHeap []*cacheEntry
|
||||
|
||||
func (h entriesPtrsHeap) Len() int { return len(h) }
|
||||
func (h entriesPtrsHeap) Less(i, j int) bool { return h[i].validUntil.Before(h[j].validUntil) }
|
||||
func (h entriesPtrsHeap) Swap(i, j int) {
|
||||
h[i], h[j] = h[j], h[i]
|
||||
h[i].index = i
|
||||
h[j].index = j
|
||||
}
|
||||
|
||||
func (h *entriesPtrsHeap) Push(x interface{}) {
|
||||
// Push and Pop use pointer receivers because they modify the slice's length,
|
||||
// not just its contents.
|
||||
n := len(*h)
|
||||
entry := x.(*cacheEntry)
|
||||
entry.index = n
|
||||
*h = append(*h, entry)
|
||||
}
|
||||
|
||||
func (h *entriesPtrsHeap) Pop() interface{} {
|
||||
old := *h
|
||||
n := len(old)
|
||||
item := old[n-1]
|
||||
item.index = -1 // for safety
|
||||
*h = old[0 : n-1]
|
||||
return item
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
type cacheKey dns.Question
|
||||
type entries map[cacheKey]*cacheEntry
|
||||
|
||||
// Cache is a thread-safe fixed capacity LRU cache.
|
||||
type Cache struct {
|
||||
Capacity int
|
||||
|
||||
entries entries
|
||||
entriesH entriesPtrsHeap // len(entriesH) <= len(entries), as pending entries can be in entries but not in entriesH
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
// NewCache creates a cache of the given capacity
|
||||
func NewCache(capacity int) (*Cache, error) {
|
||||
if capacity <= 0 {
|
||||
return nil, errInvalidCapacity
|
||||
}
|
||||
c := &Cache{
|
||||
Capacity: capacity,
|
||||
entries: make(entries, capacity),
|
||||
}
|
||||
|
||||
heap.Init(&c.entriesH)
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Clear removes all the entries in the cache
|
||||
func (c *Cache) Clear() {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
c.entries = make(entries, c.Capacity)
|
||||
heap.Init(&c.entriesH)
|
||||
}
|
||||
|
||||
// Purge removes the old elements in the cache
|
||||
func (c *Cache) Purge(now time.Time) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
for i, entry := range c.entriesH {
|
||||
if entry.hasExpired(now) {
|
||||
heap.Remove(&c.entriesH, i)
|
||||
delete(c.entries, cacheKey(entry.question))
|
||||
} else {
|
||||
return // all remaining entries must be still valid...
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add adds a reply to the cache.
|
||||
func (c *Cache) Put(request *dns.Msg, reply *dns.Msg, ttl int, flags uint8, now time.Time) int {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
question := request.Question[0]
|
||||
key := cacheKey(question)
|
||||
ent, found := c.entries[key]
|
||||
if found {
|
||||
updated := ent.setReply(reply, ttl, flags, now)
|
||||
if updated {
|
||||
heap.Fix(&c.entriesH, ent.index)
|
||||
}
|
||||
} else {
|
||||
// If we will add a new item and the capacity has been exceeded, make some room...
|
||||
if len(c.entriesH) >= c.Capacity {
|
||||
lowestEntry := heap.Pop(&c.entriesH).(*cacheEntry)
|
||||
delete(c.entries, cacheKey(lowestEntry.question))
|
||||
}
|
||||
ent = newCacheEntry(&question, now)
|
||||
ent.setReply(reply, ttl, flags, now)
|
||||
heap.Push(&c.entriesH, ent)
|
||||
c.entries[key] = ent
|
||||
}
|
||||
return ent.ReplyLen
|
||||
}
|
||||
|
||||
// Look up for a question's reply from the cache.
|
||||
// If no reply is stored in the cache, it returns a `nil` reply and no error. The caller can then `Wait()`
|
||||
// for another goroutine `Put`ing a reply in the cache.
|
||||
func (c *Cache) Get(request *dns.Msg, maxLen int, now time.Time) (reply *dns.Msg, err error) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
question := request.Question[0]
|
||||
key := cacheKey(question)
|
||||
if ent, found := c.entries[key]; found {
|
||||
reply, err = ent.getReply(request, maxLen, now)
|
||||
if ent.hasExpired(now) {
|
||||
Debug.Printf("[cache msgid %d] expired: removing", request.MsgHdr.Id)
|
||||
if ent.index > 0 {
|
||||
heap.Remove(&c.entriesH, ent.index)
|
||||
}
|
||||
delete(c.entries, key)
|
||||
reply = nil
|
||||
}
|
||||
} else {
|
||||
// we are the first asking for this name: create an entry with no reply... the caller must wait
|
||||
Debug.Printf("[cache msgid %d] addind in pending state", request.MsgHdr.Id)
|
||||
c.entries[key] = newCacheEntry(&question, now)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Remove removes the provided question from the cache.
|
||||
func (c *Cache) Remove(question *dns.Question) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
key := cacheKey(*question)
|
||||
if entry, found := c.entries[key]; found {
|
||||
if entry.index > 0 {
|
||||
heap.Remove(&c.entriesH, entry.index)
|
||||
}
|
||||
delete(c.entries, key)
|
||||
}
|
||||
}
|
||||
|
||||
// Len returns the number of entries in the cache.
|
||||
func (c *Cache) Len() int {
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
|
||||
return len(c.entries)
|
||||
}
|
||||
@@ -1,206 +0,0 @@
|
||||
package nameserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/miekg/dns"
|
||||
. "github.com/weaveworks/weave/common"
|
||||
wt "github.com/weaveworks/weave/testing"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Check that the cache keeps its intended capacity constant
|
||||
func TestCacheLength(t *testing.T) {
|
||||
InitDefaultLogging(true)
|
||||
|
||||
const cacheLen = 128
|
||||
|
||||
l, err := NewCache(cacheLen)
|
||||
wt.AssertNoErr(t, err)
|
||||
|
||||
insTime := time.Now()
|
||||
|
||||
t.Logf("Inserting 256 questions in the cache at '%s', with TTL from 0 to 255", insTime)
|
||||
for i := 0; i < cacheLen*2; i++ {
|
||||
questionMsg := new(dns.Msg)
|
||||
questionMsg.SetQuestion(fmt.Sprintf("name%d", i), dns.TypeA)
|
||||
questionMsg.RecursionDesired = true
|
||||
|
||||
question := &questionMsg.Question[0]
|
||||
|
||||
ip := net.ParseIP(fmt.Sprintf("10.0.1.%d", i))
|
||||
ips := []net.IP{ip}
|
||||
reply := makeAddressReply(questionMsg, question, ips)
|
||||
reply.Answer[0].Header().Ttl = uint32(i)
|
||||
|
||||
l.Put(questionMsg, reply, 0, 0, insTime)
|
||||
}
|
||||
|
||||
wt.AssertEqualInt(t, l.Len(), cacheLen, "cache length")
|
||||
|
||||
minExpectedTime := insTime.Add(time.Duration(cacheLen) * time.Second)
|
||||
t.Logf("Checking all remaining entries expire after insert_time + %d secs='%s'", cacheLen, minExpectedTime)
|
||||
for _, entry := range l.entries {
|
||||
if entry.validUntil.Before(minExpectedTime) {
|
||||
t.Fatalf("Entry valid until %s", entry.validUntil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check that the cache entries are ok
|
||||
func TestCacheEntries(t *testing.T) {
|
||||
InitDefaultLogging(true)
|
||||
|
||||
Info.Println("Checking cache consistency")
|
||||
|
||||
const cacheLen = 128
|
||||
|
||||
l, err := NewCache(cacheLen)
|
||||
wt.AssertNoErr(t, err)
|
||||
|
||||
questionMsg := new(dns.Msg)
|
||||
questionMsg.SetQuestion("some.name", dns.TypeA)
|
||||
questionMsg.RecursionDesired = true
|
||||
|
||||
question := &questionMsg.Question[0]
|
||||
|
||||
t.Logf("Trying to get a name")
|
||||
resp, err := l.Get(questionMsg, minUDPSize, time.Now())
|
||||
wt.AssertNoErr(t, err)
|
||||
if resp != nil {
|
||||
t.Logf("Got\n%s", resp)
|
||||
t.Fatalf("ERROR: Did not expect a reponse from Get() yet")
|
||||
}
|
||||
t.Logf("Trying to get it again")
|
||||
resp, err = l.Get(questionMsg, minUDPSize, time.Now())
|
||||
wt.AssertNoErr(t, err)
|
||||
if resp != nil {
|
||||
t.Logf("Got\n%s", resp)
|
||||
t.Fatalf("ERROR: Did not expect a reponse from Get() yet")
|
||||
}
|
||||
|
||||
t.Logf("Inserting the reply")
|
||||
reply1 := makeAddressReply(questionMsg, question, []net.IP{net.ParseIP("10.0.1.1")})
|
||||
l.Put(questionMsg, reply1, nullTTL, 0, time.Now())
|
||||
|
||||
timeGet1 := time.Now()
|
||||
t.Logf("Checking we can Get() the reply now")
|
||||
resp, err = l.Get(questionMsg, minUDPSize, timeGet1)
|
||||
wt.AssertNoErr(t, err)
|
||||
wt.AssertTrue(t, resp != nil, "reponse from Get()")
|
||||
t.Logf("Received '%s'", resp.Answer[0])
|
||||
wt.AssertType(t, resp.Answer[0], (*dns.A)(nil), "DNS record")
|
||||
ttlGet1 := resp.Answer[0].Header().Ttl
|
||||
|
||||
timeGet2 := timeGet1.Add(time.Duration(1) * time.Second)
|
||||
t.Logf("Checking that a second Get(), after 1 second, gets the same result, but with reduced TTL")
|
||||
resp, err = l.Get(questionMsg, minUDPSize, timeGet2)
|
||||
wt.AssertNoErr(t, err)
|
||||
wt.AssertTrue(t, resp != nil, "reponse from a second Get()")
|
||||
t.Logf("Received '%s'", resp.Answer[0])
|
||||
wt.AssertType(t, resp.Answer[0], (*dns.A)(nil), "DNS record")
|
||||
ttlGet2 := resp.Answer[0].Header().Ttl
|
||||
wt.AssertEqualInt(t, int(ttlGet1-ttlGet2), 1, "TTL difference")
|
||||
|
||||
timeGet3 := timeGet1.Add(time.Duration(localTTL) * time.Second)
|
||||
t.Logf("Checking that a third Get(), after %d second, gets no result", localTTL)
|
||||
resp, err = l.Get(questionMsg, minUDPSize, timeGet3)
|
||||
wt.AssertNoErr(t, err)
|
||||
if resp != nil {
|
||||
t.Logf("Got\n%s", resp)
|
||||
t.Fatalf("ERROR: Did NOT expect a reponse from the second Get()")
|
||||
}
|
||||
|
||||
t.Logf("Checking that an Remove() results in Get() returning nothing")
|
||||
replyTemp := makeAddressReply(questionMsg, question, []net.IP{net.ParseIP("10.0.9.9")})
|
||||
l.Put(questionMsg, replyTemp, nullTTL, 0, time.Now())
|
||||
lenBefore := l.Len()
|
||||
l.Remove(question)
|
||||
wt.AssertEqualInt(t, l.Len(), lenBefore-1, "cache length")
|
||||
l.Remove(question) // do it again: should have no effect...
|
||||
wt.AssertEqualInt(t, l.Len(), lenBefore-1, "cache length")
|
||||
|
||||
resp, err = l.Get(questionMsg, minUDPSize, timeGet1)
|
||||
wt.AssertNoErr(t, err)
|
||||
wt.AssertTrue(t, resp == nil, "reponse from the Get() after a Remove()")
|
||||
|
||||
t.Logf("Inserting a two replies for the same query")
|
||||
timePut2 := time.Now()
|
||||
reply2 := makeAddressReply(questionMsg, question, []net.IP{net.ParseIP("10.0.1.2")})
|
||||
l.Put(questionMsg, reply2, nullTTL, 0, timePut2)
|
||||
timePut3 := timePut2.Add(time.Duration(1) * time.Second)
|
||||
reply3 := makeAddressReply(questionMsg, question, []net.IP{net.ParseIP("10.0.1.3")})
|
||||
l.Put(questionMsg, reply3, nullTTL, 0, timePut3)
|
||||
|
||||
t.Logf("Checking we get the last one...")
|
||||
resp, err = l.Get(questionMsg, minUDPSize, timePut3)
|
||||
wt.AssertNoErr(t, err)
|
||||
wt.AssertTrue(t, resp != nil, "reponse from the Get()")
|
||||
t.Logf("Received '%s'", resp.Answer[0])
|
||||
wt.AssertType(t, resp.Answer[0], (*dns.A)(nil), "DNS record")
|
||||
wt.AssertEqualString(t, resp.Answer[0].(*dns.A).A.String(), "10.0.1.3", "IP address")
|
||||
wt.AssertEqualInt(t, int(resp.Answer[0].Header().Ttl), int(localTTL), "TTL")
|
||||
|
||||
resp, err = l.Get(questionMsg, minUDPSize, timePut3.Add(time.Duration(localTTL-1)*time.Second))
|
||||
wt.AssertNoErr(t, err)
|
||||
wt.AssertTrue(t, resp != nil, "reponse from the Get()")
|
||||
t.Logf("Received '%s'", resp.Answer[0])
|
||||
wt.AssertType(t, resp.Answer[0], (*dns.A)(nil), "DNS record")
|
||||
wt.AssertEqualString(t, resp.Answer[0].(*dns.A).A.String(), "10.0.1.3", "IP address")
|
||||
wt.AssertEqualInt(t, int(resp.Answer[0].Header().Ttl), 1, "TTL")
|
||||
|
||||
t.Logf("Checking we get empty replies when they are expired...")
|
||||
lenBefore = l.Len()
|
||||
resp, err = l.Get(questionMsg, minUDPSize, timePut3.Add(time.Duration(localTTL)*time.Second))
|
||||
wt.AssertNoErr(t, err)
|
||||
if resp != nil {
|
||||
t.Logf("Got\n%s", resp.Answer[0])
|
||||
t.Fatalf("ERROR: Did NOT expect a reponse from the Get()")
|
||||
}
|
||||
wt.AssertEqualInt(t, l.Len(), lenBefore-1, "cache length (after getting an expired entry)")
|
||||
|
||||
questionMsg2 := new(dns.Msg)
|
||||
questionMsg2.SetQuestion("some.other.name", dns.TypeA)
|
||||
questionMsg2.RecursionDesired = true
|
||||
question2 := &questionMsg2.Question[0]
|
||||
|
||||
t.Logf("Trying to Get() a name")
|
||||
resp, err = l.Get(questionMsg2, minUDPSize, time.Now())
|
||||
wt.AssertNoErr(t, err)
|
||||
wt.AssertNil(t, resp, "reponse from Get() yet")
|
||||
|
||||
t.Logf("Checking that an Remove() between Get() and Put() does not break things")
|
||||
replyTemp2 := makeAddressReply(questionMsg2, question2, []net.IP{net.ParseIP("10.0.9.9")})
|
||||
l.Remove(question2)
|
||||
l.Put(questionMsg2, replyTemp2, nullTTL, 0, time.Now())
|
||||
resp, err = l.Get(questionMsg2, minUDPSize, time.Now())
|
||||
wt.AssertNoErr(t, err)
|
||||
wt.AssertNotNil(t, resp, "reponse from Get()")
|
||||
|
||||
questionMsg3 := new(dns.Msg)
|
||||
questionMsg3.SetQuestion("some.other.name", dns.TypeA)
|
||||
questionMsg3.RecursionDesired = true
|
||||
question3 := &questionMsg3.Question[0]
|
||||
|
||||
t.Logf("Checking that a entry with CacheNoLocalReplies return an error")
|
||||
timePut3 = time.Now()
|
||||
l.Put(questionMsg3, nil, nullTTL, CacheNoLocalReplies, timePut3)
|
||||
resp, err = l.Get(questionMsg3, minUDPSize, timePut3)
|
||||
wt.AssertNil(t, resp, "Get() response with CacheNoLocalReplies")
|
||||
wt.AssertNotNil(t, err, "Get() error with CacheNoLocalReplies")
|
||||
|
||||
timeExpiredGet3 := timePut3.Add(time.Second * time.Duration(negLocalTTL+1))
|
||||
t.Logf("Checking that we get an expired response after %f seconds", timeExpiredGet3.Sub(timePut3).Seconds())
|
||||
resp, err = l.Get(questionMsg3, minUDPSize, timeExpiredGet3)
|
||||
wt.AssertNil(t, resp, "expired Get() response with CacheNoLocalReplies")
|
||||
wt.AssertNil(t, err, "expired Get() error with CacheNoLocalReplies")
|
||||
|
||||
l.Remove(question3)
|
||||
t.Logf("Checking that Put&Get with CacheNoLocalReplies with a Remove in the middle returns nothing")
|
||||
l.Put(questionMsg3, nil, nullTTL, CacheNoLocalReplies, time.Now())
|
||||
l.Remove(question3)
|
||||
resp, err = l.Get(questionMsg3, minUDPSize, time.Now())
|
||||
wt.AssertNil(t, resp, "Get() reponse with CacheNoLocalReplies")
|
||||
wt.AssertNil(t, err, "Get() error with CacheNoLocalReplies")
|
||||
}
|
||||
+271
-65
@@ -1,92 +1,298 @@
|
||||
package nameserver
|
||||
|
||||
import (
|
||||
"github.com/miekg/dns"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
|
||||
"github.com/weaveworks/weave/net/address"
|
||||
)
|
||||
|
||||
const (
|
||||
localTTL uint32 = 30 // somewhat arbitrary; we don't expect anyone downstream to cache results
|
||||
negLocalTTL = 30 // TTL for negative local resolutions
|
||||
minUDPSize = 512
|
||||
maxUDPSize = 65535
|
||||
topDomain = "."
|
||||
reverseDNSdomain = "in-addr.arpa."
|
||||
etcResolvConf = "/etc/resolv.conf"
|
||||
udpBuffSize = uint16(4096)
|
||||
minUDPSize = 512
|
||||
|
||||
DefaultListenAddress = "0.0.0.0:53"
|
||||
DefaultTTL = 1
|
||||
DefaultClientTimeout = 5 * time.Second
|
||||
)
|
||||
|
||||
func makeHeader(r *dns.Msg, q *dns.Question) *dns.RR_Header {
|
||||
return &dns.RR_Header{
|
||||
Name: q.Name, Rrtype: q.Qtype,
|
||||
Class: dns.ClassINET, Ttl: localTTL}
|
||||
type DNSServer struct {
|
||||
ns *Nameserver
|
||||
domain string
|
||||
ttl uint32
|
||||
address string
|
||||
|
||||
servers []*dns.Server
|
||||
upstream *dns.ClientConfig
|
||||
tcpClient *dns.Client
|
||||
udpClient *dns.Client
|
||||
}
|
||||
|
||||
func makeReply(r *dns.Msg, as []dns.RR) *dns.Msg {
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(r)
|
||||
m.RecursionAvailable = true
|
||||
m.Answer = as
|
||||
return m
|
||||
func filter(ss []string, s string) []string {
|
||||
for i := 0; i < len(ss); {
|
||||
if ss[i] == s {
|
||||
ss = append(ss[:i], ss[i+1:]...)
|
||||
continue
|
||||
}
|
||||
i++
|
||||
}
|
||||
return ss
|
||||
}
|
||||
|
||||
func makeTruncatedReply(r *dns.Msg) *dns.Msg {
|
||||
// for truncated response, we create a minimal reply with the Truncated bit set
|
||||
reply := new(dns.Msg)
|
||||
reply.SetReply(r)
|
||||
reply.Truncated = true
|
||||
return reply
|
||||
func NewDNSServer(ns *Nameserver, domain, address, effectiveAddress string, ttl uint32, clientTimeout time.Duration) (*DNSServer, error) {
|
||||
s := &DNSServer{
|
||||
ns: ns,
|
||||
domain: dns.Fqdn(domain),
|
||||
ttl: ttl,
|
||||
address: address,
|
||||
tcpClient: &dns.Client{Net: "tcp", ReadTimeout: clientTimeout},
|
||||
udpClient: &dns.Client{Net: "udp", ReadTimeout: clientTimeout, UDPSize: udpBuffSize},
|
||||
}
|
||||
var err error
|
||||
if s.upstream, err = dns.ClientConfigFromFile(etcResolvConf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if s.upstream != nil {
|
||||
s.upstream.Servers = filter(s.upstream.Servers, effectiveAddress)
|
||||
}
|
||||
|
||||
err = s.listen(address)
|
||||
return s, err
|
||||
}
|
||||
|
||||
func makeAddressReply(r *dns.Msg, q *dns.Question, addrs []net.IP) *dns.Msg {
|
||||
answers := make([]dns.RR, len(addrs))
|
||||
header := makeHeader(r, q)
|
||||
count := 0
|
||||
for _, addr := range addrs {
|
||||
switch q.Qtype {
|
||||
case dns.TypeA:
|
||||
if ip4 := addr.To4(); ip4 != nil {
|
||||
answers[count] = &dns.A{Hdr: *header, A: addr}
|
||||
count++
|
||||
}
|
||||
case dns.TypeAAAA:
|
||||
if ip4 := addr.To4(); ip4 == nil {
|
||||
answers[count] = &dns.AAAA{Hdr: *header, AAAA: addr}
|
||||
count++
|
||||
}
|
||||
func (d *DNSServer) String() string {
|
||||
var buf bytes.Buffer
|
||||
fmt.Fprintf(&buf, "WeaveDNS (%s)\n", d.ns.ourName)
|
||||
fmt.Fprintf(&buf, " listening on %s, for domain %s\n", d.address, d.domain)
|
||||
fmt.Fprintf(&buf, " response ttl %d\n", d.ttl)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func (d *DNSServer) listen(address string) error {
|
||||
udpListener, err := net.ListenPacket("udp", address)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
udpServer := &dns.Server{PacketConn: udpListener, Handler: d.createMux(d.udpClient, minUDPSize)}
|
||||
|
||||
tcpListener, err := net.Listen("tcp", address)
|
||||
if err != nil {
|
||||
udpServer.Shutdown()
|
||||
return err
|
||||
}
|
||||
tcpServer := &dns.Server{Listener: tcpListener, Handler: d.createMux(d.tcpClient, -1)}
|
||||
|
||||
d.servers = []*dns.Server{udpServer, tcpServer}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DNSServer) ActivateAndServe() {
|
||||
for _, server := range d.servers {
|
||||
go func(server *dns.Server) {
|
||||
server.ActivateAndServe()
|
||||
}(server)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DNSServer) Stop() error {
|
||||
for _, server := range d.servers {
|
||||
if err := server.Shutdown(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return makeReply(r, answers[:count])
|
||||
return nil
|
||||
}
|
||||
|
||||
func makePTRReply(r *dns.Msg, q *dns.Question, names []string) *dns.Msg {
|
||||
answers := make([]dns.RR, len(names))
|
||||
header := makeHeader(r, q)
|
||||
for i, name := range names {
|
||||
answers[i] = &dns.PTR{Hdr: *header, Ptr: name}
|
||||
type handler struct {
|
||||
*DNSServer
|
||||
maxResponseSize int
|
||||
client *dns.Client
|
||||
}
|
||||
|
||||
func (d *DNSServer) createMux(client *dns.Client, defaultMaxResponseSize int) *dns.ServeMux {
|
||||
m := dns.NewServeMux()
|
||||
h := &handler{
|
||||
DNSServer: d,
|
||||
maxResponseSize: defaultMaxResponseSize,
|
||||
client: client,
|
||||
}
|
||||
return makeReply(r, answers)
|
||||
}
|
||||
|
||||
func makeDNSFailResponse(r *dns.Msg) *dns.Msg {
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(r)
|
||||
m.RecursionAvailable = true
|
||||
m.Rcode = dns.RcodeNameError
|
||||
m.HandleFunc(d.domain, h.handleLocal)
|
||||
m.HandleFunc(reverseDNSdomain, h.handleReverse)
|
||||
m.HandleFunc(topDomain, h.handleRecursive)
|
||||
return m
|
||||
}
|
||||
|
||||
func makeDNSNotImplResponse(r *dns.Msg) *dns.Msg {
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(r)
|
||||
m.RecursionAvailable = true
|
||||
m.Rcode = dns.RcodeNotImplemented
|
||||
return m
|
||||
func (h *handler) handleLocal(w dns.ResponseWriter, req *dns.Msg) {
|
||||
h.ns.debugf("local request: %+v", *req)
|
||||
if len(req.Question) != 1 || req.Question[0].Qtype != dns.TypeA {
|
||||
h.nameError(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
hostname := dns.Fqdn(req.Question[0].Name)
|
||||
if strings.Count(hostname, ".") == 1 {
|
||||
hostname = hostname + h.domain
|
||||
}
|
||||
|
||||
addrs := h.ns.Lookup(hostname)
|
||||
if len(addrs) == 0 {
|
||||
h.nameError(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
header := dns.RR_Header{
|
||||
Name: req.Question[0].Name,
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: h.ttl,
|
||||
}
|
||||
answers := make([]dns.RR, len(addrs))
|
||||
for i, addr := range addrs {
|
||||
ip := addr.IP4()
|
||||
answers[i] = &dns.A{Hdr: header, A: ip}
|
||||
}
|
||||
shuffleAnswers(&answers)
|
||||
|
||||
h.respond(w, h.makeResponse(req, answers))
|
||||
}
|
||||
|
||||
// get the maximum UDP-reply length
|
||||
func getMaxReplyLen(r *dns.Msg, proto dnsProtocol) int {
|
||||
maxLen := minUDPSize
|
||||
if proto == protTCP {
|
||||
maxLen = maxUDPSize
|
||||
} else if opt := r.IsEdns0(); opt != nil {
|
||||
maxLen = int(opt.UDPSize())
|
||||
func (h *handler) handleReverse(w dns.ResponseWriter, req *dns.Msg) {
|
||||
h.ns.debugf("reverse request: %+v", *req)
|
||||
if len(req.Question) != 1 || req.Question[0].Qtype != dns.TypePTR {
|
||||
h.nameError(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
ipStr := strings.TrimSuffix(req.Question[0].Name, "."+reverseDNSdomain)
|
||||
ip, err := address.ParseIP(ipStr)
|
||||
if err != nil {
|
||||
h.nameError(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
hostname, err := h.ns.ReverseLookup(ip.Reverse())
|
||||
if err != nil {
|
||||
h.handleRecursive(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
header := dns.RR_Header{
|
||||
Name: req.Question[0].Name,
|
||||
Rrtype: dns.TypePTR,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: h.ttl,
|
||||
}
|
||||
answers := []dns.RR{&dns.PTR{
|
||||
Hdr: header,
|
||||
Ptr: hostname,
|
||||
}}
|
||||
|
||||
h.respond(w, h.makeResponse(req, answers))
|
||||
}
|
||||
|
||||
func (h *handler) handleRecursive(w dns.ResponseWriter, req *dns.Msg) {
|
||||
h.ns.debugf("recursive request: %+v", *req)
|
||||
|
||||
// Resolve unqualified names locally
|
||||
if len(req.Question) == 1 && req.Question[0].Qtype == dns.TypeA {
|
||||
hostname := dns.Fqdn(req.Question[0].Name)
|
||||
if strings.Count(hostname, ".") == 1 {
|
||||
h.handleLocal(w, req)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for _, server := range h.upstream.Servers {
|
||||
reqCopy := req.Copy()
|
||||
reqCopy.Id = dns.Id()
|
||||
response, _, err := h.client.Exchange(reqCopy, fmt.Sprintf("%s:%s", server, h.upstream.Port))
|
||||
if (err != nil && err != dns.ErrTruncated) || response == nil {
|
||||
h.ns.debugf("error trying %s: %v", server, err)
|
||||
continue
|
||||
}
|
||||
response.Id = req.Id
|
||||
if h.responseTooBig(req, response) {
|
||||
response.Compress = true
|
||||
}
|
||||
h.respond(w, response)
|
||||
return
|
||||
}
|
||||
|
||||
h.respond(w, h.makeErrorResponse(req, dns.RcodeServerFailure))
|
||||
}
|
||||
|
||||
func (h *handler) makeResponse(req *dns.Msg, answers []dns.RR) *dns.Msg {
|
||||
response := &dns.Msg{}
|
||||
response.SetReply(req)
|
||||
response.RecursionAvailable = true
|
||||
response.Authoritative = true
|
||||
response.Answer = answers
|
||||
if !h.responseTooBig(req, response) {
|
||||
return response
|
||||
}
|
||||
|
||||
// search for smallest i that is too big
|
||||
maxSize := h.getMaxResponseSize(req)
|
||||
i := sort.Search(len(answers), func(i int) bool {
|
||||
// return true if too big
|
||||
response.Answer = answers[:i+1]
|
||||
return response.Len() > maxSize
|
||||
})
|
||||
|
||||
response.Answer = answers[:i]
|
||||
if i < len(answers) {
|
||||
response.Truncated = true
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
func (h *handler) makeErrorResponse(req *dns.Msg, code int) *dns.Msg {
|
||||
response := &dns.Msg{}
|
||||
response.SetReply(req)
|
||||
response.RecursionAvailable = true
|
||||
response.Rcode = code
|
||||
return response
|
||||
}
|
||||
|
||||
func (h *handler) responseTooBig(req, response *dns.Msg) bool {
|
||||
return len(response.Answer) > 1 && h.maxResponseSize > 0 && response.Len() > h.getMaxResponseSize(req)
|
||||
}
|
||||
|
||||
func (h *handler) respond(w dns.ResponseWriter, response *dns.Msg) {
|
||||
h.ns.debugf("response: %+v", response)
|
||||
if err := w.WriteMsg(response); err != nil {
|
||||
h.ns.infof("error responding: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) nameError(w dns.ResponseWriter, req *dns.Msg) {
|
||||
h.respond(w, h.makeErrorResponse(req, dns.RcodeNameError))
|
||||
}
|
||||
|
||||
func (h *handler) getMaxResponseSize(req *dns.Msg) int {
|
||||
if opt := req.IsEdns0(); opt != nil {
|
||||
return int(opt.UDPSize())
|
||||
}
|
||||
return h.maxResponseSize
|
||||
}
|
||||
|
||||
func shuffleAnswers(answers *[]dns.RR) {
|
||||
if len(*answers) <= 1 {
|
||||
return
|
||||
}
|
||||
|
||||
for i := range *answers {
|
||||
j := rand.Intn(i + 1)
|
||||
(*answers)[i], (*answers)[j] = (*answers)[j], (*answers)[i]
|
||||
}
|
||||
return maxLen
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package nameserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/weaveworks/weave/mesh"
|
||||
"github.com/weaveworks/weave/net/address"
|
||||
)
|
||||
|
||||
func startServer(t *testing.T, upstream *dns.ClientConfig) (*DNSServer, *Nameserver, int, int) {
|
||||
peername, err := mesh.PeerNameFromString("00:00:00:02:00:00")
|
||||
require.Nil(t, err)
|
||||
nameserver := New(peername, "", func(mesh.PeerName) bool { return true })
|
||||
dnsserver, err := NewDNSServer(nameserver, "weave.local.", "0.0.0.0:0", "", 30, 5*time.Second)
|
||||
require.Nil(t, err)
|
||||
udpPort := dnsserver.servers[0].PacketConn.LocalAddr().(*net.UDPAddr).Port
|
||||
tcpPort := dnsserver.servers[1].Listener.Addr().(*net.TCPAddr).Port
|
||||
if upstream != nil {
|
||||
dnsserver.upstream = upstream
|
||||
}
|
||||
go dnsserver.ActivateAndServe()
|
||||
return dnsserver, nameserver, udpPort, tcpPort
|
||||
}
|
||||
|
||||
func TestTruncation(t *testing.T) {
|
||||
//common.SetLogLevel("debug")
|
||||
dnsserver, nameserver, udpPort, tcpPort := startServer(t, nil)
|
||||
defer dnsserver.Stop()
|
||||
|
||||
// Add 100 mappings to nameserver
|
||||
addrs := []address.Address{}
|
||||
for i := address.Address(0); i < 100; i++ {
|
||||
addrs = append(addrs, i)
|
||||
nameserver.AddEntry("foo.weave.local.", "", mesh.UnknownPeerName, i)
|
||||
}
|
||||
|
||||
doRequest := func(client *dns.Client, request *dns.Msg, port int, expectedErr error) *dns.Msg {
|
||||
request.SetQuestion("foo.weave.local.", dns.TypeA)
|
||||
response, _, err := client.Exchange(request, fmt.Sprintf("127.0.0.1:%d", port))
|
||||
require.Equal(t, expectedErr, err)
|
||||
return response
|
||||
}
|
||||
|
||||
// do a udp query, ensure we get a truncated response
|
||||
{
|
||||
udpClient := dns.Client{Net: "udp", UDPSize: minUDPSize}
|
||||
response := doRequest(&udpClient, &dns.Msg{}, udpPort, dns.ErrTruncated)
|
||||
require.True(t, response.MsgHdr.Truncated)
|
||||
require.True(t, len(response.Answer) < 100)
|
||||
}
|
||||
|
||||
// do a udp query with big size, ensure we don't get a truncated response
|
||||
{
|
||||
udpClient := dns.Client{Net: "udp", UDPSize: 65535}
|
||||
request := &dns.Msg{}
|
||||
request.SetEdns0(65535, false)
|
||||
response := doRequest(&udpClient, request, udpPort, nil)
|
||||
require.False(t, response.MsgHdr.Truncated)
|
||||
require.Equal(t, len(response.Answer), 100)
|
||||
}
|
||||
|
||||
// do a tcp query, ensure we don't get a truncated response
|
||||
{
|
||||
tcpClient := dns.Client{Net: "tcp"}
|
||||
response := doRequest(&tcpClient, &dns.Msg{}, tcpPort, nil)
|
||||
require.False(t, response.MsgHdr.Truncated)
|
||||
require.Equal(t, len(response.Answer), 100)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTruncateResponse(t *testing.T) {
|
||||
|
||||
header := dns.RR_Header{
|
||||
Name: "host.domain.com",
|
||||
Rrtype: dns.TypePTR,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 30,
|
||||
}
|
||||
|
||||
for i := 0; i < 10000; i++ {
|
||||
// generate a random answer set
|
||||
numAnswers := 40 + rand.Intn(200)
|
||||
answers := make([]dns.RR, numAnswers)
|
||||
for j := 0; j < numAnswers; j++ {
|
||||
answers[j] = &dns.A{Hdr: header, A: address.Address(j).IP4()}
|
||||
}
|
||||
|
||||
// pick a random max size, truncate response to that, check it
|
||||
maxSize := 512 + rand.Intn(2*512)
|
||||
h := handler{maxResponseSize: maxSize}
|
||||
response := h.makeResponse(&dns.Msg{}, answers)
|
||||
require.True(t, response.Len() <= maxSize)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecursiveCompress(t *testing.T) {
|
||||
const (
|
||||
hostname = "foo.example."
|
||||
maxSize = 512
|
||||
)
|
||||
|
||||
// Construct a response that is >512 when uncompressed, <512 when compressed
|
||||
response := dns.Msg{}
|
||||
response.Authoritative = true
|
||||
response.Answer = []dns.RR{}
|
||||
header := dns.RR_Header{
|
||||
Name: hostname,
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 10,
|
||||
}
|
||||
for response.Len() <= maxSize {
|
||||
ip := address.Address(rand.Uint32()).IP4()
|
||||
response.Answer = append(response.Answer, &dns.A{Hdr: header, A: ip})
|
||||
}
|
||||
response.Compress = true
|
||||
require.True(t, response.Len() <= maxSize)
|
||||
|
||||
// A dns server that returns the above response
|
||||
var gotRequest = make(chan struct{}, 1)
|
||||
handleRecursive := func(w dns.ResponseWriter, req *dns.Msg) {
|
||||
gotRequest <- struct{}{}
|
||||
require.Equal(t, req.Question[0].Name, hostname)
|
||||
response.SetReply(req)
|
||||
err := w.WriteMsg(&response)
|
||||
require.Nil(t, err)
|
||||
}
|
||||
mux := dns.NewServeMux()
|
||||
mux.HandleFunc(topDomain, handleRecursive)
|
||||
udpListener, err := net.ListenPacket("udp", "0.0.0.0:0")
|
||||
require.Nil(t, err)
|
||||
udpServer := &dns.Server{PacketConn: udpListener, Handler: mux}
|
||||
udpServerPort := udpListener.LocalAddr().(*net.UDPAddr).Port
|
||||
go udpServer.ActivateAndServe()
|
||||
defer udpServer.Shutdown()
|
||||
|
||||
// The weavedns server, pointed at the above server
|
||||
dnsserver, _, udpPort, _ := startServer(t, &dns.ClientConfig{
|
||||
Servers: []string{"127.0.0.1"},
|
||||
Port: strconv.Itoa(udpServerPort),
|
||||
Ndots: 1,
|
||||
Timeout: 5,
|
||||
Attempts: 2,
|
||||
})
|
||||
defer dnsserver.Stop()
|
||||
|
||||
// Now do lookup, check its what we expected.
|
||||
// NB this doesn't really test golang's resolver behaves correctly, as I can't see
|
||||
// a way to point golangs resolver at a specific hosts.
|
||||
req := new(dns.Msg)
|
||||
req.Id = dns.Id()
|
||||
req.RecursionDesired = true
|
||||
req.Question = make([]dns.Question, 1)
|
||||
req.Question[0] = dns.Question{
|
||||
Name: hostname,
|
||||
Qtype: dns.TypeA,
|
||||
Qclass: dns.ClassINET,
|
||||
}
|
||||
c := new(dns.Client)
|
||||
res, _, err := c.Exchange(req, fmt.Sprintf("127.0.0.1:%d", udpPort))
|
||||
require.Nil(t, err)
|
||||
require.True(t, len(gotRequest) > 0)
|
||||
require.True(t, res.Len() > maxSize)
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
package nameserver
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/weaveworks/weave/mesh"
|
||||
"github.com/weaveworks/weave/net/address"
|
||||
)
|
||||
|
||||
var now = func() int64 { return time.Now().Unix() }
|
||||
|
||||
type Entry struct {
|
||||
ContainerID string
|
||||
Origin mesh.PeerName
|
||||
Addr address.Address
|
||||
Hostname string // as supplied
|
||||
lHostname string // lowercased (not exported, so not encoded by gob)
|
||||
Version int
|
||||
Tombstone int64 // timestamp of when it was deleted
|
||||
}
|
||||
|
||||
type Entries []Entry
|
||||
type CaseSensitive Entries
|
||||
type CaseInsensitive Entries
|
||||
type SortableEntries interface {
|
||||
sort.Interface
|
||||
Get(i int) Entry
|
||||
}
|
||||
|
||||
// Gossip messages are sorted in a case sensitive order...
|
||||
func (es CaseSensitive) Len() int { return len(es) }
|
||||
func (es CaseSensitive) Swap(i, j int) { es[i], es[j] = es[j], es[i] }
|
||||
func (es CaseSensitive) Get(i int) Entry { return es[i] }
|
||||
func (es CaseSensitive) Less(i, j int) bool { return es[i].less(&es[j]) }
|
||||
|
||||
// ... but we store entries in a case insensitive order.
|
||||
func (es CaseInsensitive) Len() int { return len(es) }
|
||||
func (es CaseInsensitive) Swap(i, j int) { es[i], es[j] = es[j], es[i] }
|
||||
func (es CaseInsensitive) Get(i int) Entry { return es[i] }
|
||||
func (es CaseInsensitive) Less(i, j int) bool { return es[i].insensitiveLess(&es[j]) }
|
||||
|
||||
func (e1 Entry) equal(e2 Entry) bool {
|
||||
return e1.ContainerID == e2.ContainerID &&
|
||||
e1.Origin == e2.Origin &&
|
||||
e1.Addr == e2.Addr &&
|
||||
e1.Hostname == e2.Hostname
|
||||
}
|
||||
|
||||
func (e1 *Entry) less(e2 *Entry) bool {
|
||||
// Entries are kept sorted by Hostname, Origin, ContainerID then address
|
||||
switch {
|
||||
case e1.Hostname != e2.Hostname:
|
||||
return e1.Hostname < e2.Hostname
|
||||
|
||||
case e1.Origin != e2.Origin:
|
||||
return e1.Origin < e2.Origin
|
||||
|
||||
case e1.ContainerID != e2.ContainerID:
|
||||
return e1.ContainerID < e2.ContainerID
|
||||
|
||||
default:
|
||||
return e1.Addr < e2.Addr
|
||||
}
|
||||
}
|
||||
|
||||
func (e1 *Entry) insensitiveLess(e2 *Entry) bool {
|
||||
// Entries are kept sorted by Hostname, Origin, ContainerID then address
|
||||
e1Hostname, e2Hostname := e1.lHostname, e2.lHostname
|
||||
switch {
|
||||
case e1Hostname != e2Hostname:
|
||||
return e1Hostname < e2Hostname
|
||||
|
||||
case e1.Origin != e2.Origin:
|
||||
return e1.Origin < e2.Origin
|
||||
|
||||
case e1.ContainerID != e2.ContainerID:
|
||||
return e1.ContainerID < e2.ContainerID
|
||||
|
||||
default:
|
||||
return e1.Addr < e2.Addr
|
||||
}
|
||||
}
|
||||
|
||||
// returns true to indicate a change
|
||||
func (e1 *Entry) merge(e2 *Entry) bool {
|
||||
// we know container id, origin, add and hostname are equal
|
||||
if e2.Version > e1.Version {
|
||||
e1.Version = e2.Version
|
||||
e1.Tombstone = e2.Tombstone
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (e1 *Entry) String() string {
|
||||
return fmt.Sprintf("%s -> %s", e1.Hostname, e1.Addr.String())
|
||||
}
|
||||
|
||||
func (e1 *Entry) addLowercase() {
|
||||
e1.lHostname = strings.ToLower(e1.Hostname)
|
||||
}
|
||||
|
||||
func check(es SortableEntries) error {
|
||||
if !sort.IsSorted(es) {
|
||||
return fmt.Errorf("Not sorted!")
|
||||
}
|
||||
for i := 1; i < es.Len(); i++ {
|
||||
if es.Get(i).equal(es.Get(i - 1)) {
|
||||
return fmt.Errorf("Duplicate entry: %d:%v and %d:%v", i-1, es.Get(i-1), i, es.Get(i))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkAndPanic(es SortableEntries) {
|
||||
if err := check(es); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (es *Entries) checkAndPanic() *Entries {
|
||||
checkAndPanic(CaseInsensitive(*es))
|
||||
return es
|
||||
}
|
||||
|
||||
func (es *Entries) add(hostname, containerid string, origin mesh.PeerName, addr address.Address) Entry {
|
||||
defer es.checkAndPanic().checkAndPanic()
|
||||
|
||||
entry := Entry{Hostname: hostname, lHostname: strings.ToLower(hostname),
|
||||
Origin: origin, ContainerID: containerid, Addr: addr}
|
||||
i := sort.Search(len(*es), func(i int) bool {
|
||||
return !(*es)[i].insensitiveLess(&entry)
|
||||
})
|
||||
if i < len(*es) && (*es)[i].equal(entry) {
|
||||
if (*es)[i].Tombstone > 0 {
|
||||
(*es)[i].Tombstone = 0
|
||||
(*es)[i].Version++
|
||||
}
|
||||
} else {
|
||||
*es = append(*es, Entry{})
|
||||
copy((*es)[i+1:], (*es)[i:])
|
||||
(*es)[i] = entry
|
||||
}
|
||||
return (*es)[i]
|
||||
}
|
||||
|
||||
func (es *Entries) merge(incoming Entries) Entries {
|
||||
defer es.checkAndPanic().checkAndPanic()
|
||||
incoming.checkAndPanic()
|
||||
|
||||
newEntries := Entries{}
|
||||
i := 0
|
||||
|
||||
for _, entry := range incoming {
|
||||
for i < len(*es) && (*es)[i].insensitiveLess(&entry) {
|
||||
i++
|
||||
}
|
||||
if i < len(*es) && (*es)[i].equal(entry) {
|
||||
if (*es)[i].merge(&entry) {
|
||||
newEntries = append(newEntries, entry)
|
||||
}
|
||||
} else {
|
||||
*es = append(*es, Entry{})
|
||||
copy((*es)[i+1:], (*es)[i:])
|
||||
(*es)[i] = entry
|
||||
newEntries = append(newEntries, entry)
|
||||
}
|
||||
}
|
||||
|
||||
return newEntries
|
||||
}
|
||||
|
||||
// f returning true means keep the entry.
|
||||
func (es *Entries) tombstone(ourname mesh.PeerName, f func(*Entry) bool) Entries {
|
||||
defer es.checkAndPanic().checkAndPanic()
|
||||
|
||||
tombstoned := Entries{}
|
||||
for i, e := range *es {
|
||||
if f(&e) && e.Origin == ourname {
|
||||
e.Version++
|
||||
e.Tombstone = now()
|
||||
(*es)[i] = e
|
||||
tombstoned = append(tombstoned, e)
|
||||
}
|
||||
}
|
||||
return tombstoned
|
||||
}
|
||||
|
||||
func (es *Entries) filter(f func(*Entry) bool) {
|
||||
defer es.checkAndPanic().checkAndPanic()
|
||||
|
||||
i := 0
|
||||
for _, e := range *es {
|
||||
if !f(&e) {
|
||||
continue
|
||||
}
|
||||
(*es)[i] = e
|
||||
i++
|
||||
}
|
||||
*es = (*es)[:i]
|
||||
}
|
||||
|
||||
func (es Entries) lookup(hostname string) Entries {
|
||||
es.checkAndPanic()
|
||||
|
||||
lowerHostname := strings.ToLower(hostname)
|
||||
i := sort.Search(len(es), func(i int) bool {
|
||||
return es[i].lHostname >= lowerHostname
|
||||
})
|
||||
if i >= len(es) || es[i].lHostname != lowerHostname {
|
||||
return Entries{}
|
||||
}
|
||||
|
||||
j := sort.Search(len(es)-i, func(j int) bool {
|
||||
return es[i+j].lHostname > lowerHostname
|
||||
})
|
||||
|
||||
return es[i : i+j]
|
||||
}
|
||||
|
||||
func (es Entries) first(f func(*Entry) bool) (*Entry, error) {
|
||||
es.checkAndPanic()
|
||||
|
||||
for _, e := range es {
|
||||
if f(&e) {
|
||||
return &e, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("Not found")
|
||||
}
|
||||
|
||||
func (es Entries) addLowercase() {
|
||||
for i := range es {
|
||||
es[i].addLowercase()
|
||||
}
|
||||
}
|
||||
|
||||
type GossipData struct {
|
||||
Timestamp int64
|
||||
Entries
|
||||
}
|
||||
|
||||
func (g *GossipData) Merge(o mesh.GossipData) mesh.GossipData {
|
||||
other := o.(*GossipData)
|
||||
gossip := g.copy()
|
||||
gossip.Entries.merge(other.Entries)
|
||||
if gossip.Timestamp < other.Timestamp {
|
||||
gossip.Timestamp = other.Timestamp
|
||||
}
|
||||
return gossip
|
||||
}
|
||||
|
||||
func (g *GossipData) Decode(msg []byte) error {
|
||||
if err := gob.NewDecoder(bytes.NewReader(msg)).Decode(g); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
g.Entries.addLowercase() // lowercase strings not sent on the wire
|
||||
sort.Sort(CaseInsensitive(g.Entries))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *GossipData) Encode() [][]byte {
|
||||
g2 := g.copy()
|
||||
sort.Sort(CaseSensitive(g2.Entries))
|
||||
buf := &bytes.Buffer{}
|
||||
if err := gob.NewEncoder(buf).Encode(g2); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return [][]byte{buf.Bytes()}
|
||||
}
|
||||
|
||||
func (g *GossipData) copy() *GossipData {
|
||||
g2 := &GossipData{Timestamp: g.Timestamp, Entries: make(Entries, len(g.Entries))}
|
||||
copy(g2.Entries, g.Entries)
|
||||
return g2
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package nameserver
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/weaveworks/weave/mesh"
|
||||
"github.com/weaveworks/weave/net/address"
|
||||
)
|
||||
|
||||
func l(es Entries) Entries {
|
||||
es.addLowercase()
|
||||
return es
|
||||
}
|
||||
|
||||
func makeEntries(values string) Entries {
|
||||
entries := make(Entries, len(values))
|
||||
for i, c := range values {
|
||||
entries[i] = Entry{Hostname: string(c)}
|
||||
}
|
||||
return l(entries)
|
||||
}
|
||||
|
||||
func TestAdd(t *testing.T) {
|
||||
oldNow := now
|
||||
defer func() { now = oldNow }()
|
||||
now = func() int64 { return 1234 }
|
||||
|
||||
entries := Entries{}
|
||||
entries.add("A", "", mesh.UnknownPeerName, address.Address(0))
|
||||
expected := l(Entries{
|
||||
Entry{Hostname: "A", Origin: mesh.UnknownPeerName, Addr: address.Address(0)},
|
||||
})
|
||||
require.Equal(t, entries, expected)
|
||||
|
||||
entries.tombstone(mesh.UnknownPeerName, func(e *Entry) bool { return e.Hostname == "A" })
|
||||
expected = l(Entries{
|
||||
Entry{Hostname: "A", Origin: mesh.UnknownPeerName, Addr: address.Address(0), Version: 1, Tombstone: 1234},
|
||||
})
|
||||
require.Equal(t, entries, expected)
|
||||
|
||||
entries.add("A", "", mesh.UnknownPeerName, address.Address(0))
|
||||
expected = l(Entries{
|
||||
Entry{Hostname: "A", Origin: mesh.UnknownPeerName, Addr: address.Address(0), Version: 2},
|
||||
})
|
||||
require.Equal(t, entries, expected)
|
||||
}
|
||||
|
||||
func TestMerge(t *testing.T) {
|
||||
e1 := makeEntries("ACDF")
|
||||
e2 := makeEntries("BEF")
|
||||
|
||||
diff := e1.merge(e2)
|
||||
|
||||
require.Equal(t, makeEntries("BE"), diff)
|
||||
require.Equal(t, makeEntries("ABCDEF"), e1)
|
||||
|
||||
diff = e1.merge(e1)
|
||||
require.Equal(t, Entries{}, diff)
|
||||
}
|
||||
|
||||
func TestOldMerge(t *testing.T) {
|
||||
e1 := l(Entries{Entry{Hostname: "A", Version: 0}})
|
||||
diff := e1.merge(l(Entries{Entry{Hostname: "A", Version: 1}}))
|
||||
require.Equal(t, l(Entries{Entry{Hostname: "A", Version: 1}}), diff)
|
||||
require.Equal(t, l(Entries{Entry{Hostname: "A", Version: 1}}), e1)
|
||||
|
||||
diff = e1.merge(l(Entries{Entry{Hostname: "A", Version: 0}}))
|
||||
require.Equal(t, Entries{}, diff)
|
||||
require.Equal(t, l(Entries{Entry{Hostname: "A", Version: 1}}), e1)
|
||||
}
|
||||
|
||||
func TestTombstone(t *testing.T) {
|
||||
oldNow := now
|
||||
defer func() { now = oldNow }()
|
||||
now = func() int64 { return 1234 }
|
||||
|
||||
es := makeEntries("AB")
|
||||
|
||||
es.tombstone(mesh.UnknownPeerName, func(e *Entry) bool {
|
||||
return e.Hostname == "B"
|
||||
})
|
||||
expected := l(Entries{
|
||||
Entry{Hostname: "A"},
|
||||
Entry{Hostname: "B", Version: 1, Tombstone: 1234},
|
||||
})
|
||||
require.Equal(t, expected, es)
|
||||
}
|
||||
|
||||
func TestDelete(t *testing.T) {
|
||||
es := makeEntries("AB")
|
||||
|
||||
es.filter(func(e *Entry) bool {
|
||||
return e.Hostname != "A"
|
||||
})
|
||||
require.Equal(t, makeEntries("B"), es)
|
||||
}
|
||||
|
||||
func TestLookup(t *testing.T) {
|
||||
es := l(Entries{
|
||||
Entry{Hostname: "A"},
|
||||
Entry{Hostname: "B", ContainerID: "bar"},
|
||||
Entry{Hostname: "B", ContainerID: "foo"},
|
||||
Entry{Hostname: "C"},
|
||||
})
|
||||
|
||||
have := es.lookup("B")
|
||||
want := l(Entries{
|
||||
Entry{Hostname: "B", ContainerID: "bar"},
|
||||
Entry{Hostname: "B", ContainerID: "foo"},
|
||||
})
|
||||
require.Equal(t, have, want)
|
||||
}
|
||||
|
||||
func TestGossipDataMerge(t *testing.T) {
|
||||
g1 := GossipData{Entries: makeEntries("AcDf")}
|
||||
g2 := GossipData{Entries: makeEntries("BEf")}
|
||||
|
||||
g3 := g1.Merge(&g2).(*GossipData)
|
||||
|
||||
require.Equal(t, GossipData{Entries: makeEntries("ABcDEf")}, *g3)
|
||||
}
|
||||
+71
-67
@@ -1,95 +1,99 @@
|
||||
package nameserver
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/miekg/dns"
|
||||
. "github.com/weaveworks/weave/common"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/weaveworks/weave/common/docker"
|
||||
"github.com/weaveworks/weave/net/address"
|
||||
)
|
||||
|
||||
func httpErrorAndLog(level *log.Logger, w http.ResponseWriter, msg string,
|
||||
status int, logmsg string, logargs ...interface{}) {
|
||||
http.Error(w, msg, status)
|
||||
level.Printf("[http] "+logmsg, logargs...)
|
||||
func (n *Nameserver) badRequest(w http.ResponseWriter, err error) {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
n.infof("%v", err)
|
||||
}
|
||||
|
||||
func ListenHTTP(version string, server *DNSServer, domain string, db Zone, port int) {
|
||||
|
||||
muxRouter := mux.NewRouter()
|
||||
|
||||
muxRouter.Methods("GET").Path("/status").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintln(w, "weave DNS", version)
|
||||
fmt.Fprintln(w, server.Status())
|
||||
func (n *Nameserver) HandleHTTP(router *mux.Router, dockerCli *docker.Client) {
|
||||
router.Methods("GET").Path("/domain").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, n.domain)
|
||||
})
|
||||
|
||||
muxRouter.Methods("PUT").Path("/name/{id:.+}/{ip:.+}").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
reqError := func(msg string, logmsg string, logargs ...interface{}) {
|
||||
httpErrorAndLog(Warning, w, msg, http.StatusBadRequest, logmsg, logargs...)
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
idStr := vars["id"]
|
||||
ipStr := vars["ip"]
|
||||
name := r.FormValue("fqdn")
|
||||
|
||||
if name == "" {
|
||||
reqError("Invalid FQDN", "Invalid FQDN in request: %s, %s", r.URL, r.Form)
|
||||
router.Methods("PUT").Path("/name/{container}/{ip}").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
vars = mux.Vars(r)
|
||||
container = vars["container"]
|
||||
ipStr = vars["ip"]
|
||||
hostname = dns.Fqdn(r.FormValue("fqdn"))
|
||||
ip, err = address.ParseIP(ipStr)
|
||||
)
|
||||
if err != nil {
|
||||
n.badRequest(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
ip := net.ParseIP(ipStr)
|
||||
if ip == nil {
|
||||
reqError("Invalid IP", "Invalid IP in request: %s", ipStr)
|
||||
if !dns.IsSubDomain(n.domain, hostname) {
|
||||
n.infof("Ignoring registration %s %s %s (not a subdomain of %s)", hostname, ipStr, container, n.domain)
|
||||
return
|
||||
}
|
||||
|
||||
if dns.IsSubDomain(domain, name) {
|
||||
Info.Printf("[http] Adding %s -> %s", name, ipStr)
|
||||
if err := db.AddRecord(idStr, name, ip); err != nil {
|
||||
if _, ok := err.(DuplicateError); !ok {
|
||||
httpErrorAndLog(
|
||||
Error, w, "Internal error", http.StatusInternalServerError,
|
||||
"Unexpected error from DB: %s", err)
|
||||
return
|
||||
} // oh, I already know this. whatever.
|
||||
if err := n.AddEntry(hostname, container, n.ourName, ip); err != nil {
|
||||
n.badRequest(w, fmt.Errorf("Unable to add entry: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
if r.FormValue("check-alive") == "true" && dockerCli != nil && dockerCli.IsContainerNotRunning(container) {
|
||||
n.infof("container '%s' is not running: removing", container)
|
||||
if err := n.Delete(hostname, container, ipStr, ip); err != nil {
|
||||
n.infof("failed to remove: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
w.WriteHeader(204)
|
||||
})
|
||||
|
||||
deleteHandler := func(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
|
||||
hostname := r.FormValue("fqdn")
|
||||
if hostname == "" {
|
||||
hostname = "*"
|
||||
} else {
|
||||
Info.Printf("[http] Ignoring name %s, not in %s", name, domain)
|
||||
}
|
||||
})
|
||||
|
||||
muxRouter.Methods("DELETE").Path("/name/{id:.+}/{ip:.+}").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
reqError := func(msg string, logmsg string, logargs ...interface{}) {
|
||||
httpErrorAndLog(Warning, w, msg, http.StatusBadRequest, logmsg, logargs...)
|
||||
hostname = dns.Fqdn(hostname)
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
idStr := vars["id"]
|
||||
ipStr := vars["ip"]
|
||||
container, ok := vars["container"]
|
||||
if !ok {
|
||||
container = "*"
|
||||
}
|
||||
|
||||
ip := net.ParseIP(ipStr)
|
||||
if ip == nil {
|
||||
reqError("Invalid IP in request", "Invalid IP in request: %s", ipStr)
|
||||
ipStr, ok := vars["ip"]
|
||||
ip, err := address.ParseIP(ipStr)
|
||||
if ok && err != nil {
|
||||
n.badRequest(w, err)
|
||||
return
|
||||
} else if !ok {
|
||||
ipStr = "*"
|
||||
}
|
||||
|
||||
if err := n.Delete(hostname, container, ipStr, ip); err != nil {
|
||||
n.badRequest(w, fmt.Errorf("Unable to delete entries: %v", err))
|
||||
return
|
||||
}
|
||||
Info.Printf("[http] Deleting %s (%s)", idStr, ipStr)
|
||||
if err := db.DeleteRecord(idStr, ip); err != nil {
|
||||
if _, ok := err.(LookupError); !ok {
|
||||
httpErrorAndLog(
|
||||
Error, w, "Internal error", http.StatusInternalServerError,
|
||||
"Unexpected error from DB: %s", err)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(204)
|
||||
}
|
||||
router.Methods("DELETE").Path("/name/{container}/{ip}").HandlerFunc(deleteHandler)
|
||||
router.Methods("DELETE").Path("/name/{container}").HandlerFunc(deleteHandler)
|
||||
router.Methods("DELETE").Path("/name").HandlerFunc(deleteHandler)
|
||||
|
||||
router.Methods("GET").Path("/name").Headers("Accept", "application/json").HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
n.RLock()
|
||||
defer n.RUnlock()
|
||||
if err := json.NewEncoder(w).Encode(n.entries); err != nil {
|
||||
n.badRequest(w, fmt.Errorf("Error marshalling response: %v", err))
|
||||
}
|
||||
})
|
||||
|
||||
http.Handle("/", muxRouter)
|
||||
|
||||
address := fmt.Sprintf(":%d", port)
|
||||
if err := http.ListenAndServe(address, nil); err != nil {
|
||||
Error.Fatal("[http] Unable to create http listener: ", err)
|
||||
}
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
package nameserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
wt "github.com/weaveworks/weave/testing"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func genForm(method string, url string, data url.Values) (resp *http.Response, err error) {
|
||||
req, err := http.NewRequest(method, url, strings.NewReader(data.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
return http.DefaultClient.Do(req)
|
||||
}
|
||||
|
||||
func TestHttp(t *testing.T) {
|
||||
var (
|
||||
containerID = "deadbeef"
|
||||
testDomain = "weave.local."
|
||||
successTestName = "test1." + testDomain
|
||||
testAddr1 = "10.2.2.1/24"
|
||||
dockerIP = "9.8.7.6"
|
||||
)
|
||||
|
||||
var zone = new(ZoneDb)
|
||||
port := rand.Intn(10000) + 32768
|
||||
fmt.Println("Http test on port", port)
|
||||
go ListenHTTP("", nil, testDomain, zone, port)
|
||||
|
||||
time.Sleep(100 * time.Millisecond) // Allow for http server to get going
|
||||
|
||||
// Ask the http server to add our test address into the database
|
||||
addrParts := strings.Split(testAddr1, "/")
|
||||
addrURL := fmt.Sprintf("http://localhost:%d/name/%s/%s", port, containerID, addrParts[0])
|
||||
resp, err := genForm("PUT", addrURL,
|
||||
url.Values{"fqdn": {successTestName}, "local_ip": {dockerIP}, "routing_prefix": {addrParts[1]}})
|
||||
wt.AssertNoErr(t, err)
|
||||
wt.AssertStatus(t, resp.StatusCode, http.StatusOK, "http response")
|
||||
|
||||
// Check that the address is now there.
|
||||
foundIP, err := zone.LookupName(successTestName)
|
||||
wt.AssertNoErr(t, err)
|
||||
ip, _, _ := net.ParseCIDR(testAddr1)
|
||||
if !foundIP.Equal(ip) {
|
||||
t.Fatal("Unexpected result for", successTestName, foundIP)
|
||||
}
|
||||
|
||||
// Adding exactly the same address should be OK
|
||||
resp, err = genForm("PUT", addrURL,
|
||||
url.Values{"fqdn": {successTestName}, "local_ip": {dockerIP}, "routing_prefix": {addrParts[1]}})
|
||||
wt.AssertNoErr(t, err)
|
||||
wt.AssertStatus(t, resp.StatusCode, http.StatusOK, "http success response for duplicate add")
|
||||
|
||||
// Now try adding the same address again with a different ident --
|
||||
// again should be fine
|
||||
otherURL := fmt.Sprintf("http://localhost:%d/name/%s/%s", port, "other", addrParts[0])
|
||||
resp, err = genForm("PUT", otherURL,
|
||||
url.Values{"fqdn": {successTestName}, "local_ip": {dockerIP}, "routing_prefix": {addrParts[1]}})
|
||||
wt.AssertNoErr(t, err)
|
||||
wt.AssertStatus(t, resp.StatusCode, http.StatusOK, "http response")
|
||||
|
||||
// Delete the address
|
||||
resp, err = genForm("DELETE", addrURL, nil)
|
||||
wt.AssertNoErr(t, err)
|
||||
wt.AssertStatus(t, resp.StatusCode, http.StatusOK, "http response")
|
||||
|
||||
// Check that the address is still resolvable.
|
||||
_, err = zone.LookupName(successTestName)
|
||||
wt.AssertNoErr(t, err)
|
||||
|
||||
// Delete the address record mentioning the other container
|
||||
resp, err = genForm("DELETE", otherURL, nil)
|
||||
wt.AssertNoErr(t, err)
|
||||
wt.AssertStatus(t, resp.StatusCode, http.StatusOK, "http response")
|
||||
|
||||
// Check that the address is gone
|
||||
_, err = zone.LookupName(successTestName)
|
||||
wt.AssertErrorType(t, err, (*LookupError)(nil), "fully-removed address")
|
||||
|
||||
// Would like to shut down the http server at the end of this test
|
||||
// but it's complicated.
|
||||
// See https://groups.google.com/forum/#!topic/golang-nuts/vLHWa5sHnCE
|
||||
}
|
||||
@@ -1,232 +0,0 @@
|
||||
package nameserver
|
||||
|
||||
import (
|
||||
"github.com/miekg/dns"
|
||||
"math"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Portions of this code taken from github.com/armon/mdns
|
||||
|
||||
const (
|
||||
ipv4mdns = "224.0.0.251" // link-local multicast address
|
||||
mdnsPort = 5353 // mDNS assigned port
|
||||
// We wait this long to hear a response from other mDNS servers on
|
||||
// the network.
|
||||
mDNSTimeout = 500 * time.Millisecond
|
||||
MaxDuration = time.Duration(math.MaxInt64)
|
||||
MailboxSize = 16
|
||||
)
|
||||
|
||||
var (
|
||||
ipv4Addr = &net.UDPAddr{
|
||||
IP: net.ParseIP(ipv4mdns),
|
||||
Port: mdnsPort,
|
||||
}
|
||||
)
|
||||
|
||||
type Response struct {
|
||||
Name string
|
||||
Addr net.IP
|
||||
Err error
|
||||
}
|
||||
|
||||
type responseInfo struct {
|
||||
timeout time.Time // if no answer by this time, give up
|
||||
ch chan<- *Response
|
||||
}
|
||||
|
||||
// Represents one query that we have sent for one name.
|
||||
// If we, internally, get several requests for the same name while we have
|
||||
// a query in flight, then we don't want to send more queries out.
|
||||
// Invariant on responseInfos: they are in ascending order of timeout.
|
||||
type inflightQuery struct {
|
||||
name string
|
||||
id uint16 // the DNS message ID
|
||||
responseInfos []*responseInfo
|
||||
}
|
||||
|
||||
type MDNSClient struct {
|
||||
// note unorthodox use of 'Server' class in client logic - using it to
|
||||
// listen on a multicast socket and call callback whenever a message comes in.
|
||||
listener *dns.Server
|
||||
conn *net.UDPConn
|
||||
addr *net.UDPAddr
|
||||
inflight map[string]*inflightQuery
|
||||
actionChan chan<- MDNSAction
|
||||
}
|
||||
|
||||
type mDNSQueryInfo struct {
|
||||
name string
|
||||
querytype uint16
|
||||
responseCh chan<- *Response
|
||||
}
|
||||
|
||||
func NewMDNSClient() (*MDNSClient, error) {
|
||||
if conn, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4zero, Port: 0}); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return &MDNSClient{
|
||||
conn: conn,
|
||||
addr: ipv4Addr,
|
||||
inflight: make(map[string]*inflightQuery)}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *MDNSClient) Start(ifi *net.Interface) error {
|
||||
multicast, err := LinkLocalMulticastListener(ifi)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
handleMDNS := func(w dns.ResponseWriter, r *dns.Msg) {
|
||||
// Don't want to handle queries here, so filter anything out that isn't a response
|
||||
if len(r.Answer) > 0 {
|
||||
c.ResponseCallback(r)
|
||||
}
|
||||
}
|
||||
|
||||
c.listener = &dns.Server{Unsafe: true, PacketConn: multicast, Handler: dns.HandlerFunc(handleMDNS)}
|
||||
go c.listener.ActivateAndServe()
|
||||
|
||||
actionChan := make(chan MDNSAction, MailboxSize)
|
||||
c.actionChan = actionChan
|
||||
go c.actorLoop(actionChan)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func LinkLocalMulticastListener(ifi *net.Interface) (net.PacketConn, error) {
|
||||
return net.ListenMulticastUDP("udp", ifi, ipv4Addr)
|
||||
}
|
||||
|
||||
// ACTOR client API
|
||||
|
||||
type MDNSAction func()
|
||||
|
||||
// Async
|
||||
func (c *MDNSClient) SendQuery(name string, querytype uint16, responseCh chan<- *Response) {
|
||||
c.actionChan <- func() {
|
||||
query, found := c.inflight[name]
|
||||
if !found {
|
||||
m := new(dns.Msg)
|
||||
m.SetQuestion(name, querytype)
|
||||
m.RecursionDesired = false
|
||||
buf, err := m.Pack()
|
||||
if err != nil {
|
||||
responseCh <- &Response{Err: err}
|
||||
close(responseCh)
|
||||
return
|
||||
}
|
||||
query = &inflightQuery{
|
||||
name: name,
|
||||
id: m.Id,
|
||||
}
|
||||
if _, err = c.conn.WriteTo(buf, c.addr); err != nil {
|
||||
responseCh <- &Response{Err: err}
|
||||
close(responseCh)
|
||||
return
|
||||
}
|
||||
c.inflight[name] = query
|
||||
}
|
||||
info := &responseInfo{
|
||||
ch: responseCh,
|
||||
timeout: time.Now().Add(mDNSTimeout),
|
||||
}
|
||||
// Invariant on responseInfos: they are in ascending order of
|
||||
// timeout. Since we use a fixed interval from Now(), this
|
||||
// must be after all existing timeouts.
|
||||
query.responseInfos = append(query.responseInfos, info)
|
||||
}
|
||||
}
|
||||
|
||||
// Async - called from dns library multiplexer
|
||||
func (c *MDNSClient) ResponseCallback(r *dns.Msg) {
|
||||
c.actionChan <- func() {
|
||||
for _, answer := range r.Answer {
|
||||
var name string
|
||||
var res *Response
|
||||
|
||||
switch rr := answer.(type) {
|
||||
case *dns.A:
|
||||
name = rr.Hdr.Name
|
||||
res = &Response{Addr: rr.A}
|
||||
case *dns.PTR:
|
||||
name = rr.Hdr.Name
|
||||
res = &Response{Name: rr.Ptr}
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
if query, found := c.inflight[name]; found {
|
||||
for _, resp := range query.responseInfos {
|
||||
resp.ch <- res
|
||||
close(resp.ch)
|
||||
}
|
||||
delete(c.inflight, name)
|
||||
} else {
|
||||
// We've received a response that didn't match a query
|
||||
// Do we want to cache it?
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ACTOR server
|
||||
|
||||
// Check all in-flight queries, close all that have already timed out,
|
||||
// and return the duration until the next timeout
|
||||
func (c *MDNSClient) checkInFlightQueries() time.Duration {
|
||||
now := time.Now()
|
||||
after := MaxDuration
|
||||
for name, query := range c.inflight {
|
||||
// Invariant on responseInfos: they are in ascending order of timeout.
|
||||
numClosed := 0
|
||||
for _, item := range query.responseInfos {
|
||||
duration := item.timeout.Sub(now)
|
||||
if duration <= 0 { // timed out
|
||||
close(item.ch)
|
||||
numClosed++
|
||||
} else {
|
||||
if duration < after {
|
||||
after = duration
|
||||
}
|
||||
break // don't need to look at any more for this query
|
||||
}
|
||||
}
|
||||
// Remove timed-out items from the slice
|
||||
query.responseInfos = query.responseInfos[numClosed:]
|
||||
if len(query.responseInfos) == 0 {
|
||||
delete(c.inflight, name)
|
||||
}
|
||||
}
|
||||
return after
|
||||
}
|
||||
|
||||
func (c *MDNSClient) actorLoop(actionChan <-chan MDNSAction) {
|
||||
timer := time.NewTimer(MaxDuration)
|
||||
run := func() { timer.Reset(c.checkInFlightQueries()) }
|
||||
terminate := false
|
||||
for !terminate {
|
||||
select {
|
||||
case action := <-actionChan:
|
||||
if action == nil {
|
||||
c.listener.Shutdown()
|
||||
terminate = true
|
||||
} else {
|
||||
action()
|
||||
run()
|
||||
}
|
||||
case <-timer.C:
|
||||
run()
|
||||
}
|
||||
}
|
||||
|
||||
// Close all response channels at termination
|
||||
for _, query := range c.inflight {
|
||||
for _, item := range query.responseInfos {
|
||||
close(item.ch)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,195 +0,0 @@
|
||||
package nameserver
|
||||
|
||||
import (
|
||||
"github.com/miekg/dns"
|
||||
. "github.com/weaveworks/weave/common"
|
||||
wt "github.com/weaveworks/weave/testing"
|
||||
"log"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
successTestName = "test1.weave."
|
||||
failTestName = "test2.weave."
|
||||
testAddr = net.ParseIP("9.8.7.6")
|
||||
testInAddr = "6.7.8.9.in-addr.arpa."
|
||||
)
|
||||
|
||||
func minimalServer(w dns.ResponseWriter, req *dns.Msg) {
|
||||
|
||||
sendAnswer := func(m *dns.Msg) {
|
||||
buf, err := m.Pack()
|
||||
CheckFatal(err)
|
||||
if buf == nil {
|
||||
log.Fatal("Nil buffer")
|
||||
}
|
||||
//log.Println("minimalServer sending:", buf)
|
||||
// This is a bit of a kludge - per the RFC we should send responses from 5353, but that doesn't seem to work
|
||||
sendconn, err := net.DialUDP("udp", nil, ipv4Addr)
|
||||
CheckFatal(err)
|
||||
|
||||
_, err = sendconn.Write(buf)
|
||||
sendconn.Close()
|
||||
CheckFatal(err)
|
||||
}
|
||||
|
||||
//log.Println("minimalServer received:", req)
|
||||
if len(req.Answer) > 0 {
|
||||
return // Only interested in questions.
|
||||
}
|
||||
if len(req.Question) != 1 {
|
||||
return // We only handle single-question messages
|
||||
}
|
||||
|
||||
if req.Question[0].Qtype == dns.TypeA && req.Question[0].Name == successTestName {
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(req)
|
||||
hdr := dns.RR_Header{Name: m.Question[0].Name, Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET, Ttl: 3600}
|
||||
a := &dns.A{Hdr: hdr, A: testAddr}
|
||||
m.Answer = append(m.Answer, a)
|
||||
sendAnswer(m)
|
||||
} else if req.Question[0].Qtype == dns.TypePTR && req.Question[0].Name == testInAddr {
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(req)
|
||||
hdr := dns.RR_Header{Name: m.Question[0].Name, Rrtype: dns.TypePTR,
|
||||
Class: dns.ClassINET, Ttl: 3600}
|
||||
a := &dns.PTR{Hdr: hdr, Ptr: successTestName}
|
||||
m.Answer = append(m.Answer, a)
|
||||
sendAnswer(m)
|
||||
}
|
||||
}
|
||||
|
||||
func RunLocalMulticastServer() (*dns.Server, error) {
|
||||
multicast, err := LinkLocalMulticastListener(nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
server := &dns.Server{
|
||||
PacketConn: multicast,
|
||||
Handler: dns.HandlerFunc(minimalServer),
|
||||
ReadTimeout: 100 * time.Millisecond}
|
||||
go server.ActivateAndServe()
|
||||
return server, nil
|
||||
}
|
||||
|
||||
func setup(t *testing.T) (*MDNSClient, *dns.Server, error) {
|
||||
server, err := RunLocalMulticastServer()
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to run test server: %s. No default multicast interface?", err)
|
||||
}
|
||||
|
||||
mdnsClient, err := NewMDNSClient()
|
||||
wt.AssertNoErr(t, err)
|
||||
err = mdnsClient.Start(nil)
|
||||
wt.AssertNoErr(t, err)
|
||||
|
||||
return mdnsClient, server, err
|
||||
}
|
||||
|
||||
func (c *MDNSClient) Shutdown() {
|
||||
c.actionChan <- nil
|
||||
}
|
||||
|
||||
type testContext struct {
|
||||
receivedAddr net.IP
|
||||
receivedCount int
|
||||
channel chan *Response
|
||||
}
|
||||
|
||||
func newTestContext() *testContext {
|
||||
return &testContext{channel: make(chan *Response, 4)}
|
||||
}
|
||||
|
||||
func (c *testContext) checkResponse(t *testing.T, channelOk bool, resp *Response) {
|
||||
if !channelOk {
|
||||
c.channel = nil
|
||||
return
|
||||
}
|
||||
wt.AssertNoErr(t, resp.Err)
|
||||
log.Printf("Got address response %s addr %s", resp.Name, resp.Addr)
|
||||
c.receivedAddr = resp.Addr
|
||||
c.receivedCount++
|
||||
}
|
||||
|
||||
func TestSimpleQuery(t *testing.T) {
|
||||
log.Println("TestSimpleQuery starting")
|
||||
mdnsClient, server, _ := setup(t)
|
||||
defer mdnsClient.Shutdown()
|
||||
defer server.Shutdown()
|
||||
|
||||
context := newTestContext()
|
||||
|
||||
// First, a test we expect to succeed
|
||||
mdnsClient.SendQuery(successTestName, dns.TypeA, context.channel)
|
||||
for resp := range context.channel {
|
||||
context.checkResponse(t, true, resp)
|
||||
}
|
||||
|
||||
if !context.receivedAddr.Equal(testAddr) || context.receivedCount != 1 {
|
||||
t.Fatal("Unexpected result for", successTestName, context.receivedAddr, context.receivedCount)
|
||||
}
|
||||
|
||||
// Now, a test we expect to time out with no responses
|
||||
context = newTestContext()
|
||||
mdnsClient.SendQuery("test2.weave.", dns.TypeA, context.channel)
|
||||
for resp := range context.channel {
|
||||
context.checkResponse(t, true, resp)
|
||||
}
|
||||
|
||||
if context.receivedCount > 0 {
|
||||
t.Fatal("Unexpected result for test2.weave", context.receivedAddr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParallelQuery(t *testing.T) {
|
||||
log.Println("TestParallelQuery starting")
|
||||
mdnsClient, server, _ := setup(t)
|
||||
defer mdnsClient.Shutdown()
|
||||
defer server.Shutdown()
|
||||
|
||||
context1 := newTestContext()
|
||||
context2 := newTestContext()
|
||||
|
||||
go mdnsClient.SendQuery(successTestName, dns.TypeA, context1.channel)
|
||||
go mdnsClient.SendQuery(successTestName, dns.TypeA, context2.channel)
|
||||
timeout := time.After(2 * time.Second)
|
||||
outerloop:
|
||||
for context1.channel != nil || context2.channel != nil {
|
||||
select {
|
||||
case resp, ok := <-context1.channel:
|
||||
context1.checkResponse(t, ok, resp)
|
||||
case resp, ok := <-context2.channel:
|
||||
context2.checkResponse(t, ok, resp)
|
||||
case <-timeout:
|
||||
break outerloop
|
||||
}
|
||||
}
|
||||
|
||||
if !context1.receivedAddr.Equal(testAddr) || !context2.receivedAddr.Equal(testAddr) || context1.receivedCount != 1 || context2.receivedCount != 1 {
|
||||
t.Fatal("Unexpected result for", successTestName, context1.receivedAddr, context2.receivedAddr, context1.receivedCount, context2.receivedCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAsLookup(t *testing.T) {
|
||||
mdnsClient, server, _ := setup(t)
|
||||
defer mdnsClient.Shutdown()
|
||||
defer server.Shutdown()
|
||||
|
||||
ip, err := mdnsClient.LookupName(successTestName)
|
||||
wt.AssertNoErr(t, err)
|
||||
if !testAddr.Equal(ip) {
|
||||
t.Fatalf("Returned address incorrect %s", ip)
|
||||
}
|
||||
|
||||
ip, err = mdnsClient.LookupName("foo.example.com.")
|
||||
wt.AssertErrorType(t, err, (*LookupError)(nil), "unknown hostname")
|
||||
|
||||
name, err := mdnsClient.LookupInaddr(testInAddr)
|
||||
wt.AssertNoErr(t, err)
|
||||
if !(successTestName == name) {
|
||||
t.Fatalf("Expected name %s, got %s", successTestName, name)
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package nameserver
|
||||
|
||||
import (
|
||||
"github.com/miekg/dns"
|
||||
. "github.com/weaveworks/weave/common"
|
||||
"net"
|
||||
)
|
||||
|
||||
func mdnsLookup(client *MDNSClient, name string, qtype uint16) (*Response, error) {
|
||||
channel := make(chan *Response)
|
||||
client.SendQuery(name, qtype, channel)
|
||||
for resp := range channel {
|
||||
if err := resp.Err; err != nil {
|
||||
Debug.Printf("[mdns] Error for query type %s name %s: %s",
|
||||
dns.TypeToString[qtype], name, err)
|
||||
return nil, err
|
||||
}
|
||||
Debug.Printf("[mdns] Got response name for query type %s name %s: name %s, addr %s",
|
||||
dns.TypeToString[qtype], name, resp.Name, resp.Addr)
|
||||
return resp, nil
|
||||
}
|
||||
return nil, LookupError(name)
|
||||
}
|
||||
|
||||
func (client *MDNSClient) LookupName(name string) (net.IP, error) {
|
||||
if r, e := mdnsLookup(client, name, dns.TypeA); r != nil {
|
||||
return r.Addr, nil
|
||||
} else {
|
||||
return nil, e
|
||||
}
|
||||
}
|
||||
|
||||
func (client *MDNSClient) LookupInaddr(inaddr string) (string, error) {
|
||||
if r, e := mdnsLookup(client, inaddr, dns.TypePTR); r != nil {
|
||||
return r.Name, nil
|
||||
} else {
|
||||
return "", e
|
||||
}
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
package nameserver
|
||||
|
||||
import (
|
||||
"github.com/miekg/dns"
|
||||
. "github.com/weaveworks/weave/common"
|
||||
"net"
|
||||
)
|
||||
|
||||
type MDNSServer struct {
|
||||
localAddrs []net.Addr
|
||||
sendconn *net.UDPConn
|
||||
srv *dns.Server
|
||||
zone Zone
|
||||
}
|
||||
|
||||
func NewMDNSServer(zone Zone) (*MDNSServer, error) {
|
||||
// This is a bit of a kludge - per the RFC we should send responses from 5353, but that doesn't seem to work
|
||||
sendconn, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4zero, Port: 0})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &MDNSServer{sendconn: sendconn, zone: zone}, nil
|
||||
}
|
||||
|
||||
// Return true if testaddr is a UDP address with IP matching my local i/f
|
||||
func (s *MDNSServer) addrIsLocal(testaddr net.Addr) bool {
|
||||
if udpaddr, ok := testaddr.(*net.UDPAddr); ok {
|
||||
for _, localaddr := range s.localAddrs {
|
||||
if ipnetlocal, ok := localaddr.(*net.IPNet); ok {
|
||||
if ipnetlocal.IP.Equal(udpaddr.IP) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *MDNSServer) Start(ifi *net.Interface, localDomain string) error {
|
||||
conn, err := LinkLocalMulticastListener(ifi)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ifi == nil {
|
||||
s.localAddrs, err = net.InterfaceAddrs()
|
||||
} else {
|
||||
s.localAddrs, err = ifi.Addrs()
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
handleLocal := s.makeHandler(dns.TypeA,
|
||||
func(zone Lookup, r *dns.Msg, q *dns.Question) *dns.Msg {
|
||||
if ip, err := zone.LookupName(q.Name); err == nil {
|
||||
return makeAddressReply(r, q, []net.IP{ip})
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
handleReverse := s.makeHandler(dns.TypePTR,
|
||||
func(zone Lookup, r *dns.Msg, q *dns.Question) *dns.Msg {
|
||||
if name, err := zone.LookupInaddr(q.Name); err == nil {
|
||||
return makePTRReply(r, q, []string{name})
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
mux := dns.NewServeMux()
|
||||
mux.HandleFunc(localDomain, handleLocal)
|
||||
mux.HandleFunc(RDNSDomain, handleReverse)
|
||||
|
||||
s.srv = &dns.Server{
|
||||
Listener: nil,
|
||||
PacketConn: conn,
|
||||
Handler: mux,
|
||||
}
|
||||
go s.srv.ActivateAndServe()
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *MDNSServer) Stop() error {
|
||||
return s.srv.Shutdown()
|
||||
}
|
||||
|
||||
type LookupFunc func(Lookup, *dns.Msg, *dns.Question) *dns.Msg
|
||||
|
||||
func (s *MDNSServer) makeHandler(qtype uint16, lookup LookupFunc) dns.HandlerFunc {
|
||||
return func(_ dns.ResponseWriter, r *dns.Msg) {
|
||||
// Handle only questions, ignore answers. We might also ignore
|
||||
// questions that arise locally (i.e., that come from an IP we
|
||||
// think is local), but in the interest of avoiding
|
||||
// complication, and easier testing, this is elided on the
|
||||
// assumption that the client wouldn't ask if it already knew
|
||||
// the answer, and if it does ask, it'll be happy to get an
|
||||
// answer.
|
||||
if len(r.Answer) == 0 && len(r.Question) > 0 {
|
||||
q := &r.Question[0]
|
||||
if q.Qtype == qtype {
|
||||
if m := lookup(s.zone, r, q); m != nil {
|
||||
Debug.Printf("[mdns msgid %d] Found local answer to mDNS query %s",
|
||||
r.MsgHdr.Id, q.Name)
|
||||
if err := s.sendResponse(m); err != nil {
|
||||
Warning.Printf("[mdns msgid %d] Error writing to %v",
|
||||
r.MsgHdr.Id, s.sendconn)
|
||||
}
|
||||
} else {
|
||||
Debug.Printf("[mdns msgid %d] No local answer for mDNS query %s",
|
||||
r.MsgHdr.Id, q.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MDNSServer) sendResponse(m *dns.Msg) error {
|
||||
buf, err := m.Pack()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = s.sendconn.WriteTo(buf, ipv4Addr)
|
||||
return err
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
package nameserver
|
||||
|
||||
import (
|
||||
"github.com/miekg/dns"
|
||||
"github.com/weaveworks/weave/common"
|
||||
wt "github.com/weaveworks/weave/testing"
|
||||
"log"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
containerID = "deadbeef"
|
||||
testName = "test.weave.local."
|
||||
testAddr1 = "10.2.2.1/24"
|
||||
testInAddr1 = "1.2.2.10.in-addr.arpa."
|
||||
)
|
||||
|
||||
func sendQuery(name string, querytype uint16) error {
|
||||
m := new(dns.Msg)
|
||||
m.SetQuestion(name, querytype)
|
||||
m.RecursionDesired = false
|
||||
buf, err := m.Pack()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
conn, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4zero, Port: 0})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = conn.WriteTo(buf, ipv4Addr)
|
||||
return err
|
||||
}
|
||||
|
||||
func TestServerSimpleQuery(t *testing.T) {
|
||||
// The ff can be handy for debugging (obvs)
|
||||
common.InitDefaultLogging(true)
|
||||
|
||||
log.Println("TestServerSimpleQuery starting")
|
||||
var zone = new(ZoneDb)
|
||||
ip, _, _ := net.ParseCIDR(testAddr1)
|
||||
zone.AddRecord(containerID, testName, ip)
|
||||
|
||||
var receivedAddr net.IP
|
||||
var receivedName string
|
||||
var recvChan chan interface{}
|
||||
receivedCount := 0
|
||||
|
||||
reset := func() {
|
||||
receivedAddr = nil
|
||||
receivedName = ""
|
||||
receivedCount = 0
|
||||
recvChan = make(chan interface{})
|
||||
}
|
||||
|
||||
wait := func() {
|
||||
select {
|
||||
case <-recvChan:
|
||||
return
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Implement a minimal listener for responses
|
||||
multicast, err := LinkLocalMulticastListener(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to run test server: %s. No default multicast interface?", err)
|
||||
}
|
||||
|
||||
handleMDNS := func(w dns.ResponseWriter, r *dns.Msg) {
|
||||
// Only handle responses here
|
||||
if len(r.Answer) > 0 {
|
||||
for _, answer := range r.Answer {
|
||||
switch rr := answer.(type) {
|
||||
case *dns.A:
|
||||
receivedAddr = rr.A
|
||||
receivedCount++
|
||||
case *dns.PTR:
|
||||
receivedName = rr.Ptr
|
||||
receivedCount++
|
||||
}
|
||||
}
|
||||
recvChan <- "ok"
|
||||
}
|
||||
}
|
||||
|
||||
listener := &dns.Server{
|
||||
Unsafe: true,
|
||||
PacketConn: multicast,
|
||||
Handler: dns.HandlerFunc(handleMDNS),
|
||||
ReadTimeout: 100 * time.Millisecond}
|
||||
go listener.ActivateAndServe()
|
||||
defer listener.Shutdown()
|
||||
|
||||
mdnsServer, err := NewMDNSServer(zone)
|
||||
wt.AssertNoErr(t, err)
|
||||
err = mdnsServer.Start(nil, DefaultLocalDomain)
|
||||
wt.AssertNoErr(t, err)
|
||||
|
||||
time.Sleep(100 * time.Millisecond) // Allow for server to get going
|
||||
|
||||
reset()
|
||||
sendQuery(testName, dns.TypeA)
|
||||
wait()
|
||||
|
||||
if receivedCount != 1 {
|
||||
t.Fatalf("Unexpected result count %d for %s", receivedCount, testName)
|
||||
}
|
||||
if !receivedAddr.Equal(ip) {
|
||||
t.Fatalf("Unexpected result %s for %s", receivedAddr, testName)
|
||||
}
|
||||
|
||||
reset()
|
||||
sendQuery("testfail.weave.", dns.TypeA)
|
||||
wait()
|
||||
|
||||
if receivedCount != 0 {
|
||||
t.Fatalf("Unexpected result count %d for testfail.weave", receivedCount)
|
||||
}
|
||||
|
||||
reset()
|
||||
sendQuery(testInAddr1, dns.TypePTR)
|
||||
wait()
|
||||
|
||||
if receivedCount != 1 {
|
||||
t.Fatalf("Expected an answer to %s, got %d answers", testInAddr1, receivedCount)
|
||||
} else if !(testName == receivedName) {
|
||||
t.Fatalf("Expected answer %s to query for %s, got %s", testName, testInAddr1, receivedName)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
package nameserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
|
||||
. "github.com/weaveworks/weave/common"
|
||||
"github.com/weaveworks/weave/mesh"
|
||||
"github.com/weaveworks/weave/net/address"
|
||||
)
|
||||
|
||||
const (
|
||||
// Tombstones do not need to survive long periods of peer disconnection, as
|
||||
// we delete entries for disconnected peers. Therefore they just need to hang
|
||||
// around to account for propagation delay through gossip. We do need them
|
||||
// to hang around longer than the allowed clock skew (defined next). 30mins
|
||||
// tombstone timeout allows for 15mins max clock skew.
|
||||
tombstoneTimeout = time.Minute * 30
|
||||
|
||||
// Maximum age of acceptable gossip messages (to account for clock skew)
|
||||
gossipWindow = int64(tombstoneTimeout/time.Second) / 2
|
||||
|
||||
// Used by prog/weaver/main.go and proxy/create_container_interceptor.go
|
||||
DefaultDomain = "weave.local."
|
||||
)
|
||||
|
||||
// Nameserver: gossip-based, in memory nameserver.
|
||||
// - Holds a sorted list of (hostname, peer, container id, ip) tuples for the whole cluster.
|
||||
// - This list is gossiped & merged around the cluser.
|
||||
// - Lookup-by-hostname are O(nlogn), and return a (copy of a) slice of the entries
|
||||
// - Update is O(n) for now
|
||||
type Nameserver struct {
|
||||
sync.RWMutex
|
||||
ourName mesh.PeerName
|
||||
domain string
|
||||
gossip mesh.Gossip
|
||||
entries Entries
|
||||
isKnownPeer func(mesh.PeerName) bool
|
||||
quit chan struct{}
|
||||
}
|
||||
|
||||
func New(ourName mesh.PeerName, domain string, isKnownPeer func(mesh.PeerName) bool) *Nameserver {
|
||||
return &Nameserver{
|
||||
ourName: ourName,
|
||||
domain: dns.Fqdn(domain),
|
||||
isKnownPeer: isKnownPeer,
|
||||
quit: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Nameserver) SetGossip(gossip mesh.Gossip) {
|
||||
n.gossip = gossip
|
||||
}
|
||||
|
||||
func (n *Nameserver) Start() {
|
||||
go func() {
|
||||
ticker := time.Tick(tombstoneTimeout)
|
||||
for {
|
||||
select {
|
||||
case <-n.quit:
|
||||
return
|
||||
case <-ticker:
|
||||
n.deleteTombstones()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (n *Nameserver) Stop() {
|
||||
n.quit <- struct{}{}
|
||||
}
|
||||
|
||||
func (n *Nameserver) broadcastEntries(es ...Entry) error {
|
||||
if n.gossip == nil || len(es) == 0 {
|
||||
return nil
|
||||
}
|
||||
return n.gossip.GossipBroadcast(&GossipData{
|
||||
Entries: Entries(es),
|
||||
Timestamp: now(),
|
||||
})
|
||||
}
|
||||
|
||||
func (n *Nameserver) AddEntry(hostname, containerid string, origin mesh.PeerName, addr address.Address) error {
|
||||
n.infof("adding entry %s -> %s", hostname, addr.String())
|
||||
n.Lock()
|
||||
entry := n.entries.add(hostname, containerid, origin, addr)
|
||||
n.Unlock()
|
||||
return n.broadcastEntries(entry)
|
||||
}
|
||||
|
||||
func (n *Nameserver) Lookup(hostname string) []address.Address {
|
||||
n.RLock()
|
||||
defer n.RUnlock()
|
||||
|
||||
entries := n.entries.lookup(hostname)
|
||||
result := []address.Address{}
|
||||
for _, e := range entries {
|
||||
if e.Tombstone > 0 {
|
||||
continue
|
||||
}
|
||||
result = append(result, e.Addr)
|
||||
}
|
||||
n.debugf("lookup %s -> %s", hostname, &result)
|
||||
return result
|
||||
}
|
||||
|
||||
func (n *Nameserver) ReverseLookup(ip address.Address) (string, error) {
|
||||
n.RLock()
|
||||
defer n.RUnlock()
|
||||
|
||||
match, err := n.entries.first(func(e *Entry) bool {
|
||||
return e.Tombstone == 0 && e.Addr == ip
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
n.debugf("reverse lookup %s -> %s", ip, match.Hostname)
|
||||
return match.Hostname, nil
|
||||
}
|
||||
|
||||
func (n *Nameserver) ContainerStarted(ident string) {}
|
||||
|
||||
func (n *Nameserver) ContainerDied(ident string) {
|
||||
n.Lock()
|
||||
entries := n.entries.tombstone(n.ourName, func(e *Entry) bool {
|
||||
if e.ContainerID == ident {
|
||||
n.infof("container %s died; tombstoning entry %s", ident, e.String())
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
n.Unlock()
|
||||
if err := n.broadcastEntries(entries...); err != nil {
|
||||
n.errorf("failed to broadcast container %s death: %v", ident, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (n *Nameserver) PeerGone(peer mesh.PeerName) {
|
||||
n.infof("peer %s gone", peer.String())
|
||||
n.Lock()
|
||||
defer n.Unlock()
|
||||
n.entries.filter(func(e *Entry) bool {
|
||||
return e.Origin != peer
|
||||
})
|
||||
}
|
||||
|
||||
func (n *Nameserver) Delete(hostname, containerid, ipStr string, ip address.Address) error {
|
||||
n.Lock()
|
||||
n.infof("tombstoning hostname=%s, container=%s, ip=%s", hostname, containerid, ipStr)
|
||||
entries := n.entries.tombstone(n.ourName, func(e *Entry) bool {
|
||||
if hostname != "*" && e.Hostname != hostname {
|
||||
return false
|
||||
}
|
||||
|
||||
if containerid != "*" && e.ContainerID != containerid {
|
||||
return false
|
||||
}
|
||||
|
||||
if ipStr != "*" && e.Addr != ip {
|
||||
return false
|
||||
}
|
||||
|
||||
n.infof("tombstoning entry %v", e)
|
||||
return true
|
||||
})
|
||||
n.Unlock()
|
||||
return n.broadcastEntries(entries...)
|
||||
}
|
||||
|
||||
func (n *Nameserver) deleteTombstones() {
|
||||
n.Lock()
|
||||
defer n.Unlock()
|
||||
now := time.Now().Unix()
|
||||
n.entries.filter(func(e *Entry) bool {
|
||||
return e.Tombstone == 0 || now-e.Tombstone <= int64(tombstoneTimeout/time.Second)
|
||||
})
|
||||
}
|
||||
|
||||
func (n *Nameserver) Gossip() mesh.GossipData {
|
||||
n.RLock()
|
||||
defer n.RUnlock()
|
||||
gossip := &GossipData{
|
||||
Entries: make(Entries, len(n.entries)),
|
||||
Timestamp: now(),
|
||||
}
|
||||
copy(gossip.Entries, n.entries)
|
||||
return gossip
|
||||
}
|
||||
|
||||
func (n *Nameserver) OnGossipUnicast(sender mesh.PeerName, msg []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *Nameserver) receiveGossip(msg []byte) (mesh.GossipData, mesh.GossipData, error) {
|
||||
var gossip GossipData
|
||||
if err := gossip.Decode(msg); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if delta := gossip.Timestamp - now(); delta > gossipWindow || delta < -gossipWindow {
|
||||
return nil, nil, fmt.Errorf("clock skew of %d detected", delta)
|
||||
}
|
||||
|
||||
n.Lock()
|
||||
defer n.Unlock()
|
||||
|
||||
gossip.Entries.filter(func(e *Entry) bool {
|
||||
return n.isKnownPeer(e.Origin)
|
||||
})
|
||||
|
||||
newEntries := n.entries.merge(gossip.Entries)
|
||||
if len(newEntries) > 0 {
|
||||
return &GossipData{Entries: newEntries, Timestamp: now()}, &gossip, nil
|
||||
}
|
||||
return nil, &gossip, nil
|
||||
}
|
||||
|
||||
// merge received data into state and return "everything new I've
|
||||
// just learnt", or nil if nothing in the received data was new
|
||||
func (n *Nameserver) OnGossip(msg []byte) (mesh.GossipData, error) {
|
||||
newEntries, _, err := n.receiveGossip(msg)
|
||||
return newEntries, err
|
||||
}
|
||||
|
||||
// merge received data into state and return a representation of
|
||||
// the received data, for further propagation
|
||||
func (n *Nameserver) OnGossipBroadcast(_ mesh.PeerName, msg []byte) (mesh.GossipData, error) {
|
||||
_, entries, err := n.receiveGossip(msg)
|
||||
return entries, err
|
||||
}
|
||||
|
||||
func (n *Nameserver) infof(fmt string, args ...interface{}) {
|
||||
Log.Infof("[nameserver %s] "+fmt, append([]interface{}{n.ourName}, args...)...)
|
||||
}
|
||||
func (n *Nameserver) debugf(fmt string, args ...interface{}) {
|
||||
Log.Debugf("[nameserver %s] "+fmt, append([]interface{}{n.ourName}, args...)...)
|
||||
}
|
||||
func (n *Nameserver) errorf(fmt string, args ...interface{}) {
|
||||
Log.Errorf("[nameserver %s] "+fmt, append([]interface{}{n.ourName}, args...)...)
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
package nameserver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/weaveworks/weave/mesh"
|
||||
"github.com/weaveworks/weave/net/address"
|
||||
"github.com/weaveworks/weave/testing/gossip"
|
||||
)
|
||||
|
||||
func makeNameserver(name mesh.PeerName) *Nameserver {
|
||||
return New(name, "", func(mesh.PeerName) bool { return true })
|
||||
}
|
||||
|
||||
func makeNetwork(size int) ([]*Nameserver, *gossip.TestRouter) {
|
||||
gossipRouter := gossip.NewTestRouter(0.0)
|
||||
nameservers := make([]*Nameserver, size)
|
||||
|
||||
for i := 0; i < size; i++ {
|
||||
name, _ := mesh.PeerNameFromString(fmt.Sprintf("%02d:00:00:02:00:00", i))
|
||||
nameserver := makeNameserver(name)
|
||||
nameserver.SetGossip(gossipRouter.Connect(nameserver.ourName, nameserver))
|
||||
nameserver.Start()
|
||||
nameservers[i] = nameserver
|
||||
}
|
||||
|
||||
return nameservers, gossipRouter
|
||||
}
|
||||
|
||||
func stopNetwork(nameservers []*Nameserver, grouter *gossip.TestRouter) {
|
||||
for _, nameserver := range nameservers {
|
||||
nameserver.Stop()
|
||||
}
|
||||
grouter.Stop()
|
||||
}
|
||||
|
||||
type pair struct {
|
||||
origin mesh.PeerName
|
||||
addr address.Address
|
||||
}
|
||||
|
||||
type mapping struct {
|
||||
hostname string
|
||||
addrs []pair
|
||||
}
|
||||
|
||||
type addrs []address.Address
|
||||
|
||||
func (a addrs) Len() int { return len(a) }
|
||||
func (a addrs) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a addrs) Less(i, j int) bool { return a[i] < a[j] }
|
||||
func (a addrs) String() string {
|
||||
ss := []string{}
|
||||
for _, addr := range a {
|
||||
ss = append(ss, addr.String())
|
||||
}
|
||||
return strings.Join(ss, " ")
|
||||
}
|
||||
|
||||
func (m mapping) Addrs() []address.Address {
|
||||
want := addrs{}
|
||||
for _, p := range m.addrs {
|
||||
want = append(want, p.addr)
|
||||
}
|
||||
sort.Sort(want)
|
||||
return want
|
||||
}
|
||||
|
||||
func TestNameservers(t *testing.T) {
|
||||
//common.SetLogLevel("debug")
|
||||
|
||||
lookupTimeout := 10 // ms
|
||||
nameservers, grouter := makeNetwork(30)
|
||||
defer stopNetwork(nameservers, grouter)
|
||||
// This subset will sometimes lose touch with some of the others
|
||||
badNameservers := nameservers[25:]
|
||||
// This subset will remain well-connected, and we will deal mainly with them
|
||||
nameservers = nameservers[:25]
|
||||
nameserversByName := map[mesh.PeerName]*Nameserver{}
|
||||
for _, n := range nameservers {
|
||||
nameserversByName[n.ourName] = n
|
||||
}
|
||||
mappings := []mapping{}
|
||||
|
||||
check := func(nameserver *Nameserver, expected mapping) {
|
||||
have := []address.Address{}
|
||||
for i := 0; i < lookupTimeout; i++ {
|
||||
have = nameserver.Lookup(expected.hostname)
|
||||
sort.Sort(addrs(have))
|
||||
if reflect.DeepEqual(have, expected.Addrs()) {
|
||||
return
|
||||
}
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
}
|
||||
want := expected.Addrs()
|
||||
require.Equal(t, addrs(want).String(), addrs(have).String())
|
||||
}
|
||||
|
||||
addMapping := func() {
|
||||
nameserver := nameservers[rand.Intn(len(nameservers))]
|
||||
addr := address.Address(rand.Int31())
|
||||
// Create a hostname which has some upper and lowercase letters,
|
||||
// and a unique number so we don't have to check if we allocated it already
|
||||
randomBits := rand.Int63()
|
||||
firstLetter := 'H' + (randomBits&1)*32
|
||||
secondLetter := 'O' + (randomBits&2)*16
|
||||
randomBits = randomBits >> 2
|
||||
hostname := fmt.Sprintf("%c%cstname%d", firstLetter, secondLetter, randomBits)
|
||||
mapping := mapping{hostname, []pair{{nameserver.ourName, addr}}}
|
||||
mappings = append(mappings, mapping)
|
||||
|
||||
require.Nil(t, nameserver.AddEntry(hostname, "", nameserver.ourName, addr))
|
||||
check(nameserver, mapping)
|
||||
}
|
||||
|
||||
addExtraMapping := func() {
|
||||
if len(mappings) <= 0 {
|
||||
return
|
||||
}
|
||||
nameserver := nameservers[rand.Intn(len(nameservers))]
|
||||
i := rand.Intn(len(mappings))
|
||||
mapping := mappings[i]
|
||||
addr := address.Address(rand.Int31())
|
||||
mapping.addrs = append(mapping.addrs, pair{nameserver.ourName, addr})
|
||||
mappings[i] = mapping
|
||||
|
||||
require.Nil(t, nameserver.AddEntry(mapping.hostname, "", nameserver.ourName, addr))
|
||||
check(nameserver, mapping)
|
||||
}
|
||||
|
||||
loseConnection := func() {
|
||||
nameserver1 := badNameservers[rand.Intn(len(badNameservers))]
|
||||
nameserver2 := nameservers[rand.Intn(len(nameservers))]
|
||||
nameserver1.PeerGone(nameserver2.ourName)
|
||||
}
|
||||
|
||||
deleteMapping := func() {
|
||||
if len(mappings) <= 0 {
|
||||
return
|
||||
}
|
||||
i := rand.Intn(len(mappings))
|
||||
mapping := mappings[i]
|
||||
if len(mapping.addrs) <= 0 {
|
||||
return
|
||||
}
|
||||
j := rand.Intn(len(mapping.addrs))
|
||||
pair := mapping.addrs[j]
|
||||
mapping.addrs = append(mapping.addrs[:j], mapping.addrs[j+1:]...)
|
||||
mappings[i] = mapping
|
||||
nameserver := nameserversByName[pair.origin]
|
||||
|
||||
require.Nil(t, nameserver.Delete(mapping.hostname, "*", pair.addr.String(), pair.addr))
|
||||
check(nameserver, mapping)
|
||||
}
|
||||
|
||||
doLookup := func() {
|
||||
if len(mappings) <= 0 {
|
||||
return
|
||||
}
|
||||
mapping := mappings[rand.Intn(len(mappings))]
|
||||
nameserver := nameservers[rand.Intn(len(nameservers))]
|
||||
check(nameserver, mapping)
|
||||
}
|
||||
|
||||
doReverseLookup := func() {
|
||||
if len(mappings) <= 0 {
|
||||
return
|
||||
}
|
||||
mapping := mappings[rand.Intn(len(mappings))]
|
||||
if len(mapping.addrs) <= 0 {
|
||||
return
|
||||
}
|
||||
nameserver := nameservers[rand.Intn(len(nameservers))]
|
||||
hostname := ""
|
||||
var err error
|
||||
for i := 0; i < lookupTimeout; i++ {
|
||||
hostname, err = nameserver.ReverseLookup(mapping.addrs[0].addr)
|
||||
if err != nil && mapping.hostname == hostname {
|
||||
return
|
||||
}
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
}
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, mapping.hostname, hostname)
|
||||
}
|
||||
|
||||
for i := 0; i < 800; i++ {
|
||||
r := rand.Float32()
|
||||
switch {
|
||||
case r < 0.1:
|
||||
addMapping()
|
||||
|
||||
case 0.1 <= r && r < 0.2:
|
||||
addExtraMapping()
|
||||
|
||||
case 0.2 <= r && r < 0.3:
|
||||
deleteMapping()
|
||||
|
||||
case 0.3 <= r && r < 0.35:
|
||||
loseConnection()
|
||||
|
||||
case 0.35 <= r && r < 0.9:
|
||||
doLookup()
|
||||
|
||||
case 0.9 <= r:
|
||||
doReverseLookup()
|
||||
}
|
||||
|
||||
grouter.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
func TestContainerAndPeerDeath(t *testing.T) {
|
||||
peername, err := mesh.PeerNameFromString("00:00:00:02:00:00")
|
||||
require.Nil(t, err)
|
||||
nameserver := makeNameserver(peername)
|
||||
|
||||
err = nameserver.AddEntry("hostname", "containerid", peername, address.Address(0))
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, []address.Address{0}, nameserver.Lookup("hostname"))
|
||||
|
||||
nameserver.ContainerDied("containerid")
|
||||
require.Equal(t, []address.Address{}, nameserver.Lookup("hostname"))
|
||||
|
||||
err = nameserver.AddEntry("hostname", "containerid", peername, address.Address(0))
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, []address.Address{0}, nameserver.Lookup("hostname"))
|
||||
|
||||
nameserver.PeerGone(peername)
|
||||
require.Equal(t, []address.Address{}, nameserver.Lookup("hostname"))
|
||||
}
|
||||
|
||||
func TestTombstoneDeletion(t *testing.T) {
|
||||
oldNow := now
|
||||
defer func() { now = oldNow }()
|
||||
now = func() int64 { return 1234 }
|
||||
|
||||
peername, err := mesh.PeerNameFromString("00:00:00:02:00:00")
|
||||
require.Nil(t, err)
|
||||
nameserver := makeNameserver(peername)
|
||||
|
||||
err = nameserver.AddEntry("hostname", "containerid", peername, address.Address(0))
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, []address.Address{0}, nameserver.Lookup("hostname"))
|
||||
|
||||
nameserver.deleteTombstones()
|
||||
require.Equal(t, []address.Address{0}, nameserver.Lookup("hostname"))
|
||||
|
||||
err = nameserver.Delete("hostname", "containerid", "", address.Address(0))
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, []address.Address{}, nameserver.Lookup("hostname"))
|
||||
require.Equal(t, l(Entries{Entry{
|
||||
ContainerID: "containerid",
|
||||
Origin: peername,
|
||||
Addr: address.Address(0),
|
||||
Hostname: "hostname",
|
||||
Version: 1,
|
||||
Tombstone: 1234,
|
||||
}}), nameserver.entries)
|
||||
|
||||
now = func() int64 { return 1234 + int64(tombstoneTimeout/time.Second) + 1 }
|
||||
nameserver.deleteTombstones()
|
||||
require.Equal(t, Entries{}, nameserver.entries)
|
||||
}
|
||||
@@ -1,365 +0,0 @@
|
||||
package nameserver
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/miekg/dns"
|
||||
. "github.com/weaveworks/weave/common"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultLocalDomain = "weave.local." // The default name used for the local domain
|
||||
DefaultServerPort = 53 // The default server port
|
||||
DefaultCLICfgFile = "/etc/resolv.conf" // default "resolv.conf" file to try to load
|
||||
DefaultUDPBuflen = 4096 // bigger than the default 512
|
||||
DefaultCacheLen = 8192 // default cache capacity
|
||||
DefaultResolvWorkers = 8 // default number of resolution workers
|
||||
DefaultTimeout = 5 // default timeout for DNS resolutions
|
||||
)
|
||||
|
||||
type DNSServerConfig struct {
|
||||
// (Optional) client config file for resolving upstream servers
|
||||
UpstreamCfgFile string
|
||||
// (Optional) DNS client config for the fallback server(s)
|
||||
UpstreamCfg *dns.ClientConfig
|
||||
// (Optional) port number (for TCP and UDP)
|
||||
Port int
|
||||
// (Optional) local domain (ie, "weave.local.")
|
||||
LocalDomain string
|
||||
// (Optional) cache size
|
||||
CacheLen int
|
||||
// (Optional) timeout for DNS queries
|
||||
Timeout int
|
||||
// (Optional) UDP buffer length
|
||||
UDPBufLen int
|
||||
}
|
||||
|
||||
type dnsProtocol uint8
|
||||
|
||||
const (
|
||||
protUDP dnsProtocol = iota // UDP protocol
|
||||
protTCP dnsProtocol = iota // TCP protocol
|
||||
)
|
||||
|
||||
func (proto dnsProtocol) String() string {
|
||||
switch proto {
|
||||
case protUDP:
|
||||
return "UDP"
|
||||
case protTCP:
|
||||
return "TCP"
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// get a new dns.Client for a protocol
|
||||
func (proto dnsProtocol) GetNewClient(bufsize int) *dns.Client {
|
||||
switch proto {
|
||||
case protTCP:
|
||||
return &dns.Client{Net: "tcp"}
|
||||
case protUDP:
|
||||
return &dns.Client{Net: "udp", UDPSize: uint16(bufsize)}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type DNSServer struct {
|
||||
Zone Zone
|
||||
Iface *net.Interface
|
||||
Upstream *dns.ClientConfig
|
||||
udpSrv *dns.Server
|
||||
tcpSrv *dns.Server
|
||||
mdnsCli *MDNSClient
|
||||
mdnsSrv *MDNSServer
|
||||
cache *Cache
|
||||
timeout int
|
||||
udpBuf int
|
||||
listenersWg *sync.WaitGroup
|
||||
|
||||
Domain string // the local domain
|
||||
ListenAddr string // the address the server is listening at
|
||||
}
|
||||
|
||||
// Creates a new DNS server
|
||||
func NewDNSServer(config DNSServerConfig, zone Zone, iface *net.Interface) (s *DNSServer, err error) {
|
||||
s = &DNSServer{
|
||||
Zone: zone,
|
||||
Iface: iface,
|
||||
listenersWg: new(sync.WaitGroup),
|
||||
|
||||
Domain: DefaultLocalDomain,
|
||||
ListenAddr: fmt.Sprintf(":%d", DefaultServerPort),
|
||||
}
|
||||
|
||||
// fill empty parameters with defaults...
|
||||
if config.Port != 0 {
|
||||
s.ListenAddr = fmt.Sprintf(":%d", config.Port)
|
||||
}
|
||||
if len(config.LocalDomain) > 0 {
|
||||
s.Domain = config.LocalDomain
|
||||
}
|
||||
if config.UpstreamCfg != nil {
|
||||
s.Upstream = config.UpstreamCfg
|
||||
} else {
|
||||
cfgFile := DefaultCLICfgFile
|
||||
if len(config.UpstreamCfgFile) > 0 {
|
||||
cfgFile = config.UpstreamCfgFile
|
||||
}
|
||||
if s.Upstream, err = dns.ClientConfigFromFile(cfgFile); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if config.Timeout > 0 {
|
||||
s.timeout = config.Timeout
|
||||
}
|
||||
if config.UDPBufLen > 0 {
|
||||
s.udpBuf = config.UDPBufLen
|
||||
}
|
||||
s.mdnsCli, err = NewMDNSClient()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
s.mdnsSrv, err = NewMDNSServer(s.Zone)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
cacheLen := DefaultCacheLen
|
||||
if config.CacheLen > 0 {
|
||||
cacheLen = config.CacheLen
|
||||
}
|
||||
Debug.Printf("[dns] Initializing cache: %d entries", config.CacheLen)
|
||||
s.cache, err = NewCache(cacheLen)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// create two DNS request multiplexerers, depending on the protocol used by clients
|
||||
// (we use the same protocol for asking upstream servers)
|
||||
mux := func(proto dnsProtocol) *dns.ServeMux {
|
||||
m := dns.NewServeMux()
|
||||
m.HandleFunc(s.Domain, s.queryHandler(proto))
|
||||
m.HandleFunc(RDNSDomain, s.rdnsHandler(proto))
|
||||
m.HandleFunc(".", s.notUsHandler(proto))
|
||||
return m
|
||||
}
|
||||
|
||||
s.udpSrv = &dns.Server{Addr: s.ListenAddr, Net: "udp", Handler: mux(protUDP)}
|
||||
s.tcpSrv = &dns.Server{Addr: s.ListenAddr, Net: "tcp", Handler: mux(protTCP)}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Start the DNS server
|
||||
func (s *DNSServer) Start() error {
|
||||
Info.Printf("Using mDNS on %v", s.Iface)
|
||||
err := s.mdnsCli.Start(s.Iface)
|
||||
CheckFatal(err)
|
||||
err = s.mdnsSrv.Start(s.Iface, s.Domain)
|
||||
CheckFatal(err)
|
||||
|
||||
s.listenersWg.Add(2)
|
||||
|
||||
go func() {
|
||||
defer s.listenersWg.Done()
|
||||
|
||||
Debug.Printf("Listening for DNS on %s (UDP)", s.ListenAddr)
|
||||
err = s.udpSrv.ListenAndServe()
|
||||
CheckFatal(err)
|
||||
Debug.Printf("DNS UDP server exiting...")
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer s.listenersWg.Done()
|
||||
|
||||
Debug.Printf("Listening for DNS on %s (TCP)", s.ListenAddr)
|
||||
err = s.tcpSrv.ListenAndServe()
|
||||
CheckFatal(err)
|
||||
Debug.Printf("DNS TCP server exiting...")
|
||||
}()
|
||||
|
||||
// Waiting for all goroutines to finish (otherwise they die as main routine dies)
|
||||
s.listenersWg.Wait()
|
||||
|
||||
Info.Printf("WeaveDNS server exiting...")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Return status string
|
||||
func (s *DNSServer) Status() string {
|
||||
var buf bytes.Buffer
|
||||
fmt.Fprintln(&buf, "Local domain", s.Domain)
|
||||
fmt.Fprintln(&buf, "Listen address", s.ListenAddr)
|
||||
fmt.Fprintln(&buf, "mDNS interface", s.Iface)
|
||||
fmt.Fprintln(&buf, "Fallback DNS config", s.Upstream)
|
||||
fmt.Fprintf(&buf, "Zone database:\n%s", s.Zone)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// Perform a graceful shutdown
|
||||
func (s *DNSServer) Stop() error {
|
||||
// Stop the listeners/handlers
|
||||
if err := s.tcpSrv.Shutdown(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.udpSrv.Shutdown(); err != nil {
|
||||
return err
|
||||
}
|
||||
s.listenersWg.Wait()
|
||||
|
||||
// shutdown the mDNS server
|
||||
s.mdnsSrv.Stop()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *DNSServer) queryHandler(proto dnsProtocol) dns.HandlerFunc {
|
||||
return func(w dns.ResponseWriter, r *dns.Msg) {
|
||||
now := time.Now()
|
||||
q := r.Question[0]
|
||||
maxLen := getMaxReplyLen(r, proto)
|
||||
lookups := []Lookup{s.Zone, s.mdnsCli}
|
||||
Debug.Printf("Query: %+v", q)
|
||||
|
||||
reply, err := s.cache.Get(r, maxLen, time.Now())
|
||||
if err != nil {
|
||||
if err == errNoLocalReplies {
|
||||
Debug.Printf("[dns msgid %d] Cached 'no local replies' - skipping local lookup", r.MsgHdr.Id)
|
||||
lookups = []Lookup{s.Zone}
|
||||
} else {
|
||||
Debug.Printf("[dns msgid %d] Error from cache: %s", r.MsgHdr.Id, err)
|
||||
w.WriteMsg(makeDNSFailResponse(r))
|
||||
return
|
||||
}
|
||||
}
|
||||
if reply != nil {
|
||||
Debug.Printf("[dns msgid %d] Returning reply from cache: %s/%d answers",
|
||||
r.MsgHdr.Id, dns.RcodeToString[reply.MsgHdr.Rcode], len(reply.Answer))
|
||||
w.WriteMsg(reply)
|
||||
return
|
||||
}
|
||||
|
||||
// catch unsupported queries
|
||||
if q.Qtype != dns.TypeA {
|
||||
Debug.Printf("[dns msgid %d] Unsuported query type %s", r.MsgHdr.Id, dns.TypeToString[q.Qtype])
|
||||
m := makeDNSNotImplResponse(r)
|
||||
s.cache.Put(r, m, negLocalTTL, 0, now)
|
||||
w.WriteMsg(m)
|
||||
return
|
||||
}
|
||||
|
||||
for _, lookup := range lookups {
|
||||
if ip, err := lookup.LookupName(q.Name); err == nil {
|
||||
m := makeAddressReply(r, &q, []net.IP{ip})
|
||||
m.Authoritative = true
|
||||
|
||||
Debug.Printf("[dns msgid %d] Caching response for %s-query for \"%s\": %s [code:%s]",
|
||||
m.MsgHdr.Id, dns.TypeToString[q.Qtype], q.Name, ip, dns.RcodeToString[m.Rcode])
|
||||
s.cache.Put(r, m, nullTTL, 0, now)
|
||||
w.WriteMsg(m)
|
||||
return
|
||||
}
|
||||
now = time.Now()
|
||||
}
|
||||
|
||||
Info.Printf("[dns msgid %d] No results for type %s query %s [caching no-local]",
|
||||
r.MsgHdr.Id, dns.TypeToString[q.Qtype], q.Name)
|
||||
s.cache.Put(r, nil, negLocalTTL, CacheNoLocalReplies, now)
|
||||
w.WriteMsg(makeDNSFailResponse(r))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DNSServer) rdnsHandler(proto dnsProtocol) dns.HandlerFunc {
|
||||
fallback := s.notUsHandler(proto)
|
||||
return func(w dns.ResponseWriter, r *dns.Msg) {
|
||||
now := time.Now()
|
||||
q := r.Question[0]
|
||||
lookups := []Lookup{s.Zone, s.mdnsCli}
|
||||
maxLen := getMaxReplyLen(r, proto)
|
||||
|
||||
Debug.Printf("Reverse query: %+v", q)
|
||||
reply, err := s.cache.Get(r, maxLen, time.Now())
|
||||
if err != nil {
|
||||
if err == errNoLocalReplies {
|
||||
Debug.Printf("[dns msgid %d] Cached 'no local replies' - skipping local lookup", r.MsgHdr.Id)
|
||||
lookups = []Lookup{s.Zone}
|
||||
} else {
|
||||
Debug.Printf("[dns msgid %d] Error from cache: %s", r.MsgHdr.Id, err)
|
||||
w.WriteMsg(makeDNSFailResponse(r))
|
||||
return
|
||||
}
|
||||
}
|
||||
if reply != nil {
|
||||
Debug.Printf("[dns msgid %d] Returning reply from cache: %s/%d answers",
|
||||
r.MsgHdr.Id, dns.RcodeToString[reply.MsgHdr.Rcode], len(reply.Answer))
|
||||
w.WriteMsg(reply)
|
||||
return
|
||||
}
|
||||
|
||||
// catch unsupported queries
|
||||
if q.Qtype != dns.TypePTR {
|
||||
Warning.Printf("[dns msgid %d] Unexpected reverse query type %s: %+v",
|
||||
r.MsgHdr.Id, dns.TypeToString[q.Qtype], q)
|
||||
m := makeDNSNotImplResponse(r)
|
||||
s.cache.Put(r, m, negLocalTTL, 0, now)
|
||||
w.WriteMsg(m)
|
||||
return
|
||||
}
|
||||
|
||||
for _, lookup := range lookups {
|
||||
if name, err := lookup.LookupInaddr(q.Name); err == nil {
|
||||
m := makePTRReply(r, &q, []string{name})
|
||||
m.Authoritative = true
|
||||
|
||||
Debug.Printf("[dns msgid %d] Caching response for %s-query for \"%s\": %s [code:%s]",
|
||||
m.MsgHdr.Id, dns.TypeToString[q.Qtype], q.Name, name, dns.RcodeToString[m.Rcode])
|
||||
s.cache.Put(r, m, nullTTL, 0, now)
|
||||
w.WriteMsg(m)
|
||||
return
|
||||
}
|
||||
now = time.Now()
|
||||
}
|
||||
|
||||
Info.Printf("[dns msgid %d] No results for %s-query about '%s' [caching no-local] -> sending to fallback server",
|
||||
r.MsgHdr.Id, dns.TypeToString[q.Qtype], q.Name)
|
||||
s.cache.Put(r, nil, negLocalTTL, CacheNoLocalReplies, now)
|
||||
fallback(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// When we receive a request for a name outside of our '.weave.local.'
|
||||
// domain, ask the configured DNS server as a fallback.
|
||||
func (s *DNSServer) notUsHandler(proto dnsProtocol) dns.HandlerFunc {
|
||||
dnsClient := proto.GetNewClient(DefaultUDPBuflen)
|
||||
|
||||
return func(w dns.ResponseWriter, r *dns.Msg) {
|
||||
maxLen := getMaxReplyLen(r, proto)
|
||||
q := r.Question[0]
|
||||
|
||||
// create a request where we announce our max payload size
|
||||
rcopy := r
|
||||
rcopy.SetEdns0(uint16(maxLen), false)
|
||||
|
||||
Debug.Printf("[dns msgid %d] Fallback query: %+v [%s, max:%d bytes]", rcopy.MsgHdr.Id, q, proto, maxLen)
|
||||
for _, server := range s.Upstream.Servers {
|
||||
reply, _, err := dnsClient.Exchange(rcopy, fmt.Sprintf("%s:%s", server, s.Upstream.Port))
|
||||
if err != nil {
|
||||
Debug.Printf("[dns msgid %d] Network error trying %s (%s)",
|
||||
r.MsgHdr.Id, server, err)
|
||||
continue
|
||||
}
|
||||
if reply != nil && reply.Rcode != dns.RcodeSuccess {
|
||||
Debug.Printf("[dns msgid %d] Failure reported by %s for query %s",
|
||||
r.MsgHdr.Id, server, q.Name)
|
||||
continue
|
||||
}
|
||||
Debug.Printf("[dns msgid %d] Given answer by %s for query %s",
|
||||
r.MsgHdr.Id, server, q.Name)
|
||||
w.WriteMsg(reply)
|
||||
return
|
||||
}
|
||||
Warning.Printf("[dns msgid %d] Failed lookup for external name %s", r.MsgHdr.Id, q.Name)
|
||||
w.WriteMsg(makeDNSFailResponse(r))
|
||||
}
|
||||
}
|
||||
@@ -1,300 +0,0 @@
|
||||
package nameserver
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"github.com/miekg/dns"
|
||||
. "github.com/weaveworks/weave/common"
|
||||
wt "github.com/weaveworks/weave/testing"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
testRDNSsuccess = "1.2.2.10.in-addr.arpa."
|
||||
testRDNSfail = "4.3.2.1.in-addr.arpa."
|
||||
testRDNSnonlocal = "8.8.8.8.in-addr.arpa."
|
||||
testUDPBufSize = 16384
|
||||
)
|
||||
|
||||
var (
|
||||
testPort = 17625
|
||||
)
|
||||
|
||||
func setupForTest(t *testing.T) {
|
||||
// fail early if we cannot find a default multicast interface
|
||||
multicast, err := LinkLocalMulticastListener(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to create multicast listener: %s. No default multicast interface?", err)
|
||||
}
|
||||
multicast.Close()
|
||||
|
||||
testPort++
|
||||
}
|
||||
|
||||
func TestUDPDNSServer(t *testing.T) {
|
||||
setupForTest(t)
|
||||
const (
|
||||
successTestName = "test1.weave.local."
|
||||
failTestName = "test2.weave.local."
|
||||
nonLocalName = "weave.works."
|
||||
testAddr1 = "10.2.2.1"
|
||||
)
|
||||
testCIDR1 := testAddr1 + "/24"
|
||||
|
||||
InitDefaultLogging(true)
|
||||
var zone = new(ZoneDb)
|
||||
ip, _, _ := net.ParseCIDR(testCIDR1)
|
||||
zone.AddRecord(containerID, successTestName, ip)
|
||||
|
||||
fallbackHandler := func(w dns.ResponseWriter, req *dns.Msg) {
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(req)
|
||||
if len(req.Question) == 1 {
|
||||
q := req.Question[0]
|
||||
if q.Name == nonLocalName && q.Qtype == dns.TypeMX {
|
||||
m.Answer = make([]dns.RR, 1)
|
||||
m.Answer[0] = &dns.MX{Hdr: dns.RR_Header{Name: m.Question[0].Name, Rrtype: dns.TypeMX, Class: dns.ClassINET, Ttl: 0}, Mx: "mail." + nonLocalName}
|
||||
} else if q.Name == nonLocalName && q.Qtype == dns.TypeANY {
|
||||
m.Answer = make([]dns.RR, 512/len("mailn."+nonLocalName)+1)
|
||||
for i := range m.Answer {
|
||||
m.Answer[i] = &dns.MX{Hdr: dns.RR_Header{Name: m.Question[0].Name, Rrtype: dns.TypeMX, Class: dns.ClassINET, Ttl: 0}, Mx: fmt.Sprintf("mail%d.%s", i, nonLocalName)}
|
||||
}
|
||||
} else if q.Name == testRDNSnonlocal && q.Qtype == dns.TypePTR {
|
||||
m.Answer = make([]dns.RR, 1)
|
||||
m.Answer[0] = &dns.PTR{Hdr: dns.RR_Header{Name: m.Question[0].Name, Rrtype: dns.TypePTR, Class: dns.ClassINET, Ttl: 0}, Ptr: "ns1.google.com."}
|
||||
} else if q.Name == testRDNSfail && q.Qtype == dns.TypePTR {
|
||||
m.Rcode = dns.RcodeNameError
|
||||
}
|
||||
}
|
||||
w.WriteMsg(m)
|
||||
}
|
||||
|
||||
// Run another DNS server for fallback
|
||||
s, fallbackAddr, err := runLocalUDPServer(t, "127.0.0.1:0", fallbackHandler)
|
||||
wt.AssertNoErr(t, err)
|
||||
defer s.Shutdown()
|
||||
|
||||
_, fallbackPort, err := net.SplitHostPort(fallbackAddr)
|
||||
wt.AssertNoErr(t, err)
|
||||
|
||||
config := &dns.ClientConfig{Servers: []string{"127.0.0.1"}, Port: fallbackPort}
|
||||
srv, err := NewDNSServer(DNSServerConfig{UpstreamCfg: config, Port: testPort}, zone, nil)
|
||||
wt.AssertNoErr(t, err)
|
||||
defer srv.Stop()
|
||||
go srv.Start()
|
||||
time.Sleep(100 * time.Millisecond) // Allow sever goroutine to start
|
||||
|
||||
var r *dns.Msg
|
||||
|
||||
r = assertExchange(t, successTestName, dns.TypeA, 1, 1, 0)
|
||||
wt.AssertType(t, r.Answer[0], (*dns.A)(nil), "DNS record")
|
||||
wt.AssertEqualString(t, r.Answer[0].(*dns.A).A.String(), testAddr1, "IP address")
|
||||
|
||||
assertExchange(t, failTestName, dns.TypeA, 0, 0, dns.RcodeNameError)
|
||||
|
||||
r = assertExchange(t, testRDNSsuccess, dns.TypePTR, 1, 1, 0)
|
||||
wt.AssertType(t, r.Answer[0], (*dns.PTR)(nil), "DNS record")
|
||||
wt.AssertEqualString(t, r.Answer[0].(*dns.PTR).Ptr, successTestName, "IP address")
|
||||
|
||||
assertExchange(t, testRDNSfail, dns.TypePTR, 0, 0, dns.RcodeNameError)
|
||||
|
||||
// This should fail because we don't handle MX records
|
||||
assertExchange(t, successTestName, dns.TypeMX, 0, 0, dns.RcodeNotImplemented)
|
||||
|
||||
// This non-local query for an MX record should succeed by being
|
||||
// passed on to the fallback server
|
||||
assertExchange(t, nonLocalName, dns.TypeMX, 1, -1, 0)
|
||||
|
||||
// Now ask a query that we expect to return a lot of data.
|
||||
assertExchange(t, nonLocalName, dns.TypeANY, 5, -1, 0)
|
||||
|
||||
assertExchange(t, testRDNSnonlocal, dns.TypePTR, 1, -1, 0)
|
||||
|
||||
// Not testing MDNS functionality of server here (yet), since it
|
||||
// needs two servers, each listening on its own address
|
||||
}
|
||||
|
||||
func TestTCPDNSServer(t *testing.T) {
|
||||
setupForTest(t)
|
||||
const (
|
||||
numAnswers = 512
|
||||
nonLocalName = "weave.works."
|
||||
)
|
||||
dnsAddr := fmt.Sprintf("localhost:%d", testPort)
|
||||
|
||||
InitDefaultLogging(true)
|
||||
var zone = new(ZoneDb)
|
||||
|
||||
// generate a list of `numAnswers` IP addresses
|
||||
var addrs []net.IP
|
||||
bs := make([]byte, 4)
|
||||
for i := 0; i < numAnswers; i++ {
|
||||
binary.LittleEndian.PutUint32(bs, uint32(i))
|
||||
addrs = append(addrs, net.IPv4(bs[0], bs[1], bs[2], bs[3]))
|
||||
}
|
||||
|
||||
// handler for the fallback server: it will just return a very long response
|
||||
fallbackUDPHandler := func(w dns.ResponseWriter, req *dns.Msg) {
|
||||
maxLen := getMaxReplyLen(req, protUDP)
|
||||
|
||||
t.Logf("Fallback UDP server got asked: returning %d answers", numAnswers)
|
||||
q := req.Question[0]
|
||||
m := makeAddressReply(req, &q, addrs)
|
||||
mLen := m.Len()
|
||||
m.SetEdns0(uint16(maxLen), false)
|
||||
|
||||
if mLen > maxLen {
|
||||
t.Logf("... truncated response (%d > %d)", mLen, maxLen)
|
||||
m.Truncated = true
|
||||
}
|
||||
w.WriteMsg(m)
|
||||
}
|
||||
fallbackTCPHandler := func(w dns.ResponseWriter, req *dns.Msg) {
|
||||
t.Logf("Fallback TCP server got asked: returning %d answers", numAnswers)
|
||||
q := req.Question[0]
|
||||
m := makeAddressReply(req, &q, addrs)
|
||||
w.WriteMsg(m)
|
||||
}
|
||||
|
||||
t.Logf("Running a DNS fallback server with UDP")
|
||||
us, fallbackUDPAddr, err := runLocalUDPServer(t, "127.0.0.1:0", fallbackUDPHandler)
|
||||
wt.AssertNoErr(t, err)
|
||||
defer us.Shutdown()
|
||||
|
||||
_, fallbackPort, err := net.SplitHostPort(fallbackUDPAddr)
|
||||
wt.AssertNoErr(t, err)
|
||||
|
||||
t.Logf("Starting another fallback server, with TCP, on the same port as the UDP server")
|
||||
fallbackTCPAddr := fmt.Sprintf("127.0.0.1:%s", fallbackPort)
|
||||
ts, fallbackTCPAddr, err := runLocalTCPServer(t, fallbackTCPAddr, fallbackTCPHandler)
|
||||
wt.AssertNoErr(t, err)
|
||||
defer ts.Shutdown()
|
||||
|
||||
t.Logf("Creating a WeaveDNS server instance, falling back to 127.0.0.1:%s", fallbackPort)
|
||||
config := &dns.ClientConfig{Servers: []string{"127.0.0.1"}, Port: fallbackPort}
|
||||
srv, err := NewDNSServer(DNSServerConfig{UpstreamCfg: config, Port: testPort}, zone, nil)
|
||||
wt.AssertNoErr(t, err)
|
||||
defer srv.Stop()
|
||||
go srv.Start()
|
||||
time.Sleep(100 * time.Millisecond) // Allow sever goroutine to start
|
||||
|
||||
t.Logf("Creating a UDP and a TCP client")
|
||||
uc := new(dns.Client)
|
||||
uc.UDPSize = minUDPSize
|
||||
tc := new(dns.Client)
|
||||
tc.Net = "tcp"
|
||||
|
||||
t.Logf("Creating DNS query message")
|
||||
m := new(dns.Msg)
|
||||
m.RecursionDesired = true
|
||||
m.SetQuestion(nonLocalName, dns.TypeA)
|
||||
|
||||
t.Logf("Checking the fallback server at %s returns a truncated response with UDP", fallbackUDPAddr)
|
||||
r, _, err := uc.Exchange(m, fallbackUDPAddr)
|
||||
t.Logf("Got response from fallback server (UDP) with %d answers", len(r.Answer))
|
||||
t.Logf("Response:\n%+v\n", r)
|
||||
wt.AssertNoErr(t, err)
|
||||
wt.AssertTrue(t, r.MsgHdr.Truncated, "DNS truncated reponse flag")
|
||||
wt.AssertNotEqualInt(t, len(r.Answer), numAnswers, "number of answers (UDP)")
|
||||
|
||||
t.Logf("Checking the WeaveDNS server at %s returns a truncated reponse with UDP", dnsAddr)
|
||||
r, _, err = uc.Exchange(m, dnsAddr)
|
||||
t.Logf("UDP Response:\n%+v\n", r)
|
||||
wt.AssertNoErr(t, err)
|
||||
wt.AssertNotNil(t, r, "response")
|
||||
t.Logf("%d answers", len(r.Answer))
|
||||
wt.AssertTrue(t, r.MsgHdr.Truncated, "DNS truncated reponse flag")
|
||||
wt.AssertNotEqualInt(t, len(r.Answer), numAnswers, "number of answers (UDP)")
|
||||
|
||||
t.Logf("Checking the WeaveDNS server at %s does not return a truncated reponse with TCP", dnsAddr)
|
||||
r, _, err = tc.Exchange(m, dnsAddr)
|
||||
t.Logf("TCP Response:\n%+v\n", r)
|
||||
wt.AssertNoErr(t, err)
|
||||
wt.AssertNotNil(t, r, "response")
|
||||
t.Logf("%d answers", len(r.Answer))
|
||||
wt.AssertFalse(t, r.MsgHdr.Truncated, "DNS truncated response flag")
|
||||
wt.AssertEqualInt(t, len(r.Answer), numAnswers, "number of answers (TCP)")
|
||||
|
||||
t.Logf("Checking the WeaveDNS server at %s does not return a truncated reponse with UDP with a bigger buffer", dnsAddr)
|
||||
m.SetEdns0(testUDPBufSize, false)
|
||||
r, _, err = uc.Exchange(m, dnsAddr)
|
||||
t.Logf("UDP-large Response:\n%+v\n", r)
|
||||
wt.AssertNoErr(t, err)
|
||||
wt.AssertNotNil(t, r, "response")
|
||||
t.Logf("%d answers", len(r.Answer))
|
||||
wt.AssertNoErr(t, err)
|
||||
wt.AssertFalse(t, r.MsgHdr.Truncated, "DNS truncated response flag")
|
||||
wt.AssertEqualInt(t, len(r.Answer), numAnswers, "number of answers (UDP-long)")
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// perform a DNS query and assert the reply code, number or answers, etc
|
||||
func assertExchange(t *testing.T, z string, ty uint16, minAnswers int, maxAnswers int, expErr int) *dns.Msg {
|
||||
c := new(dns.Client)
|
||||
c.UDPSize = testUDPBufSize
|
||||
m := new(dns.Msg)
|
||||
m.RecursionDesired = true
|
||||
m.SetQuestion(z, ty)
|
||||
m.SetEdns0(testUDPBufSize, false) // we don't want to play with truncation here...
|
||||
|
||||
r, _, err := c.Exchange(m, fmt.Sprintf("127.0.0.1:%d", testPort))
|
||||
t.Logf("Response:\n%+v\n", r)
|
||||
wt.AssertNoErr(t, err)
|
||||
if minAnswers == 0 && maxAnswers == 0 {
|
||||
wt.AssertStatus(t, r.Rcode, expErr, "DNS response code")
|
||||
} else {
|
||||
wt.AssertStatus(t, r.Rcode, dns.RcodeSuccess, "DNS response code")
|
||||
}
|
||||
answers := len(r.Answer)
|
||||
if minAnswers >= 0 && answers < minAnswers {
|
||||
wt.Fatalf(t, "Number of answers >= %d", minAnswers)
|
||||
}
|
||||
if maxAnswers >= 0 && answers > maxAnswers {
|
||||
wt.Fatalf(t, "Number of answers <= %d", maxAnswers)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// run a UDP fallback server
|
||||
func runLocalUDPServer(t *testing.T, laddr string, handler dns.HandlerFunc) (*dns.Server, string, error) {
|
||||
t.Logf("Starting fallback UDP server at %s", laddr)
|
||||
pc, err := net.ListenPacket("udp", laddr)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
server := &dns.Server{PacketConn: pc, Handler: handler, ReadTimeout: 100 * time.Millisecond}
|
||||
|
||||
go func() {
|
||||
server.ActivateAndServe()
|
||||
pc.Close()
|
||||
}()
|
||||
|
||||
t.Logf("Fallback UDP server listening at %s", pc.LocalAddr())
|
||||
return server, pc.LocalAddr().String(), nil
|
||||
}
|
||||
|
||||
// run a TCP fallback server
|
||||
func runLocalTCPServer(t *testing.T, laddr string, handler dns.HandlerFunc) (*dns.Server, string, error) {
|
||||
t.Logf("Starting fallback TCP server at %s", laddr)
|
||||
laddrTCP, err := net.ResolveTCPAddr("tcp", laddr)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
l, err := net.ListenTCP("tcp", laddrTCP)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
server := &dns.Server{Listener: l, Handler: handler, ReadTimeout: 100 * time.Millisecond}
|
||||
|
||||
go func() {
|
||||
server.ActivateAndServe()
|
||||
l.Close()
|
||||
}()
|
||||
|
||||
t.Logf("Fallback TCP server listening at %s", l.Addr().String())
|
||||
return server, l.Addr().String(), nil
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package nameserver
|
||||
|
||||
type Status struct {
|
||||
Domain string
|
||||
Upstream []string
|
||||
Address string
|
||||
TTL uint32
|
||||
Entries []EntryStatus
|
||||
}
|
||||
|
||||
type EntryStatus struct {
|
||||
Hostname string
|
||||
Origin string
|
||||
ContainerID string
|
||||
Address string
|
||||
Version int
|
||||
Tombstone int64
|
||||
}
|
||||
|
||||
func NewStatus(ns *Nameserver, dnsServer *DNSServer) *Status {
|
||||
if dnsServer == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
ns.RLock()
|
||||
defer ns.RUnlock()
|
||||
|
||||
var entryStatusSlice []EntryStatus
|
||||
for _, entry := range ns.entries {
|
||||
entryStatusSlice = append(entryStatusSlice, EntryStatus{
|
||||
entry.Hostname,
|
||||
entry.Origin.String(),
|
||||
entry.ContainerID,
|
||||
entry.Addr.String(),
|
||||
entry.Version,
|
||||
entry.Tombstone})
|
||||
}
|
||||
|
||||
return &Status{
|
||||
dnsServer.domain,
|
||||
dnsServer.upstream.Servers,
|
||||
dnsServer.address,
|
||||
dnsServer.ttl,
|
||||
entryStatusSlice}
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
package nameserver
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/miekg/dns"
|
||||
. "github.com/weaveworks/weave/common"
|
||||
"net"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const (
|
||||
RDNSDomain = "in-addr.arpa."
|
||||
)
|
||||
|
||||
// +1 to also exclude a dot
|
||||
var rdnsDomainLen = len(RDNSDomain) + 1
|
||||
|
||||
type Lookup interface {
|
||||
LookupName(name string) (net.IP, error)
|
||||
LookupInaddr(inaddr string) (string, error)
|
||||
}
|
||||
|
||||
type Zone interface {
|
||||
AddRecord(ident string, name string, ip net.IP) error
|
||||
DeleteRecord(ident string, ip net.IP) error
|
||||
DeleteRecordsFor(ident string) error
|
||||
Lookup
|
||||
}
|
||||
|
||||
type Record struct {
|
||||
Ident string
|
||||
Name string
|
||||
IP net.IP
|
||||
}
|
||||
|
||||
// Very simple data structure for now, with linear searching.
|
||||
// TODO: make more sophisticated to improve performance.
|
||||
type ZoneDb struct {
|
||||
mx sync.RWMutex
|
||||
recs []Record
|
||||
}
|
||||
|
||||
type LookupError string
|
||||
|
||||
func (ops LookupError) Error() string {
|
||||
return "Unable to find " + string(ops)
|
||||
}
|
||||
|
||||
type DuplicateError struct {
|
||||
}
|
||||
|
||||
func (dup DuplicateError) Error() string {
|
||||
return "Tried to add a duplicate entry"
|
||||
}
|
||||
|
||||
func (zone *ZoneDb) indexOf(match func(Record) bool) int {
|
||||
for i, r := range zone.recs {
|
||||
if match(r) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func (zone *ZoneDb) String() string {
|
||||
zone.mx.RLock()
|
||||
defer zone.mx.RUnlock()
|
||||
var buf bytes.Buffer
|
||||
for _, r := range zone.recs {
|
||||
fmt.Fprintf(&buf, "%.12s %s %v\n", r.Ident, r.IP, r.Name)
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func (zone *ZoneDb) LookupName(name string) (net.IP, error) {
|
||||
zone.mx.RLock()
|
||||
defer zone.mx.RUnlock()
|
||||
for _, r := range zone.recs {
|
||||
if r.Name == name {
|
||||
return r.IP, nil
|
||||
}
|
||||
}
|
||||
return nil, LookupError(name)
|
||||
}
|
||||
|
||||
func (zone *ZoneDb) LookupInaddr(inaddr string) (string, error) {
|
||||
if revIP := net.ParseIP(inaddr[:len(inaddr)-rdnsDomainLen]); revIP != nil {
|
||||
revIP4 := revIP.To4()
|
||||
ip := []byte{revIP4[3], revIP4[2], revIP4[1], revIP4[0]}
|
||||
Debug.Printf("[zonedb] Looking for address: %+v", ip)
|
||||
zone.mx.RLock()
|
||||
defer zone.mx.RUnlock()
|
||||
for _, r := range zone.recs {
|
||||
if r.IP.Equal(ip) {
|
||||
return r.Name, nil
|
||||
}
|
||||
}
|
||||
return "", LookupError(inaddr)
|
||||
}
|
||||
Warning.Printf("[zonedb] Asked to reverse lookup %s", inaddr)
|
||||
return "", LookupError(inaddr)
|
||||
}
|
||||
|
||||
func (zone *ZoneDb) AddRecord(ident string, name string, ip net.IP) error {
|
||||
zone.mx.Lock()
|
||||
defer zone.mx.Unlock()
|
||||
fqdn := dns.Fqdn(name)
|
||||
pred := func(r Record) bool { return r.Name == fqdn && r.IP.Equal(ip) && r.Ident == ident }
|
||||
if index := zone.indexOf(pred); index != -1 {
|
||||
return DuplicateError{}
|
||||
}
|
||||
zone.recs = append(zone.recs, Record{ident, fqdn, ip})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (zone *ZoneDb) DeleteRecord(ident string, ip net.IP) error {
|
||||
zone.mx.Lock()
|
||||
defer zone.mx.Unlock()
|
||||
pred := func(r Record) bool { return r.Ident == ident && r.IP.Equal(ip) }
|
||||
if index := zone.indexOf(pred); index != -1 {
|
||||
zone.recs = append(zone.recs[:index], zone.recs[index+1:]...)
|
||||
return nil
|
||||
}
|
||||
return LookupError(ident)
|
||||
}
|
||||
|
||||
func (zone *ZoneDb) DeleteRecordsFor(ident string) error {
|
||||
zone.mx.Lock()
|
||||
defer zone.mx.Unlock()
|
||||
w := 0 // write index
|
||||
|
||||
for _, r := range zone.recs {
|
||||
if r.Ident != ident {
|
||||
zone.recs[w] = r
|
||||
w++
|
||||
}
|
||||
}
|
||||
zone.recs = zone.recs[:w]
|
||||
return nil
|
||||
}
|
||||
|
||||
func (zone *ZoneDb) ContainerDied(ident string) error {
|
||||
Info.Printf("[zonedb] Container %s down. Removing records", ident)
|
||||
return zone.DeleteRecordsFor(ident)
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
package nameserver
|
||||
|
||||
import (
|
||||
wt "github.com/weaveworks/weave/testing"
|
||||
"net"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestZone(t *testing.T) {
|
||||
var (
|
||||
containerID = "deadbeef"
|
||||
otherContainerID = "cowjuice"
|
||||
successTestName = "test1.weave."
|
||||
testAddr1 = "10.2.2.1/24"
|
||||
)
|
||||
|
||||
var zone = new(ZoneDb)
|
||||
|
||||
ip, _, _ := net.ParseCIDR(testAddr1)
|
||||
err := zone.AddRecord(containerID, successTestName, ip)
|
||||
wt.AssertNoErr(t, err)
|
||||
|
||||
// Add a few more records to make the job harder
|
||||
err = zone.AddRecord("abcdef0123", "adummy.weave.", net.ParseIP("10.2.0.1"))
|
||||
wt.AssertNoErr(t, err)
|
||||
err = zone.AddRecord("0123abcdef", "zdummy.weave.", net.ParseIP("10.2.0.2"))
|
||||
wt.AssertNoErr(t, err)
|
||||
|
||||
// Check that the address is now there.
|
||||
foundIP, err := zone.LookupName(successTestName)
|
||||
wt.AssertNoErr(t, err)
|
||||
|
||||
if !foundIP.Equal(ip) {
|
||||
t.Fatal("Unexpected result for", successTestName, foundIP)
|
||||
}
|
||||
|
||||
// See if we can find the address by IP.
|
||||
foundName, err := zone.LookupInaddr("1.2.2.10.in-addr.arpa.")
|
||||
wt.AssertNoErr(t, err)
|
||||
|
||||
if foundName != successTestName {
|
||||
t.Fatal("Unexpected result for", ip, foundName)
|
||||
}
|
||||
|
||||
err = zone.AddRecord(containerID, successTestName, ip)
|
||||
wt.AssertErrorType(t, err, (*DuplicateError)(nil), "duplicate add")
|
||||
|
||||
err = zone.AddRecord(otherContainerID, successTestName, ip)
|
||||
// Delete the record for the original container
|
||||
err = zone.DeleteRecord(containerID, ip)
|
||||
wt.AssertNoErr(t, err)
|
||||
|
||||
_, err = zone.LookupName(successTestName)
|
||||
wt.AssertNoErr(t, err)
|
||||
|
||||
err = zone.DeleteRecord(otherContainerID, ip)
|
||||
wt.AssertNoErr(t, err)
|
||||
|
||||
// Check that the address is not there now.
|
||||
_, err = zone.LookupName(successTestName)
|
||||
wt.AssertErrorType(t, err, (*LookupError)(nil), "after deleting record")
|
||||
|
||||
// Delete a record that isn't there
|
||||
err = zone.DeleteRecord(containerID, net.ParseIP("0.0.0.0"))
|
||||
wt.AssertErrorType(t, err, (*LookupError)(nil), "when deleting record that doesn't exist")
|
||||
}
|
||||
|
||||
func TestDeleteFor(t *testing.T) {
|
||||
var (
|
||||
id = "foobar"
|
||||
name = "foo.weave."
|
||||
addr1 = "10.2.2.3/24"
|
||||
addr2 = "10.2.7.8/24"
|
||||
)
|
||||
zone := new(ZoneDb)
|
||||
for _, addr := range []string{addr1, addr2} {
|
||||
ip, _, _ := net.ParseCIDR(addr)
|
||||
err := zone.AddRecord(id, name, ip)
|
||||
wt.AssertNoErr(t, err)
|
||||
}
|
||||
|
||||
_, err := zone.LookupName(name)
|
||||
wt.AssertNoErr(t, err)
|
||||
|
||||
err = zone.DeleteRecordsFor(id)
|
||||
_, err = zone.LookupName(name)
|
||||
wt.AssertErrorType(t, err, (*LookupError)(nil), "after deleting records for ident")
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package address
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/weaveworks/weave/common"
|
||||
)
|
||||
|
||||
// Using 32-bit integer to represent IPv4 address
|
||||
type Address uint32
|
||||
type Offset uint32
|
||||
|
||||
type Range struct {
|
||||
Start, End Address // [Start, End); Start <= End
|
||||
}
|
||||
|
||||
func NewRange(start Address, size Offset) Range {
|
||||
return Range{Start: start, End: Add(start, size)}
|
||||
}
|
||||
func (r Range) Size() Offset { return Subtract(r.End, r.Start) }
|
||||
func (r Range) String() string { return fmt.Sprintf("%s-%s", r.Start, r.End-1) }
|
||||
func (r Range) Overlaps(or Range) bool { return !(r.Start >= or.End || r.End <= or.Start) }
|
||||
func (r Range) Contains(addr Address) bool { return addr >= r.Start && addr < r.End }
|
||||
|
||||
func (r Range) AsCIDRString() string {
|
||||
prefixLen := 32
|
||||
for size := r.Size(); size > 1; size = size / 2 {
|
||||
if size%2 != 0 { // Size not a power of two; cannot be expressed as a CIDR.
|
||||
return r.String()
|
||||
}
|
||||
prefixLen--
|
||||
}
|
||||
return CIDR{Start: r.Start, PrefixLen: prefixLen}.String()
|
||||
}
|
||||
|
||||
type CIDR struct {
|
||||
Start Address
|
||||
PrefixLen int
|
||||
}
|
||||
|
||||
func ParseIP(s string) (Address, error) {
|
||||
if ip := net.ParseIP(s); ip != nil {
|
||||
return FromIP4(ip), nil
|
||||
}
|
||||
return 0, &net.ParseError{Type: "IP Address", Text: s}
|
||||
}
|
||||
|
||||
func ParseCIDR(s string) (Address, CIDR, error) {
|
||||
if ip, ipnet, err := net.ParseCIDR(s); err != nil {
|
||||
return 0, CIDR{}, err
|
||||
} else if ipnet.IP.To4() == nil {
|
||||
return 0, CIDR{}, &net.ParseError{Type: "Non-IPv4 address not supported", Text: s}
|
||||
} else {
|
||||
prefixLen, _ := ipnet.Mask.Size()
|
||||
return FromIP4(ip), CIDR{Start: FromIP4(ipnet.IP), PrefixLen: prefixLen}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (cidr CIDR) Size() Offset { return 1 << uint(32-cidr.PrefixLen) }
|
||||
|
||||
func (cidr CIDR) Range() Range {
|
||||
return NewRange(cidr.Start, cidr.Size())
|
||||
}
|
||||
func (cidr CIDR) HostRange() Range {
|
||||
// Respect RFC1122 exclusions of first and last addresses
|
||||
return NewRange(cidr.Start+1, cidr.Size()-2)
|
||||
}
|
||||
|
||||
func (cidr CIDR) String() string {
|
||||
return fmt.Sprintf("%s/%d", cidr.Start.String(), cidr.PrefixLen)
|
||||
}
|
||||
|
||||
// FromIP4 converts an ipv4 address to our integer address type
|
||||
func FromIP4(ip4 net.IP) (r Address) {
|
||||
for _, b := range ip4.To4() {
|
||||
r <<= 8
|
||||
r |= Address(b)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// IP4 converts our integer address type to an ipv4 address
|
||||
func (addr Address) IP4() (r net.IP) {
|
||||
r = make([]byte, net.IPv4len)
|
||||
for i := 3; i >= 0; i-- {
|
||||
r[i] = byte(addr)
|
||||
addr >>= 8
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (addr Address) MarshalJSON() ([]byte, error) {
|
||||
return []byte(fmt.Sprintf("%q", addr.String())), nil
|
||||
}
|
||||
|
||||
func (addr Address) String() string {
|
||||
return addr.IP4().String()
|
||||
}
|
||||
|
||||
func Add(addr Address, i Offset) Address {
|
||||
return addr + Address(i)
|
||||
}
|
||||
|
||||
func Subtract(a, b Address) Offset {
|
||||
common.Assert(a >= b)
|
||||
return Offset(a - b)
|
||||
}
|
||||
|
||||
func Min(a, b Offset) Offset {
|
||||
if a > b {
|
||||
return b
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
func (addr Address) Reverse() Address {
|
||||
return ((addr >> 24) & 0xff) | // move byte 3 to byte 0
|
||||
((addr << 8) & 0xff0000) | // move byte 1 to byte 2
|
||||
((addr >> 8) & 0xff00) | // move byte 2 to byte 1
|
||||
((addr << 24) & 0xff000000) // byte 0 to byte 3
|
||||
}
|
||||
@@ -3,18 +3,75 @@ package net
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
"syscall"
|
||||
|
||||
"github.com/vishvananda/netlink"
|
||||
)
|
||||
|
||||
func EnsureInterface(ifaceName string, wait int) (iface *net.Interface, err error) {
|
||||
if iface, err = findInterface(ifaceName); err == nil || wait == 0 {
|
||||
return
|
||||
// Wait for an interface to come up.
|
||||
func EnsureInterface(ifaceName string) (*net.Interface, error) {
|
||||
iface, err := ensureInterface(ifaceName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for ; err != nil && wait > 0; wait-- {
|
||||
time.Sleep(1 * time.Second)
|
||||
iface, err = findInterface(ifaceName)
|
||||
return iface, err
|
||||
}
|
||||
|
||||
func ensureInterface(ifaceName string) (*net.Interface, error) {
|
||||
ch := make(chan netlink.LinkUpdate)
|
||||
done := make(chan struct{})
|
||||
defer close(done)
|
||||
if err := netlink.LinkSubscribe(ch, done); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return
|
||||
// check for currently-existing interface after subscribing, to avoid race
|
||||
if iface, err := findInterface(ifaceName); err == nil {
|
||||
return iface, nil
|
||||
}
|
||||
for update := range ch {
|
||||
if ifaceName == update.Link.Attrs().Name && update.IfInfomsg.Flags&syscall.IFF_UP != 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
iface, err := findInterface(ifaceName)
|
||||
return iface, err
|
||||
}
|
||||
|
||||
// Wait for an interface to come up and have a route added to the multicast subnet.
|
||||
// This matches the behaviour in 'weave attach', which is the only context in which
|
||||
// we expect this to be called. If you change one, change the other to match.
|
||||
func EnsureInterfaceAndMcastRoute(ifaceName string) (*net.Interface, error) {
|
||||
iface, err := ensureInterface(ifaceName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ch := make(chan netlink.RouteUpdate)
|
||||
done := make(chan struct{})
|
||||
defer close(done)
|
||||
if err := netlink.RouteSubscribe(ch, done); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dest := net.IPv4(224, 0, 0, 0)
|
||||
check := func(route netlink.Route) bool {
|
||||
return route.Dst != nil && route.Dst.IP.Equal(dest)
|
||||
}
|
||||
// check for currently-existing route after subscribing, to avoid race
|
||||
routes, err := netlink.RouteList(nil, netlink.FAMILY_V4)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, route := range routes {
|
||||
if check(route) {
|
||||
return iface, nil
|
||||
}
|
||||
}
|
||||
for update := range ch {
|
||||
if check(update.Route) {
|
||||
return iface, nil
|
||||
}
|
||||
}
|
||||
// should never get here
|
||||
return iface, nil
|
||||
}
|
||||
|
||||
func findInterface(ifaceName string) (iface *net.Interface, err error) {
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package net
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/vishvananda/netlink"
|
||||
)
|
||||
|
||||
// A network is considered free if it does not overlap any existing
|
||||
// routes on this host. This is the same approach taken by Docker.
|
||||
func CheckNetworkFree(subnet *net.IPNet, ignoreIfaceNames map[string]struct{}) error {
|
||||
return forEachRoute(ignoreIfaceNames, func(route netlink.Route) error {
|
||||
if route.Dst != nil && overlaps(route.Dst, subnet) {
|
||||
return fmt.Errorf("Network %s overlaps with existing route %s on host.", subnet, route.Dst)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// Two networks overlap if the start-point of one is inside the other.
|
||||
func overlaps(n1, n2 *net.IPNet) bool {
|
||||
return n1.Contains(n2.IP) || n2.Contains(n1.IP)
|
||||
}
|
||||
|
||||
// For a specific address, we only care if it is actually *inside* an
|
||||
// existing route, because weave-local traffic never hits IP routing.
|
||||
func CheckAddressOverlap(addr net.IP, ignoreIfaceNames map[string]struct{}) error {
|
||||
return forEachRoute(ignoreIfaceNames, func(route netlink.Route) error {
|
||||
if route.Dst != nil && route.Dst.Contains(addr) {
|
||||
return fmt.Errorf("Address %s overlaps with existing route %s on host.", addr, route.Dst)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func forEachRoute(ignoreIfaceNames map[string]struct{}, check func(r netlink.Route) error) error {
|
||||
ignoreIfaceIndices := make(map[int]struct{})
|
||||
for ifaceName := range ignoreIfaceNames {
|
||||
if iface, err := net.InterfaceByName(ifaceName); err == nil {
|
||||
ignoreIfaceIndices[iface.Index] = struct{}{}
|
||||
}
|
||||
}
|
||||
routes, err := netlink.RouteList(nil, netlink.FAMILY_V4)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, route := range routes {
|
||||
if _, found := ignoreIfaceIndices[route.LinkIndex]; found {
|
||||
continue
|
||||
}
|
||||
if err := check(route); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package net
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestOverlap(t *testing.T) {
|
||||
_, subnet1, _ := net.ParseCIDR("10.0.3.0/24")
|
||||
_, subnet2, _ := net.ParseCIDR("10.0.4.0/24")
|
||||
_, subnet3, _ := net.ParseCIDR("10.0.3.128/25")
|
||||
_, subnet4, _ := net.ParseCIDR("10.0.3.192/25")
|
||||
_, universe, _ := net.ParseCIDR("10.0.0.0/8")
|
||||
require.False(t, overlaps(subnet1, subnet2))
|
||||
require.False(t, overlaps(subnet2, subnet1))
|
||||
require.True(t, overlaps(subnet1, subnet1))
|
||||
require.True(t, overlaps(subnet1, subnet3))
|
||||
require.True(t, overlaps(subnet1, subnet4))
|
||||
require.False(t, overlaps(subnet2, subnet4))
|
||||
require.False(t, overlaps(subnet4, subnet2))
|
||||
require.True(t, overlaps(universe, subnet1))
|
||||
require.True(t, overlaps(subnet1, universe))
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
# Weave network driver extension for Docker
|
||||
|
||||
A [plugin](http://docs.docker.com/engine/extend/plugin_api/) to
|
||||
integrate [weave Net](http://weave.works/net/) with Docker.
|
||||
|
||||
The plugin runs automatically when you `weave launch`, provided your
|
||||
Docker daemon is version 1.9 or newer.
|
||||
|
||||
More detail [here](http://docs.weave.works/weave/latest_release/plugin.html).
|
||||
@@ -0,0 +1,69 @@
|
||||
package ipamplugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/docker/libnetwork/ipamapi"
|
||||
"github.com/docker/libnetwork/netlabel"
|
||||
godocker "github.com/fsouza/go-dockerclient"
|
||||
"github.com/weaveworks/weave/api"
|
||||
. "github.com/weaveworks/weave/common"
|
||||
"github.com/weaveworks/weave/common/docker"
|
||||
)
|
||||
|
||||
const (
|
||||
WeaveContainer = "weave"
|
||||
)
|
||||
|
||||
type ipam struct {
|
||||
client *docker.Client
|
||||
weave *api.Client
|
||||
}
|
||||
|
||||
func NewIpam(client *docker.Client, version string) (ipamapi.Ipam, error) {
|
||||
resolver := func() (string, error) {
|
||||
addr, err := client.GetContainerIP(WeaveContainer)
|
||||
if _, ok := err.(*godocker.NoSuchContainer); ok {
|
||||
return "", fmt.Errorf("%s container is not present. Have you launched it?", WeaveContainer)
|
||||
}
|
||||
return addr, err
|
||||
}
|
||||
return &ipam{client: client, weave: api.NewClientWithResolver(resolver)}, nil
|
||||
}
|
||||
|
||||
func (i *ipam) GetDefaultAddressSpaces() (string, string, error) {
|
||||
Log.Debugln("GetDefaultAddressSpaces")
|
||||
return "weavelocal", "weaveglobal", nil
|
||||
}
|
||||
|
||||
func (i *ipam) RequestPool(addressSpace, pool, subPool string, options map[string]string, v6 bool) (string, *net.IPNet, map[string]string, error) {
|
||||
Log.Debugln("RequestPool", addressSpace, pool, subPool, options)
|
||||
cidr, err := i.weave.DefaultSubnet()
|
||||
Log.Debugln("RequestPool returning ", cidr, err)
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
}
|
||||
// Pass back a fake "gateway address"; we don't actually use it,
|
||||
// so just give the network address.
|
||||
data := map[string]string{netlabel.Gateway: cidr.String()}
|
||||
return "weavepool", cidr, data, err
|
||||
}
|
||||
|
||||
func (i *ipam) ReleasePool(poolID string) error {
|
||||
Log.Debugln("ReleasePool", poolID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *ipam) RequestAddress(poolID string, address net.IP, options map[string]string) (*net.IPNet, map[string]string, error) {
|
||||
Log.Debugln("RequestAddress", poolID, address, options)
|
||||
// Pass magic string to weave IPAM, which then stores the address under its own string
|
||||
ip, err := i.weave.AllocateIP("_")
|
||||
Log.Debugln("allocateIP returned", ip, err)
|
||||
return ip, nil, err
|
||||
}
|
||||
|
||||
func (i *ipam) ReleaseAddress(poolID string, address net.IP) error {
|
||||
Log.Debugln("ReleaseAddress", poolID, address)
|
||||
return i.weave.ReleaseIP(address.String())
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/docker/libnetwork/drivers/remote/api"
|
||||
"github.com/docker/libnetwork/types"
|
||||
|
||||
. "github.com/weaveworks/weave/common"
|
||||
"github.com/weaveworks/weave/common/docker"
|
||||
"github.com/weaveworks/weave/common/odp"
|
||||
"github.com/weaveworks/weave/plugin/skel"
|
||||
|
||||
"github.com/vishvananda/netlink"
|
||||
)
|
||||
|
||||
const (
|
||||
WeaveContainer = "weave"
|
||||
WeaveBridge = "weave"
|
||||
)
|
||||
|
||||
type driver struct {
|
||||
version string
|
||||
nameserver string
|
||||
scope string
|
||||
noMulticastRoute bool
|
||||
sync.RWMutex
|
||||
endpoints map[string]struct{}
|
||||
}
|
||||
|
||||
func New(client *docker.Client, version string, nameserver string, scope string, noMulticastRoute bool) (skel.Driver, error) {
|
||||
driver := &driver{
|
||||
nameserver: nameserver,
|
||||
noMulticastRoute: noMulticastRoute,
|
||||
version: version,
|
||||
scope: scope,
|
||||
endpoints: make(map[string]struct{}),
|
||||
}
|
||||
|
||||
_, err := NewWatcher(client, driver)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return driver, nil
|
||||
}
|
||||
|
||||
func errorf(format string, a ...interface{}) error {
|
||||
Log.Errorf(format, a...)
|
||||
return fmt.Errorf(format, a...)
|
||||
}
|
||||
|
||||
// === protocol handlers
|
||||
|
||||
func (driver *driver) GetCapabilities() (*api.GetCapabilityResponse, error) {
|
||||
var caps = &api.GetCapabilityResponse{
|
||||
Scope: driver.scope,
|
||||
}
|
||||
Log.Debugf("Get capabilities: responded with %+v", caps)
|
||||
return caps, nil
|
||||
}
|
||||
|
||||
func (driver *driver) CreateNetwork(create *api.CreateNetworkRequest) error {
|
||||
Log.Debugf("Create network request %+v", create)
|
||||
Log.Infof("Create network %s", create.NetworkID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (driver *driver) DeleteNetwork(delete *api.DeleteNetworkRequest) error {
|
||||
Log.Debugf("Delete network request: %+v", delete)
|
||||
Log.Infof("Destroy network %s", delete.NetworkID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (driver *driver) CreateEndpoint(create *api.CreateEndpointRequest) (*api.CreateEndpointResponse, error) {
|
||||
Log.Debugf("Create endpoint request %+v", create)
|
||||
endID := create.EndpointID
|
||||
|
||||
if create.Interface == nil {
|
||||
return nil, fmt.Errorf("Not supported: creating an interface from within CreateEndpoint")
|
||||
}
|
||||
driver.Lock()
|
||||
driver.endpoints[endID] = struct{}{}
|
||||
driver.Unlock()
|
||||
resp := &api.CreateEndpointResponse{}
|
||||
|
||||
Log.Infof("Create endpoint %s %+v", endID, resp)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (driver *driver) DeleteEndpoint(deleteReq *api.DeleteEndpointRequest) error {
|
||||
Log.Debugf("Delete endpoint request: %+v", deleteReq)
|
||||
Log.Infof("Delete endpoint %s", deleteReq.EndpointID)
|
||||
driver.Lock()
|
||||
delete(driver.endpoints, deleteReq.EndpointID)
|
||||
driver.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (driver *driver) HasEndpoint(endpointID string) bool {
|
||||
driver.Lock()
|
||||
_, found := driver.endpoints[endpointID]
|
||||
driver.Unlock()
|
||||
return found
|
||||
}
|
||||
|
||||
func (driver *driver) EndpointInfo(req *api.EndpointInfoRequest) (*api.EndpointInfoResponse, error) {
|
||||
Log.Debugf("Endpoint info request: %+v", req)
|
||||
Log.Infof("Endpoint info %s", req.EndpointID)
|
||||
return &api.EndpointInfoResponse{Value: map[string]interface{}{}}, nil
|
||||
}
|
||||
|
||||
func (driver *driver) JoinEndpoint(j *api.JoinRequest) (*api.JoinResponse, error) {
|
||||
endID := j.EndpointID
|
||||
|
||||
maybeBridge, err := netlink.LinkByName(WeaveBridge)
|
||||
if err != nil {
|
||||
return nil, errorf(`bridge "%s" not present; did you launch weave?`, WeaveBridge)
|
||||
}
|
||||
|
||||
// create and attach local name to the bridge
|
||||
local := vethPair(endID[:5])
|
||||
local.Attrs().MTU = maybeBridge.Attrs().MTU
|
||||
if err := netlink.LinkAdd(local); err != nil {
|
||||
return nil, errorf("could not create veth pair: %s", err)
|
||||
}
|
||||
|
||||
switch maybeBridge.(type) {
|
||||
case *netlink.Bridge:
|
||||
if err := netlink.LinkSetMasterByIndex(local, maybeBridge.Attrs().Index); err != nil {
|
||||
return nil, errorf(`unable to set master: %s`, err)
|
||||
}
|
||||
case *netlink.GenericLink:
|
||||
if maybeBridge.Type() != "openvswitch" {
|
||||
Log.Errorf("device %s is %+v", WeaveBridge, maybeBridge)
|
||||
return nil, errorf(`device "%s" is of type "%s"`, WeaveBridge, maybeBridge.Type())
|
||||
}
|
||||
odp.AddDatapathInterface(WeaveBridge, local.Name)
|
||||
case *netlink.Device:
|
||||
Log.Warnf("kernel does not report what kind of device %s is, just %+v", WeaveBridge, maybeBridge)
|
||||
// Assume it's our openvswitch device, and the kernel has not been updated to report the kind.
|
||||
odp.AddDatapathInterface(WeaveBridge, local.Name)
|
||||
default:
|
||||
Log.Errorf("device %s is %+v", WeaveBridge, maybeBridge)
|
||||
return nil, errorf(`device "%s" not a bridge`, WeaveBridge)
|
||||
}
|
||||
if err := netlink.LinkSetUp(local); err != nil {
|
||||
return nil, errorf(`unable to bring veth up: %s`, err)
|
||||
}
|
||||
|
||||
ifname := &api.InterfaceName{
|
||||
SrcName: local.PeerName,
|
||||
DstPrefix: "ethwe",
|
||||
}
|
||||
|
||||
response := &api.JoinResponse{
|
||||
InterfaceName: ifname,
|
||||
}
|
||||
if driver.nameserver != "" {
|
||||
routeToDNS := api.StaticRoute{
|
||||
Destination: driver.nameserver + "/32",
|
||||
RouteType: types.CONNECTED,
|
||||
NextHop: "",
|
||||
}
|
||||
response.StaticRoutes = []api.StaticRoute{routeToDNS}
|
||||
}
|
||||
if !driver.noMulticastRoute {
|
||||
multicastRoute := api.StaticRoute{
|
||||
Destination: "224.0.0.0/4",
|
||||
RouteType: types.CONNECTED,
|
||||
}
|
||||
response.StaticRoutes = append(response.StaticRoutes, multicastRoute)
|
||||
}
|
||||
Log.Infof("Join endpoint %s:%s to %s", j.NetworkID, j.EndpointID, j.SandboxKey)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (driver *driver) LeaveEndpoint(leave *api.LeaveRequest) error {
|
||||
Log.Debugf("Leave request: %+v", leave)
|
||||
|
||||
local := vethPair(leave.EndpointID[:5])
|
||||
if err := netlink.LinkDel(local); err != nil {
|
||||
Log.Warningf("unable to delete veth on leave: %s", err)
|
||||
}
|
||||
Log.Infof("Leave %s:%s", leave.NetworkID, leave.EndpointID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (driver *driver) DiscoverNew(disco *api.DiscoveryNotification) error {
|
||||
Log.Debugf("Dicovery new notification: %+v", disco)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (driver *driver) DiscoverDelete(disco *api.DiscoveryNotification) error {
|
||||
Log.Debugf("Dicovery delete notification: %+v", disco)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ===
|
||||
|
||||
func vethPair(suffix string) *netlink.Veth {
|
||||
return &netlink.Veth{
|
||||
LinkAttrs: netlink.LinkAttrs{Name: "vethwl" + suffix},
|
||||
PeerName: "vethwg" + suffix,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
weaveapi "github.com/weaveworks/weave/api"
|
||||
. "github.com/weaveworks/weave/common"
|
||||
"github.com/weaveworks/weave/common/docker"
|
||||
)
|
||||
|
||||
const (
|
||||
WeaveDomain = "weave.local"
|
||||
)
|
||||
|
||||
type watcher struct {
|
||||
client *docker.Client
|
||||
weave *weaveapi.Client
|
||||
driver *driver
|
||||
}
|
||||
|
||||
type Watcher interface {
|
||||
}
|
||||
|
||||
func NewWatcher(client *docker.Client, driver *driver) (Watcher, error) {
|
||||
resolver := func() (string, error) { return client.GetContainerIP(WeaveContainer) }
|
||||
w := &watcher{client: client, driver: driver,
|
||||
weave: weaveapi.NewClientWithResolver(resolver)}
|
||||
err := client.AddObserver(w)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return w, nil
|
||||
}
|
||||
|
||||
func (w *watcher) ContainerStarted(id string) {
|
||||
Log.Debugf("Container started %s", id)
|
||||
info, err := w.client.InspectContainer(id)
|
||||
if err != nil {
|
||||
Log.Warningf("error inspecting container: %s", err)
|
||||
return
|
||||
}
|
||||
// check that it's on our network, via the endpointID
|
||||
for _, net := range info.NetworkSettings.Networks {
|
||||
if w.driver.HasEndpoint(net.EndpointID) {
|
||||
fqdn := fmt.Sprintf("%s.%s", info.Config.Hostname, info.Config.Domainname)
|
||||
if err := w.weave.RegisterWithDNS(id, fqdn, net.IPAddress); err != nil {
|
||||
Log.Warningf("unable to register with weaveDNS: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *watcher) ContainerDied(id string) {
|
||||
// don't need to do this as WeaveDNS removes names on container died anyway
|
||||
// (note by the time we get this event we can't see the EndpointID)
|
||||
}
|
||||
Loaded 100 of 272 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user