This question has been nicely addressed by the previous answers, so I'll just write about a method you can use if you want your tube to have custom cross-sections; or, like in this question, you need to have the tube as a bunch of Polygon[]
s.
(* Pixar method; http://jcgt.org/published/0006/01/01/ *)
orthogonalDirections[{p1_?VectorQ, p2_?VectorQ}] := Module[{s, w, w1, xx, yy, zz},
{xx, yy, zz} = Normalize[p2 - p1];
s = 2 UnitStep[zz] - 1; w = -1/(s + zz); w1 = xx yy w;
{{1 + s w xx^2, s w1, -s xx}, {w1, s + w yy^2, -yy}}]
orthogonalDirections[{p1_?VectorQ, p2_?VectorQ, p3_?VectorQ}] := Module[{d, u, v},
{u, v} = Normalize /@ {p3 - p2, p1 - p2};
If[Chop[Norm[u - v] Norm[u + v]] != 0,
d = (u + v)/2; Normalize /@ {d, Cross[u, d]},
orthogonalDirections[{p1, p2}]]]
extend[cs_, q_, d_, nrms_] :=
cs + Outer[Times, First[LinearSolve[Transpose[Prepend[-nrms, d]],
q - Transpose[cs]]], d]
(* for custom cross-sections *)
crossSection[pointList_?MatrixQ, r_, csList_?MatrixQ] := Module[{p1, p2},
{p1, p2} = Take[pointList, 2];
(p1 + #) & /@ (r csList.orthogonalDirections[{p1, p2}])] /;
Last[Dimensions[pointList]] == 3 && Last[Dimensions[csList]] == 2
(* for circular cross-sections *)
crossSection[pointList_?MatrixQ, r_, n_Integer] :=
crossSection[pointList, r, Composition[Through, {Cos, Sin}] /@ Range[0, 2 Pi, 2 Pi/n]]
(* approximate vertex normals, for a smooth appearance *)
vertNormals[vl_ /; ArrayQ[vl, 3, NumericQ]] := Block[{mdu, mdv, msh},
msh = ArrayPad[#, {{1, 1}, {1, 1}}, "Extrapolated", InterpolationOrder -> 2] & /@
Transpose[vl, {2, 3, 1}];
mdu = ListCorrelate[{{1, 0, -1}}/2, #, {{-2, 1}, {2, -1}}, 0] & /@ msh;
mdv = ListCorrelate[{{-1}, {0}, {1}}/2, #, {{1, -2}, {-1, 2}}, 0] & /@ msh;
MapThread[Composition[Normalize, Cross], Transpose[{mdu, mdv}, {1, 4, 2, 3}], 2]]
MakePolygons[vl_ /; ArrayQ[vl, 3, NumericQ], OptionsPattern[{"Normals" -> True}]] :=
Module[{dims = Most[Dimensions[vl]]},
GraphicsComplex[Apply[Join, vl],
Polygon[Flatten[Apply[Join[Reverse[#1], #2] &,
Partition[Partition[Range[Times @@ dims], Last[dims]],
{2, 2}, {1, 1}], {2}], 1]],
If[TrueQ[OptionValue["Normals"] /. Automatic -> True],
VertexNormals -> Apply[Join, vertNormals[vl]],
Unevaluated[]]]]
Options[TubePolygons] = {"Normals" -> True, "Scale" -> 1.};
TubePolygons[path_?MatrixQ, cs : (_Integer | _?MatrixQ), OptionsPattern[]] :=
With[{p3 = PadRight[path, {Automatic, 3}]},
MakePolygons[FoldList[Function[{p, t},
extend[p, t[[2]], t[[2]] - t[[1]],
orthogonalDirections[t]]],
crossSection[p3, OptionValue["Scale"], cs],
Partition[p3, 3, 1, {1, 2}, {}]],
"Normals" -> OptionValue["Normals"]]]
Try it out:
path = First @ Cases[ParametricPlot3D[BSplineFunction[
{{0, 0, 0}, {1, 1, 1}, {2, -1, -1}, {3, 0, 1}, {4, 1, -1}}][u] // Evaluate,
{u, 0, 1}, MaxRecursion -> 1], Line[l_] :> l, Infinity];
cs = First @ Cases[ParametricPlot[
BSplineFunction[{{0., 0.}, {0.25, 0.}, {0.5, 0.125}, {0.25, 0.25}, {0., 0.25}},
SplineClosed -> True][u] // Evaluate,
{u, 0, 1}, MaxRecursion -> 1], Line[l_] :> l, Infinity];
Graphics3D[{EdgeForm[], TubePolygons[path, cs]}, Boxed -> False]
Of course, you can elect to have a circular cross section, as is usual:
Graphics3D[{EdgeForm[], TubePolygons[path, 20, "Scale" -> .2]}, Boxed -> False]